[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# Functional Programming and Monads in Go
|
||||
|
||||
## What is Functional Programming?
|
||||
|
||||
Functional programming (FP) treats computation as evaluation of mathematical functions. Core principles:
|
||||
|
||||
- **Immutability** — data doesn't change after creation; transformations produce new values
|
||||
- **Pure functions** — same input always produces same output, no side effects
|
||||
- **Composition** — build complex behavior by chaining simple functions
|
||||
- **Types as documentation** — types express constraints and invariants
|
||||
|
||||
Go isn't a pure FP language, but Go 1.18+ generics make FP patterns practical. samber/mo brings the most battle-tested FP abstractions — monads — to Go.
|
||||
|
||||
## What is a Monad?
|
||||
|
||||
A monad is a design pattern (not a class or interface) that:
|
||||
|
||||
1. **Wraps a value** in a context (Option wraps "maybe absent", Result wraps "maybe failed")
|
||||
2. **Chains operations** that transform the wrapped value without unwrapping it
|
||||
3. **Handles the context automatically** — if an Option is None, Map/FlatMap skip the transformation; if a Result is Err, subsequent Maps short-circuit
|
||||
|
||||
Think of it as a **container with a policy**: "I hold a value, and I know what to do when operations succeed or fail."
|
||||
|
||||
### The Railway Metaphor
|
||||
|
||||
Imagine two parallel railway tracks:
|
||||
|
||||
- **Happy track** (top): data flows through transformations successfully
|
||||
- **Error track** (bottom): once something goes wrong, the train switches to the error track and skips remaining transformations
|
||||
|
||||
```
|
||||
Input → [Transform A] → [Transform B] → [Transform C] → Output
|
||||
↓ (error) (skipped) (skipped)
|
||||
Error track ────────────────────────────────────→ Error
|
||||
```
|
||||
|
||||
This is exactly how Result.Map and Result.FlatMap work — errors propagate automatically without explicit if/else checks.
|
||||
|
||||
## Why Monads Are Valuable in Go
|
||||
|
||||
### 1. Compile-Time Nil Safety (Option)
|
||||
|
||||
Go's type system doesn't distinguish "this pointer could be nil" from "this pointer is always valid". Option[T] makes this explicit:
|
||||
|
||||
```go
|
||||
// Without mo — caller must remember to check nil
|
||||
func FindUser(id string) *User { ... } // might return nil
|
||||
|
||||
// With mo — the type TELLS you it might be absent
|
||||
func FindUser(id string) mo.Option[User] { ... } // caller must handle None
|
||||
```
|
||||
|
||||
The type signature is the documentation. No nil pointer panics at runtime — the compiler forces you to handle absence.
|
||||
|
||||
### 2. Railway-Oriented Error Handling (Result)
|
||||
|
||||
Go's idiomatic error handling requires checking errors at every step:
|
||||
|
||||
```go
|
||||
// Without mo — repetitive error checking
|
||||
data, err := readFile(path)
|
||||
if err != nil { return err }
|
||||
config, err := parseConfig(data)
|
||||
if err != nil { return err }
|
||||
validated, err := validate(config)
|
||||
if err != nil { return err }
|
||||
```
|
||||
|
||||
With Result and `mo.Do`, errors short-circuit through the chain:
|
||||
|
||||
```go
|
||||
// With mo — errors propagate automatically via Do notation
|
||||
result := mo.Do(func() Config {
|
||||
data := mo.TupleToResult(readFile(path)).MustGet()
|
||||
config := mo.TupleToResult(parseConfig(data)).MustGet()
|
||||
validated := mo.TupleToResult(validate(config)).MustGet()
|
||||
return validated
|
||||
})
|
||||
```
|
||||
|
||||
Same logic, less boilerplate. The error path is handled by the monad — any `MustGet()` failure short-circuits to `Err`.
|
||||
|
||||
**Note:** Direct `.Map`/`.FlatMap` methods cannot change the type parameter (Go methods cannot introduce new generic types). For type-changing pipelines, use sub-package `result.Pipe` functions or `mo.Do` notation as shown above.
|
||||
|
||||
### 3. Composable Pipelines
|
||||
|
||||
Monads compose naturally. You can build complex data transformations from simple, testable pieces:
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo/option"
|
||||
|
||||
result := option.Pipe3(
|
||||
getUserOption(id),
|
||||
option.Map(func(u User) string { return u.Email }),
|
||||
option.FlatMap(func(email string) mo.Option[string] {
|
||||
if isValid(email) { return mo.Some(email) }
|
||||
return mo.None[string]()
|
||||
}),
|
||||
option.Map(func(email string) EmailAddress { return NewEmailAddress(email) }),
|
||||
)
|
||||
```
|
||||
|
||||
Each step is a pure function. The pipeline handles None propagation. Each step is independently testable.
|
||||
|
||||
## The Three Core Monads
|
||||
|
||||
### Option — Represents Absence
|
||||
|
||||
**Problem it solves:** nil pointer panics, ambiguous zero values.
|
||||
|
||||
| Concept | Go without mo | Go with mo |
|
||||
| ------------- | --------------------- | ------------------------- |
|
||||
| Value present | `*User` (non-nil) | `mo.Some(user)` |
|
||||
| Value absent | `*User` (nil) | `mo.None[User]()` |
|
||||
| Safe access | `if u != nil { ... }` | `opt.OrElse(defaultUser)` |
|
||||
| Transform | manual nil check | `opt.Map(transform)` |
|
||||
|
||||
Use Option when: a value might legitimately be absent (nullable DB columns, optional config, cache lookups).
|
||||
|
||||
Don't use Option when: a zero value is meaningful (empty string is valid, 0 is a valid count).
|
||||
|
||||
### Result — Represents Fallibility
|
||||
|
||||
**Problem it solves:** verbose error checking, lost error context in chains.
|
||||
|
||||
| Concept | Go without mo | Go with mo |
|
||||
| --------- | ---------------------------- | ----------------------------- |
|
||||
| Success | `return value, nil` | `mo.Ok(value)` |
|
||||
| Failure | `return zero, err` | `mo.Err[T](err)` |
|
||||
| Chain ops | `if err != nil` at each step | `.Map(...)` / `.FlatMap(...)` |
|
||||
| Default | manual fallback | `.OrElse(default)` |
|
||||
|
||||
Use Result when: you're chaining multiple fallible operations and want errors to propagate automatically.
|
||||
|
||||
Don't use Result when: you need to inspect or modify the error at each step (standard Go error handling is more explicit).
|
||||
|
||||
### Either — Represents Alternatives
|
||||
|
||||
**Problem it solves:** functions that legitimately return one of two types.
|
||||
|
||||
| Concept | Example |
|
||||
| ---------------------- | --------------------------------- |
|
||||
| Cached vs fresh data | `Either[CachedUser, FreshUser]` |
|
||||
| Sync vs async result | `Either[SyncResult, AsyncResult]` |
|
||||
| Left vs right strategy | `Either[StrategyA, StrategyB]` |
|
||||
|
||||
Use Either when: both outcomes are valid, neither is an "error". If one side is always an error, use Result instead.
|
||||
|
||||
## When to Use mo vs Plain Go
|
||||
|
||||
**Use mo when:**
|
||||
|
||||
- You're building data transformation pipelines with multiple steps
|
||||
- You need type-safe nullable values (especially in JSON/DB models)
|
||||
- Error handling chains become repetitive
|
||||
- You want to make impossible states unrepresentable in the type system
|
||||
|
||||
**Stick with plain Go when:**
|
||||
|
||||
- Simple one-step operations where `if err != nil` is clear enough
|
||||
- Performance-critical hot paths (monads add thin allocation overhead)
|
||||
- Your team isn't familiar with FP concepts (readability > cleverness)
|
||||
- The operation has complex error recovery at each step (explicit handling is clearer)
|
||||
Reference in New Issue
Block a user