[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
# Advanced Types Reference
|
||||
|
||||
These types are less commonly used than Option/Result/Either but provide powerful abstractions for specific scenarios.
|
||||
|
||||
## Type Hierarchy
|
||||
|
||||
```
|
||||
Synchronous Asynchronous
|
||||
----------- ------------
|
||||
IO[T] (no error) → Task[T] (no error) → Future[T]
|
||||
IOEither[T] (with error) → TaskEither[T] (with error) → Future[T]
|
||||
```
|
||||
|
||||
- **IO** wraps a synchronous side-effecting computation
|
||||
- **Task** wraps an asynchronous side-effecting computation (returns a Future)
|
||||
- **Future** represents a value that will be available later
|
||||
- **Either** variants add error handling capability
|
||||
|
||||
## Future[T] — Asynchronous Values
|
||||
|
||||
Represents a value that may not yet be available. Similar to JavaScript's Promise.
|
||||
|
||||
### Constructor
|
||||
|
||||
```go
|
||||
future := mo.NewFuture(func(resolve func(int), reject func(error)) {
|
||||
// runs asynchronously
|
||||
result, err := expensiveComputation()
|
||||
if err != nil {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Chaining
|
||||
|
||||
```go
|
||||
future.
|
||||
Then(func(v int) (int, error) {
|
||||
return v * 2, nil // transform on success
|
||||
}).
|
||||
Catch(func(err error) (int, error) {
|
||||
return 0, err // handle error
|
||||
}).
|
||||
Finally(func(v int, err error) (int, error) {
|
||||
// always runs, regardless of success/failure
|
||||
log.Println("Done")
|
||||
return v, err
|
||||
})
|
||||
```
|
||||
|
||||
### Collecting Results
|
||||
|
||||
```go
|
||||
value, err := future.Collect() // blocks until resolved
|
||||
result := future.Result() // blocks, returns Result[T]
|
||||
either := future.Either() // blocks, returns Either[error, T]
|
||||
```
|
||||
|
||||
### Cancellation
|
||||
|
||||
```go
|
||||
future.Cancel() // terminates the future chain
|
||||
```
|
||||
|
||||
## IO[T] — Synchronous Side Effects
|
||||
|
||||
Wraps a function that performs side effects. The computation is lazy — it only runs when `Run()` is called. IO never fails.
|
||||
|
||||
### Variants by Parameter Count
|
||||
|
||||
```go
|
||||
// No parameters
|
||||
io := mo.NewIO(func() string { return "hello" })
|
||||
result := io.Run() // "hello"
|
||||
|
||||
// 1 parameter
|
||||
io1 := mo.NewIO1(func(name string) string { return "hello " + name })
|
||||
result := io1.Run("Alice") // "hello Alice"
|
||||
|
||||
// 2-5 parameters (IO2, IO3, IO4, IO5)
|
||||
io2 := mo.NewIO2(func(a, b int) int { return a + b })
|
||||
result := io2.Run(1, 2) // 3
|
||||
```
|
||||
|
||||
## IOEither[T] — Synchronous Side Effects with Errors
|
||||
|
||||
Like IO but the computation can fail. The callback must return `Either[error, R]`, not `(R, error)`.
|
||||
|
||||
```go
|
||||
io := mo.NewIOEither(func() mo.Either[error, string] {
|
||||
data, err := os.ReadFile("config.yaml")
|
||||
if err != nil {
|
||||
return mo.Left[error, string](err)
|
||||
}
|
||||
return mo.Right[error, string](string(data))
|
||||
})
|
||||
|
||||
either := io.Run() // Either[error, string]
|
||||
```
|
||||
|
||||
### Variants by Parameter Count
|
||||
|
||||
```go
|
||||
// 1 parameter
|
||||
io1 := mo.NewIOEither1(func(path string) mo.Either[error, string] {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return mo.Left[error, string](err)
|
||||
}
|
||||
return mo.Right[error, string](string(data))
|
||||
})
|
||||
either := io1.Run("config.yaml") // Either[error, string]
|
||||
|
||||
// IOEither2 through IOEither5 follow the same pattern
|
||||
```
|
||||
|
||||
## Task[T] — Asynchronous Computations
|
||||
|
||||
Lazy async computation — `Run()` calls the wrapped function, which returns a `*Future[T]`. Never fails.
|
||||
|
||||
```go
|
||||
task := mo.NewTask(func() *mo.Future[int] {
|
||||
return mo.NewFuture(func(resolve func(int), reject func(error)) {
|
||||
time.Sleep(time.Second)
|
||||
resolve(42)
|
||||
})
|
||||
})
|
||||
|
||||
future := task.Run() // executes the function, returns *Future[int]
|
||||
value, err := future.Collect() // blocks until done
|
||||
```
|
||||
|
||||
Note: `NewTask` accepts `func() *Future[R]` — it wraps a Future-producing function for lazy execution.
|
||||
|
||||
### From IO
|
||||
|
||||
```go
|
||||
io := mo.NewIO(func() int { return 42 })
|
||||
task := mo.NewTaskFromIO(io) // wrap IO as async Task
|
||||
```
|
||||
|
||||
### Variants by Parameter Count
|
||||
|
||||
```go
|
||||
task1 := mo.NewTask1(func(n int) *mo.Future[int] {
|
||||
return mo.NewFuture(func(resolve func(int), reject func(error)) {
|
||||
resolve(n * 2)
|
||||
})
|
||||
})
|
||||
future := task1.Run(21) // *Future[int] resolving to 42
|
||||
```
|
||||
|
||||
## TaskEither[T] — Async Computations with Errors
|
||||
|
||||
Like Task but the computation can fail. Combines Task semantics with error handling.
|
||||
|
||||
```go
|
||||
te := mo.NewTaskEither(func() *mo.Future[string] {
|
||||
return mo.NewFuture(func(resolve func(string), reject func(error)) {
|
||||
resp, err := http.Get("https://api.example.com/data")
|
||||
if err != nil {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(string(body))
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Note: Like `NewTask`, `NewTaskEither` accepts `func() *Future[R]`. The difference is in the methods available on the returned type — TaskEither provides `Match`, `OrElse`, `ToEither`, and `ToTask`.
|
||||
|
||||
### Methods
|
||||
|
||||
```go
|
||||
te.OrElse("fallback") // blocks, returns value or fallback
|
||||
te.ToEither() // blocks, returns Either[error, T]
|
||||
te.ToTask("fallback") // converts to Task (uses fallback on error)
|
||||
te.Match(
|
||||
func(err error) mo.Either[error, string] { ... }, // on error
|
||||
func(v string) mo.Either[error, string] { ... }, // on success
|
||||
)
|
||||
```
|
||||
|
||||
## State[S, A] — Stateful Computations
|
||||
|
||||
Represents a computation that threads state through a series of operations. The state type S flows through the computation while producing result values of type A.
|
||||
|
||||
### Constructor
|
||||
|
||||
```go
|
||||
// State computation: takes state, returns (result, newState)
|
||||
counter := mo.NewState(func(count int) (string, int) {
|
||||
return fmt.Sprintf("count=%d", count), count + 1
|
||||
})
|
||||
|
||||
result, newState := counter.Run(0) // ("count=0", 1)
|
||||
```
|
||||
|
||||
### ReturnState — wrap a value without modifying state
|
||||
|
||||
```go
|
||||
state := mo.ReturnState[int, string]("hello")
|
||||
result, s := state.Run(42) // ("hello", 42) — state unchanged
|
||||
```
|
||||
|
||||
### State Manipulation
|
||||
|
||||
```go
|
||||
// Get — return current state as result
|
||||
getter := mo.NewState(func(s int) (int, int) { return s, s })
|
||||
|
||||
// Put — replace state
|
||||
putter := state.Put(100)
|
||||
_, s := putter.Run(0) // (_, 100)
|
||||
|
||||
// Modify — transform state
|
||||
modified := state.Modify(func(s int) int { return s * 2 })
|
||||
_, s := modified.Run(5) // (_, 10)
|
||||
```
|
||||
|
||||
### Chaining State Computations
|
||||
|
||||
State is useful for accumulating results while threading context:
|
||||
|
||||
```go
|
||||
// Parse tokens while tracking position
|
||||
type ParseState struct {
|
||||
Input string
|
||||
Position int
|
||||
}
|
||||
|
||||
parseChar := mo.NewState(func(s ParseState) (byte, ParseState) {
|
||||
ch := s.Input[s.Position]
|
||||
return ch, ParseState{Input: s.Input, Position: s.Position + 1}
|
||||
})
|
||||
```
|
||||
|
||||
## When to Use Advanced Types
|
||||
|
||||
| Type | Use when... |
|
||||
| ---------- | ------------------------------------------------------------- |
|
||||
| Future | You need async computation with chaining (Then/Catch/Finally) |
|
||||
| IO | You want to defer and compose synchronous side effects |
|
||||
| IOEither | Deferred side effects that can fail |
|
||||
| Task | Deferred async computation (lazy Future) |
|
||||
| TaskEither | Deferred async computation that can fail |
|
||||
| State | Threading state through a series of pure computations |
|
||||
|
||||
Most Go projects only need **Option**, **Result**, and **Either**. The advanced types are valuable when building functional pipelines or when you want explicit control over when side effects execute.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Either[L, R] API Reference
|
||||
|
||||
A discriminated union representing a value of one of two possible types. By convention, Left is the "alternative" path and Right is the "primary" path, but neither implies success or failure.
|
||||
|
||||
## Constructors
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------- | --------------------------- |
|
||||
| `mo.Left[L, R](value L)` | Creates a left-side Either |
|
||||
| `mo.Right[L, R](value R)` | Creates a right-side Either |
|
||||
|
||||
## Type Checking
|
||||
|
||||
| Method | Returns | Description |
|
||||
| ----------- | ------- | -------------------------------------- |
|
||||
| `IsLeft()` | `bool` | True if the value is on the left side |
|
||||
| `IsRight()` | `bool` | True if the value is on the right side |
|
||||
|
||||
## Value Extraction
|
||||
|
||||
| Method | Returns | Description |
|
||||
| --- | --- | --- |
|
||||
| `Left()` | `(L, bool)` | Left value and whether it exists |
|
||||
| `Right()` | `(R, bool)` | Right value and whether it exists |
|
||||
| `MustLeft()` | `L` | Left value or panics |
|
||||
| `MustRight()` | `R` | Right value or panics |
|
||||
| `LeftOrElse(fallback L)` | `L` | Left value or fallback |
|
||||
| `RightOrElse(fallback R)` | `R` | Right value or fallback |
|
||||
| `LeftOrEmpty()` | `L` | Left value or zero value |
|
||||
| `RightOrEmpty()` | `R` | Right value or zero value |
|
||||
| `Unpack()` | `(L, R)` | Both values (one will be zero value) |
|
||||
|
||||
## Transformations
|
||||
|
||||
### Swap — exchange left and right
|
||||
|
||||
```go
|
||||
e := mo.Left[string, int]("hello")
|
||||
swapped := e.Swap() // Either[int, string] — Right("hello")
|
||||
```
|
||||
|
||||
### MapLeft / MapRight — transform one side
|
||||
|
||||
The callback receives the value and must return a new `Either[L, R]`:
|
||||
|
||||
```go
|
||||
e := mo.Left[string, int]("hello")
|
||||
upper := e.MapLeft(func(s string) mo.Either[string, int] {
|
||||
return mo.Left[string, int](strings.ToUpper(s))
|
||||
})
|
||||
// Left("HELLO")
|
||||
|
||||
e2 := mo.Right[string, int](42)
|
||||
doubled := e2.MapRight(func(v int) mo.Either[string, int] {
|
||||
return mo.Right[string, int](v * 2)
|
||||
})
|
||||
// Right(84)
|
||||
```
|
||||
|
||||
**Go limitation:** Like Option.Map and Result.Map, direct Either methods cannot change the type parameters. Use sub-package `either.MapLeft`/`either.MapRight` for type-changing transforms — see [Pipelines Reference](./pipelines.md).
|
||||
|
||||
### Match — pattern matching
|
||||
|
||||
```go
|
||||
e.Match(
|
||||
func(left string) mo.Either[string, int] {
|
||||
fmt.Println("Left:", left)
|
||||
return mo.Left[string, int](left)
|
||||
},
|
||||
func(right int) mo.Either[string, int] {
|
||||
fmt.Println("Right:", right)
|
||||
return mo.Right[string, int](right)
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### ForEach — side effects
|
||||
|
||||
```go
|
||||
e.ForEach(
|
||||
func(left string) { fmt.Println("Left:", left) },
|
||||
func(right int) { fmt.Println("Right:", right) },
|
||||
)
|
||||
```
|
||||
|
||||
## Either vs Result
|
||||
|
||||
| Feature | Either[L, R] | Result[T] |
|
||||
| ------------- | ----------------------- | ----------------------- |
|
||||
| Left/Err type | Any type L | Always `error` |
|
||||
| Semantics | Two valid alternatives | Success or failure |
|
||||
| Use case | Cached vs fresh, A vs B | Operation that may fail |
|
||||
| JSON | Not supported | JSON-RPC format |
|
||||
|
||||
`Result[T]` is equivalent to `Either[error, T]` — use `result.ToEither()` to convert.
|
||||
|
||||
## Either3[T1, T2, T3] — Three-Type Union
|
||||
|
||||
### Constructors
|
||||
|
||||
```go
|
||||
e := mo.NewEither3Arg1[string, int, bool]("hello") // T1 variant
|
||||
e := mo.NewEither3Arg2[string, int, bool](42) // T2 variant
|
||||
e := mo.NewEither3Arg3[string, int, bool](true) // T3 variant
|
||||
```
|
||||
|
||||
### Type Checking and Extraction
|
||||
|
||||
```go
|
||||
e.IsArg1() // true if T1
|
||||
e.IsArg2() // true if T2
|
||||
e.IsArg3() // true if T3
|
||||
|
||||
val, ok := e.Arg1() // (T1, bool)
|
||||
val := e.MustArg1() // T1 or panics
|
||||
val := e.Arg1OrElse(fb) // T1 or fallback
|
||||
val := e.Arg1OrEmpty() // T1 or zero value
|
||||
t1, t2, t3 := e.Unpack() // all three (two will be zero)
|
||||
```
|
||||
|
||||
### Pattern Matching
|
||||
|
||||
```go
|
||||
e.Match(
|
||||
func(s string) mo.Either3[string, int, bool] { ... },
|
||||
func(i int) mo.Either3[string, int, bool] { ... },
|
||||
func(b bool) mo.Either3[string, int, bool] { ... },
|
||||
)
|
||||
```
|
||||
|
||||
### Transformations
|
||||
|
||||
MapArg callbacks receive the value and return a new Either3:
|
||||
|
||||
```go
|
||||
e.MapArg1(func(s string) mo.Either3[string, int, bool] {
|
||||
return mo.NewEither3Arg1[string, int, bool](strings.ToUpper(s))
|
||||
})
|
||||
e.MapArg2(func(i int) mo.Either3[string, int, bool] {
|
||||
return mo.NewEither3Arg2[string, int, bool](i * 2)
|
||||
})
|
||||
```
|
||||
|
||||
## Either4 and Either5
|
||||
|
||||
Follow the exact same pattern as Either3 with 4 and 5 type parameters respectively:
|
||||
|
||||
- **Either4[T1, T2, T3, T4]**: `NewEither4Arg1` through `NewEither4Arg4`, `IsArg1`-`IsArg4`, `MapArg1`-`MapArg4`
|
||||
- **Either5[T1, T2, T3, T4, T5]**: `NewEither5Arg1` through `NewEither5Arg5`, `IsArg1`-`IsArg5`, `MapArg1`-`MapArg5`
|
||||
|
||||
Use Either3+ when you need a type-safe union of multiple types — for example, an API that returns different response shapes depending on the request type.
|
||||
@@ -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)
|
||||
@@ -0,0 +1,151 @@
|
||||
# Option[T] API Reference
|
||||
|
||||
## Constructors
|
||||
|
||||
| Function | Description |
|
||||
| --- | --- |
|
||||
| `mo.Some[T](value T)` | Creates Option with a present value |
|
||||
| `mo.None[T]()` | Creates Option with an absent value |
|
||||
| `mo.TupleToOption[T](value T, ok bool)` | Converts (value, bool) tuple — Some if ok is true, None otherwise |
|
||||
| `mo.EmptyableToOption[T](value T)` | None if value equals its zero value, Some otherwise |
|
||||
| `mo.PointerToOption[T](value *T)` | None if pointer is nil, Some(\*value) otherwise |
|
||||
|
||||
## Query Methods
|
||||
|
||||
| Method | Returns | Description |
|
||||
| --- | --- | --- |
|
||||
| `IsPresent()` / `IsSome()` | `bool` | True if value exists |
|
||||
| `IsAbsent()` / `IsNone()` | `bool` | True if value is missing |
|
||||
| `Size()` | `int` | 1 if present, 0 if absent |
|
||||
| `Get()` | `(T, bool)` | Value and presence indicator |
|
||||
| `MustGet()` | `T` | Value or panics — use only inside `mo.Do` |
|
||||
|
||||
## Value Extraction
|
||||
|
||||
| Method | Returns | Description |
|
||||
| -------------------- | ------- | -------------------------------------- |
|
||||
| `OrElse(fallback T)` | `T` | Value if present, fallback otherwise |
|
||||
| `OrEmpty()` | `T` | Value if present, zero value otherwise |
|
||||
| `ToPointer()` | `*T` | Pointer to value, nil if absent |
|
||||
|
||||
## Transformations
|
||||
|
||||
### Map — transform the value if present
|
||||
|
||||
```go
|
||||
opt := mo.Some(42)
|
||||
doubled := opt.Map(func(v int) (int, bool) {
|
||||
return v * 2, true // (new value, keep as Some)
|
||||
})
|
||||
// Some(84)
|
||||
|
||||
// Return false to convert to None
|
||||
filtered := opt.Map(func(v int) (int, bool) {
|
||||
return v, v > 100 // None because 42 <= 100
|
||||
})
|
||||
```
|
||||
|
||||
**Go limitation:** Option.Map takes `func(T) (T, bool)` — the input and output types must be the same `T`. The bool controls whether the result is Some or None. To change the type (e.g. `Option[int]` to `Option[string]`), use sub-package `option.Map` — see [Pipelines Reference](./pipelines.md).
|
||||
|
||||
### MapValue — transform without filter
|
||||
|
||||
```go
|
||||
opt := mo.Some(42)
|
||||
doubled := opt.MapValue(func(v int) int { return v * 2 })
|
||||
// Some(84) — always stays Some if input was Some, no bool needed
|
||||
```
|
||||
|
||||
Unlike Map, MapValue's callback returns just `T` (not `(T, bool)`), so it cannot convert to None.
|
||||
|
||||
### MapNone — provide value when absent
|
||||
|
||||
```go
|
||||
opt := mo.None[int]()
|
||||
filled := opt.MapNone(func() (int, bool) {
|
||||
return 42, true // provide default as Some
|
||||
})
|
||||
// Some(42)
|
||||
```
|
||||
|
||||
### FlatMap — chain Options (same type)
|
||||
|
||||
```go
|
||||
func findUser(id string) mo.Option[User] { ... }
|
||||
func refreshUser(u User) mo.Option[User] { ... }
|
||||
|
||||
refreshed := findUser("123").FlatMap(func(u User) mo.Option[User] {
|
||||
return refreshUser(u) // same type: Option[User] -> Option[User]
|
||||
})
|
||||
```
|
||||
|
||||
**Go limitation:** Direct `.FlatMap` requires `func(T) Option[T]` — same input and output type. For type-changing chains (e.g. `Option[User]` to `Option[string]`), use `option.FlatMap` from the sub-package or `mo.Do` notation.
|
||||
|
||||
### Match — handle both cases
|
||||
|
||||
```go
|
||||
opt.Match(
|
||||
func(v int) (int, bool) {
|
||||
fmt.Println("Got:", v)
|
||||
return v, true // keep as Some
|
||||
},
|
||||
func() (int, bool) {
|
||||
fmt.Println("Empty!")
|
||||
return 0, false // stay as None
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### ForEach — side effect on present value
|
||||
|
||||
```go
|
||||
opt.ForEach(func(v int) {
|
||||
fmt.Println("Value:", v) // only executes if present
|
||||
})
|
||||
```
|
||||
|
||||
## Equality
|
||||
|
||||
```go
|
||||
mo.Some(42).Equal(mo.Some(42)) // true
|
||||
mo.Some(42).Equal(mo.None[int]()) // false
|
||||
mo.None[int]().Equal(mo.None[int]()) // true
|
||||
```
|
||||
|
||||
## Serialization
|
||||
|
||||
Option implements multiple encoding interfaces:
|
||||
|
||||
| Interface | Behavior |
|
||||
| --- | --- |
|
||||
| `json.Marshaler` / `json.Unmarshaler` | Some(42) -> `42`, None -> `null` |
|
||||
| `encoding.TextMarshaler` / `TextUnmarshaler` | Text encoding/decoding |
|
||||
| `encoding.BinaryMarshaler` / `BinaryUnmarshaler` | Binary encoding/decoding |
|
||||
| `encoding/gob.GobEncoder` / `GobDecoder` | Gob encoding/decoding |
|
||||
|
||||
## Database Support
|
||||
|
||||
Option implements `sql.Scanner` and `driver.Valuer`:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID int
|
||||
Phone mo.Option[string] // nullable column
|
||||
}
|
||||
|
||||
// Scanning
|
||||
err := row.Scan(&u.ID, &u.Phone)
|
||||
|
||||
// Inserting
|
||||
_, err := db.Exec("INSERT INTO users (id, phone) VALUES ($1, $2)", u.ID, u.Phone)
|
||||
```
|
||||
|
||||
## Go 1.24+ omitzero Support
|
||||
|
||||
```go
|
||||
type Response struct {
|
||||
Data string `json:"data"`
|
||||
Extra mo.Option[string] `json:"extra,omitzero"` // omitted when None
|
||||
}
|
||||
```
|
||||
|
||||
`IsZero()` returns true when the Option is None, enabling the `omitzero` JSON tag.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Pipeline Sub-Packages Reference
|
||||
|
||||
samber/mo provides sub-packages (`option`, `result`, `either`, `either3`, `either4`, `either5`) with standalone functions for type-changing transformations and composable pipelines.
|
||||
|
||||
## Why Sub-Packages Exist
|
||||
|
||||
Direct methods on Option/Result/Either (`.Map`, `.FlatMap`) cannot change the type parameter because Go methods cannot introduce new type parameters. For example:
|
||||
|
||||
```go
|
||||
opt := mo.Some(42)
|
||||
// opt.Map can return Option[int], but NOT Option[string]
|
||||
// because Map's signature is: func (o Option[T]) Map(func(T) (T, bool)) Option[T]
|
||||
```
|
||||
|
||||
Sub-package functions solve this by being standalone generic functions:
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo/option"
|
||||
|
||||
// option.Map CAN change the type: Option[int] -> Option[string]
|
||||
strOpt := option.Map(func(v int) string {
|
||||
return strconv.Itoa(v)
|
||||
})(mo.Some(42))
|
||||
// Some("42")
|
||||
```
|
||||
|
||||
## option/ Package
|
||||
|
||||
### Transformation Functions
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `option.Map` | `func(I) O -> func(Option[I]) Option[O]` | Transform value, changing type |
|
||||
| `option.FlatMap` | `func(I) Option[O] -> func(Option[I]) Option[O]` | Chain with type change |
|
||||
| `option.Match` | `(onValue, onNone) -> func(Option[I]) Option[O]` | Branch with type change |
|
||||
| `option.FlatMatch` | `(onValue, onNone) -> func(Option[I]) Option[O]` | Branch returning Options |
|
||||
|
||||
### Pipe Functions
|
||||
|
||||
Chain multiple transformations in a readable pipeline:
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo/option"
|
||||
|
||||
result := option.Pipe3(
|
||||
mo.Some(42), // Option[int]
|
||||
option.Map(func(v int) string { return strconv.Itoa(v) }), // -> Option[string]
|
||||
option.Map(func(s string) []byte { return []byte(s) }), // -> Option[[]byte]
|
||||
option.FlatMap(func(b []byte) mo.Option[string] { // -> Option[string]
|
||||
if len(b) > 0 { return mo.Some(string(b)) }
|
||||
return mo.None[string]()
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Available: `option.Pipe1` through `option.Pipe10` (1 to 10 transformation steps).
|
||||
|
||||
## result/ Package
|
||||
|
||||
### Transformation Functions
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `result.Map` | `func(I) O -> func(Result[I]) Result[O]` | Transform success value, changing type |
|
||||
| `result.FlatMap` | `func(I) Result[O] -> func(Result[I]) Result[O]` | Chain with type change |
|
||||
| `result.Match` | `(onValue, onError) -> func(Result[I]) Result[O]` | Branch with type change |
|
||||
| `result.FlatMatch` | `(onValue, onError) -> func(Result[I]) Result[O]` | Branch returning Results |
|
||||
|
||||
### Pipe Functions
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo/result"
|
||||
|
||||
parsed := result.Pipe2(
|
||||
mo.TupleToResult(os.ReadFile("config.yaml")), // Result[[]byte]
|
||||
result.Map(func(data []byte) Config { // -> Result[Config]
|
||||
var cfg Config
|
||||
yaml.Unmarshal(data, &cfg)
|
||||
return cfg
|
||||
}),
|
||||
result.FlatMap(func(cfg Config) mo.Result[ValidConfig] { // -> Result[ValidConfig]
|
||||
return validateConfig(cfg)
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Available: `result.Pipe1` through `result.Pipe10`.
|
||||
|
||||
## either/ Package
|
||||
|
||||
### Transformation Functions
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `either.MapLeft` | `func(Lin) Lout -> func(Either[Lin, R]) Either[Lout, R]` | Transform left side type |
|
||||
| `either.MapRight` | `func(Rin) Rout -> func(Either[L, Rin]) Either[L, Rout]` | Transform right side type |
|
||||
| `either.FlatMapLeft` | `func(Lin) Either[Lout, R] -> func(Either[Lin, R]) Either[Lout, R]` | Chain left with type change |
|
||||
| `either.FlatMapRight` | `func(Rin) Either[L, Rout] -> func(Either[L, Rin]) Either[L, Rout]` | Chain right with type change |
|
||||
| `either.Match` | `(onLeft, onRight) -> func(Either[Lin, Rin]) Either[Lout, Rout]` | Branch both sides |
|
||||
| `either.Swap` | `func(Either[I, O]) Either[O, I]` | Exchange left and right |
|
||||
|
||||
### Pipe Functions
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo/either"
|
||||
|
||||
result := either.Pipe2(
|
||||
mo.Right[error, int](42),
|
||||
either.MapRight(func(v int) string { return strconv.Itoa(v) }),
|
||||
either.MapRight(func(s string) []byte { return []byte(s) }),
|
||||
)
|
||||
```
|
||||
|
||||
Available: `either.Pipe1` through `either.Pipe10`.
|
||||
|
||||
## either3/, either4/, either5/ Packages
|
||||
|
||||
Each provides:
|
||||
|
||||
- `Match` with handlers for each argument type
|
||||
- `MapArg1`, `MapArg2`, `MapArg3` (up to `MapArg5` for either5)
|
||||
- `Pipe1` through `Pipe10`
|
||||
|
||||
## When to Use Pipes vs Direct Methods
|
||||
|
||||
| Scenario | Use | Why |
|
||||
| --- | --- | --- |
|
||||
| Same type in, same type out | Direct method (`.Map`) | Simpler, no import needed |
|
||||
| Type changes across steps | Sub-package function | Go methods can't add type params |
|
||||
| 3+ chained type transforms | `Pipe3`+ | Readable left-to-right flow |
|
||||
| Single type transform | Sub-package function call | Pipe1 is overkill |
|
||||
| Mixed same-type and cross-type | Combine both | Direct for same-type, pipe for cross-type |
|
||||
|
||||
### Example: Combined Usage
|
||||
|
||||
```go
|
||||
// Start with direct method (same type)
|
||||
opt := mo.Some(42).
|
||||
Map(func(v int) (int, bool) { return v * 2, true }) // still Option[int]
|
||||
|
||||
// Then use pipe for type change
|
||||
result := option.Pipe2(
|
||||
opt,
|
||||
option.Map(func(v int) string { return strconv.Itoa(v) }), // -> Option[string]
|
||||
option.Map(func(s string) User { return User{Name: s} }), // -> Option[User]
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,145 @@
|
||||
# Result[T] API Reference
|
||||
|
||||
## Constructors
|
||||
|
||||
| Function | Description |
|
||||
| --- | --- |
|
||||
| `mo.Ok[T](value T)` | Creates a successful Result |
|
||||
| `mo.Err[T](err error)` | Creates a failed Result |
|
||||
| `mo.Errf[T](format string, a ...any)` | Creates failed Result with formatted error message |
|
||||
| `mo.TupleToResult[T](value T, err error)` | Converts Go's (T, error) tuple — Ok if err is nil, Err otherwise |
|
||||
| `mo.Try[T](f func() (T, error))` | Executes function, wraps result — Ok on success, Err on error |
|
||||
|
||||
### Do Notation
|
||||
|
||||
```go
|
||||
result := mo.Do(func() int {
|
||||
a := mo.Ok(10).MustGet() // panics if Err -> caught by Do
|
||||
b := mo.Ok(32).MustGet()
|
||||
return a + b
|
||||
})
|
||||
// Ok(42)
|
||||
```
|
||||
|
||||
`mo.Do` executes a closure and catches any panic from `MustGet()` calls, converting them to `Err`. This enables imperative-style code with monadic error propagation.
|
||||
|
||||
## Query Methods
|
||||
|
||||
| Method | Returns | Description |
|
||||
| --- | --- | --- |
|
||||
| `IsOk()` | `bool` | True if Result is successful |
|
||||
| `IsError()` | `bool` | True if Result is a failure |
|
||||
| `Error()` | `error` | Returns the error, or nil if Ok |
|
||||
| `Get()` | `(T, error)` | Returns value and error (Go-style) |
|
||||
| `MustGet()` | `T` | Returns value or panics — use only inside `mo.Do` |
|
||||
|
||||
## Value Extraction
|
||||
|
||||
| Method | Returns | Description |
|
||||
| -------------------- | ------- | ------------------------------ |
|
||||
| `OrElse(fallback T)` | `T` | Value if Ok, fallback if Err |
|
||||
| `OrEmpty()` | `T` | Value if Ok, zero value if Err |
|
||||
|
||||
## Transformations
|
||||
|
||||
### Map — transform successful value
|
||||
|
||||
```go
|
||||
result := mo.Ok(42).
|
||||
Map(func(v int) (int, error) {
|
||||
return v * 2, nil
|
||||
})
|
||||
// Ok(84)
|
||||
|
||||
// Errors short-circuit
|
||||
result := mo.Err[int](errors.New("fail")).
|
||||
Map(func(v int) (int, error) {
|
||||
return v * 2, nil // never called
|
||||
})
|
||||
// Err("fail")
|
||||
```
|
||||
|
||||
**Go limitation:** Result.Map takes `func(T) (T, error)` — the input and output types must be the same `T`. Returning a non-nil error converts Ok to Err. To change the type (e.g. `Result[[]byte]` to `Result[Config]`), use sub-package `result.Map` or `mo.Do` notation — see [Pipelines Reference](./pipelines.md).
|
||||
|
||||
### MapValue — transform without error possibility
|
||||
|
||||
```go
|
||||
result := mo.Ok(42).MapValue(func(v int) int {
|
||||
return v * 2
|
||||
})
|
||||
// Ok(84) — no error possible in the mapper
|
||||
```
|
||||
|
||||
### MapErr — transform error state
|
||||
|
||||
```go
|
||||
result := mo.Err[int](errors.New("fail")).
|
||||
MapErr(func(err error) (int, error) {
|
||||
return 0, fmt.Errorf("wrapped: %w", err)
|
||||
})
|
||||
// Err("wrapped: fail")
|
||||
```
|
||||
|
||||
### FlatMap — chain Results
|
||||
|
||||
```go
|
||||
func parseAge(s string) mo.Result[int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return mo.TupleToResult(v, err)
|
||||
}
|
||||
|
||||
func validateAge(age int) mo.Result[int] {
|
||||
if age < 0 || age > 150 {
|
||||
return mo.Errf[int]("invalid age: %d", age)
|
||||
}
|
||||
return mo.Ok(age)
|
||||
}
|
||||
|
||||
result := parseAge("25").FlatMap(func(age int) mo.Result[int] {
|
||||
return validateAge(age)
|
||||
})
|
||||
// Ok(25)
|
||||
```
|
||||
|
||||
### Match — handle both cases
|
||||
|
||||
```go
|
||||
result.Match(
|
||||
func(v int) (int, error) {
|
||||
fmt.Println("Success:", v)
|
||||
return v, nil
|
||||
},
|
||||
func(err error) (int, error) {
|
||||
fmt.Println("Error:", err)
|
||||
return 0, err
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### ForEach — side effect on success
|
||||
|
||||
```go
|
||||
result.ForEach(func(v int) {
|
||||
fmt.Println("Got:", v) // only executes if Ok
|
||||
})
|
||||
```
|
||||
|
||||
## Conversion
|
||||
|
||||
```go
|
||||
either := result.ToEither() // Either[error, T]
|
||||
// Ok(42) -> Right(42)
|
||||
// Err(e) -> Left(e)
|
||||
```
|
||||
|
||||
## JSON Serialization
|
||||
|
||||
Result marshals to JSON-RPC format:
|
||||
|
||||
```go
|
||||
// Ok(42) marshals to:
|
||||
{"result": 42}
|
||||
|
||||
// Err("fail") marshals to:
|
||||
{"error": {"message": "fail"}}
|
||||
```
|
||||
Reference in New Issue
Block a user