[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1 @@
|
||||
XingfenD
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
name: golang-samber-mo
|
||||
description: "Monadic types for Golang using samber/mo — Option, Result, Either, Future, IO, Task, and State types for type-safe nullable values, error handling, and functional composition with pipeline sub-packages. Apply when using or adopting samber/mo, when the codebase imports `github.com/samber/mo`, or when considering functional programming patterns as a safety design for Golang."
|
||||
user-invocable: true
|
||||
license: MIT
|
||||
compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang.
|
||||
metadata:
|
||||
author: samber
|
||||
version: "1.0.7"
|
||||
openclaw:
|
||||
emoji: "🎭"
|
||||
homepage: https://github.com/samber/cc-skills-golang
|
||||
requires:
|
||||
bins:
|
||||
- go
|
||||
install: []
|
||||
skill-library-version: "1.16.0"
|
||||
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs AskUserQuestion Bash(godig:*) Bash(gopls:*) LSP mcp__gopls__*
|
||||
---
|
||||
|
||||
**Persona:** You are a Go engineer bringing functional programming safety to Go. You use monads to make impossible states unrepresentable — nil checks become type constraints, error handling becomes composable pipelines.
|
||||
|
||||
**Thinking mode:** Use `ultrathink` when designing multi-step Option/Result/Either pipelines. Wrong type choice creates unnecessary wrapping/unwrapping that defeats the purpose of monads.
|
||||
|
||||
# samber/mo — Monads and Functional Abstractions for Go
|
||||
|
||||
Go 1.18+ library providing type-safe monadic types with zero dependencies. Inspired by Scala, Rust, and fp-ts.
|
||||
|
||||
**Official Resources:**
|
||||
|
||||
- [pkg.go.dev/github.com/samber/mo](https://pkg.go.dev/github.com/samber/mo)
|
||||
- [github.com/samber/mo](https://github.com/samber/mo)
|
||||
|
||||
This skill is not exhaustive. Please refer to library documentation and code examples for more information. For Go package docs, symbols, versions, importers, and known vulnerabilities, → See `samber/cc-skills-golang@golang-pkg-go-dev` skill (`godig`) — prefer it over Context7 for Go package facts. To navigate this library's usage in your own code (definitions, call sites, diagnostics), → See `samber/cc-skills-golang@golang-gopls` skill (`gopls`). Context7 remains a fallback for docs not indexed on pkg.go.dev.
|
||||
|
||||
```bash
|
||||
go get github.com/samber/mo
|
||||
```
|
||||
|
||||
For an introduction to functional programming concepts and why monads are valuable in Go, see [Monads Guide](./references/monads-guide.md).
|
||||
|
||||
## Core Types at a Glance
|
||||
|
||||
| Type | Purpose | Think of it as... |
|
||||
| --- | --- | --- |
|
||||
| `Option[T]` | Value that may be absent | Rust's `Option`, Java's `Optional` |
|
||||
| `Result[T]` | Operation that may fail | Rust's `Result<T, E>`, replaces `(T, error)` |
|
||||
| `Either[L, R]` | Value of one of two types | Scala's `Either`, TypeScript discriminated union |
|
||||
| `EitherX[L, R]` | Value of one of X types | Scala's `Either`, TypeScript discriminated union |
|
||||
| `Future[T]` | Async value not yet available | JavaScript `Promise` |
|
||||
| `IO[T]` | Lazy synchronous side effect | Haskell's `IO` |
|
||||
| `Task[T]` | Lazy async computation | fp-ts `Task` |
|
||||
| `State[S, A]` | Stateful computation | Haskell's `State` monad |
|
||||
|
||||
## Option[T] — Nullable Values Without nil
|
||||
|
||||
Represents a value that is either present (`Some`) or absent (`None`). Eliminates nil pointer risks at the type level.
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo"
|
||||
|
||||
name := mo.Some("Alice") // Option[string] with value
|
||||
empty := mo.None[string]() // Option[string] without value
|
||||
fromPtr := mo.PointerToOption(ptr) // nil pointer -> None
|
||||
|
||||
// Safe extraction
|
||||
name.OrElse("Anonymous") // "Alice"
|
||||
empty.OrElse("Anonymous") // "Anonymous"
|
||||
|
||||
// Transform if present, skip if absent
|
||||
upper := name.Map(func(s string) (string, bool) {
|
||||
return strings.ToUpper(s), true
|
||||
})
|
||||
```
|
||||
|
||||
**Key methods:** `Some`, `None`, `Get`, `MustGet`, `OrElse`, `OrEmpty`, `Map`, `FlatMap`, `Match`, `ForEach`, `ToPointer`, `IsPresent`, `IsAbsent`.
|
||||
|
||||
Option implements `json.Marshaler/Unmarshaler`, `sql.Scanner`, `driver.Valuer` — use it directly in JSON structs and database models.
|
||||
|
||||
For full API reference, see [Option Reference](./references/option.md).
|
||||
|
||||
## Result[T] — Error Handling as Values
|
||||
|
||||
Represents success (`Ok`) or failure (`Err`). Equivalent to `Either[error, T]` but specialized for Go's error pattern.
|
||||
|
||||
```go
|
||||
// Wrap Go's (value, error) pattern
|
||||
result := mo.TupleToResult(os.ReadFile("config.yaml"))
|
||||
|
||||
// Same-type transform — errors short-circuit automatically
|
||||
upper := mo.Ok("hello").Map(func(s string) (string, error) {
|
||||
return strings.ToUpper(s), nil
|
||||
})
|
||||
// Ok("HELLO")
|
||||
|
||||
// Extract with fallback
|
||||
val := upper.OrElse("default")
|
||||
```
|
||||
|
||||
**Go limitation:** Direct methods (`.Map`, `.FlatMap`) cannot change the type parameter — `Result[T].Map` returns `Result[T]`, not `Result[U]`. Go methods cannot introduce new type parameters. For type-changing transforms (e.g. `Result[[]byte]` to `Result[Config]`), use sub-package functions or `mo.Do`:
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo/result"
|
||||
|
||||
// Type-changing pipeline: []byte -> Config -> ValidConfig
|
||||
parsed := result.Pipe2(
|
||||
mo.TupleToResult(os.ReadFile("config.yaml")),
|
||||
result.Map(func(data []byte) Config { return parseConfig(data) }),
|
||||
result.FlatMap(func(cfg Config) mo.Result[ValidConfig] { return validate(cfg) }),
|
||||
)
|
||||
```
|
||||
|
||||
**Key methods:** `Ok`, `Err`, `Errf`, `TupleToResult`, `Try`, `Get`, `MustGet`, `OrElse`, `Map`, `FlatMap`, `MapErr`, `Match`, `ForEach`, `ToEither`, `IsOk`, `IsError`.
|
||||
|
||||
For full API reference, see [Result Reference](./references/result.md).
|
||||
|
||||
## Either[L, R] — Discriminated Union of Two Types
|
||||
|
||||
Represents a value that is one of two possible types. Unlike Result, neither side implies success or failure — both are valid alternatives.
|
||||
|
||||
```go
|
||||
// API that returns either cached data or fresh data
|
||||
func fetchUser(id string) mo.Either[CachedUser, FreshUser] {
|
||||
if cached, ok := cache.Get(id); ok {
|
||||
return mo.Left[CachedUser, FreshUser](cached)
|
||||
}
|
||||
return mo.Right[CachedUser, FreshUser](db.Fetch(id))
|
||||
}
|
||||
|
||||
// Pattern match
|
||||
result := fetchUser("user-123")
|
||||
result.Match(
|
||||
func(cached CachedUser) mo.Either[CachedUser, FreshUser] { /* use cached */ },
|
||||
func(fresh FreshUser) mo.Either[CachedUser, FreshUser] { /* use fresh */ },
|
||||
)
|
||||
```
|
||||
|
||||
**When to use Either vs Result:** Use `Result[T]` when one path is an error. Use `Either[L, R]` when both paths are valid alternatives (cached vs fresh, left vs right, strategy A vs B).
|
||||
|
||||
`Either3[T1, T2, T3]`, `Either4`, and `Either5` extend this to 3-5 type variants.
|
||||
|
||||
For full API reference, see [Either Reference](./references/either.md).
|
||||
|
||||
## Do Notation — Imperative Style with Monadic Safety
|
||||
|
||||
`mo.Do` wraps imperative code in a `Result`, catching panics from `MustGet()` calls:
|
||||
|
||||
```go
|
||||
result := mo.Do(func() int {
|
||||
// MustGet panics on None/Err — Do catches it as Result error
|
||||
a := mo.Some(21).MustGet()
|
||||
b := mo.Ok(2).MustGet()
|
||||
return a * b // 42
|
||||
})
|
||||
// result is Ok(42)
|
||||
|
||||
result := mo.Do(func() int {
|
||||
val := mo.None[int]().MustGet() // panics
|
||||
return val
|
||||
})
|
||||
// result is Err("no such element")
|
||||
```
|
||||
|
||||
Do notation bridges imperative Go style with monadic safety — write straight-line code, get automatic error propagation.
|
||||
|
||||
## Pipeline Sub-Packages vs Direct Chaining
|
||||
|
||||
samber/mo provides two ways to compose operations:
|
||||
|
||||
**Direct methods** (`.Map`, `.FlatMap`) — work when the output type equals the input type:
|
||||
|
||||
```go
|
||||
opt := mo.Some(42)
|
||||
doubled := opt.Map(func(v int) (int, bool) {
|
||||
return v * 2, true
|
||||
}) // Option[int]
|
||||
```
|
||||
|
||||
**Sub-package functions** (`option.Map`, `result.Map`) — required when the output type differs from input:
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo/option"
|
||||
|
||||
// int -> string type change: use sub-package Map
|
||||
strOpt := option.Map(func(v int) string {
|
||||
return fmt.Sprintf("value: %d", v)
|
||||
})(mo.Some(42)) // Option[string]
|
||||
```
|
||||
|
||||
**Pipe functions** (`option.Pipe3`, `result.Pipe3`) — chain multiple type-changing transformations readably:
|
||||
|
||||
```go
|
||||
import "github.com/samber/mo/option"
|
||||
|
||||
result := option.Pipe3(
|
||||
mo.Some(42),
|
||||
option.Map(func(v int) string { return strconv.Itoa(v) }),
|
||||
option.Map(func(s string) []byte { return []byte(s) }),
|
||||
option.FlatMap(func(b []byte) mo.Option[string] {
|
||||
if len(b) > 0 { return mo.Some(string(b)) }
|
||||
return mo.None[string]()
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
**Rule of thumb:** Use direct methods for same-type transforms. Use sub-package functions + pipes when types change across steps.
|
||||
|
||||
For detailed pipeline API reference, see [Pipelines Reference](./references/pipelines.md).
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### JSON API responses with Option
|
||||
|
||||
```go
|
||||
type UserResponse struct {
|
||||
Name string `json:"name"`
|
||||
Nickname mo.Option[string] `json:"nickname"` // omits null gracefully
|
||||
Bio mo.Option[string] `json:"bio"`
|
||||
}
|
||||
```
|
||||
|
||||
### Database nullable columns
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID int
|
||||
Email string
|
||||
Phone mo.Option[string] // implements sql.Scanner + driver.Valuer
|
||||
}
|
||||
|
||||
err := row.Scan(&u.ID, &u.Email, &u.Phone)
|
||||
```
|
||||
|
||||
### Wrapping existing Go APIs
|
||||
|
||||
```go
|
||||
// Convert map lookup to Option
|
||||
func MapGet[K comparable, V any](m map[K]V, key K) mo.Option[V] {
|
||||
return mo.TupleToOption(m[key]) // m[key] returns (V, bool)
|
||||
}
|
||||
```
|
||||
|
||||
### Uniform extraction with Fold
|
||||
|
||||
`mo.Fold` works uniformly across Option, Result, and Either via the `Foldable` interface:
|
||||
|
||||
```go
|
||||
str := mo.Fold[error, int, string](
|
||||
mo.Ok(42), // works with Option, Result, or Either
|
||||
func(v int) string { return fmt.Sprintf("got %d", v) },
|
||||
func(err error) string { return "failed" },
|
||||
)
|
||||
// "got 42"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Prefer `OrElse` over `MustGet`** — `MustGet` panics on absent/error values; use it only inside `mo.Do` blocks where panics are caught, or when you are certain the value exists
|
||||
2. **Use `TupleToResult` at API boundaries** — convert Go's `(T, error)` to `Result[T]` at the boundary, then chain with `Map`/`FlatMap` inside your domain logic
|
||||
3. **Use `Result[T]` for errors, `Either[L, R]` for alternatives** — Result is specialized for success/failure; Either is for two valid types
|
||||
4. **Option for nullable fields, not zero values** — `Option[string]` distinguishes "absent" from "empty string"; use plain `string` when empty string is a valid value
|
||||
5. **Chain, don't nest** — `result.Map(...).FlatMap(...).OrElse(default)` reads left-to-right; avoid nested if/else patterns when monadic chaining is cleaner
|
||||
6. **Use sub-package pipes for multi-step type transformations** — when 3+ steps each change the type, `option.Pipe3(...)` is more readable than nested function calls
|
||||
|
||||
For advanced types (Future, IO, Task, State), see [Advanced Types Reference](./references/advanced-types.md).
|
||||
|
||||
If you encounter a bug or unexpected behavior in samber/mo, open an issue at <https://github.com/samber/mo/issues>.
|
||||
|
||||
## Cross-References
|
||||
|
||||
- -> See `samber/cc-skills-golang@golang-samber-lo` skill for functional collection transforms (Map, Filter, Reduce on slices) that compose with mo types
|
||||
- -> See `samber/cc-skills-golang@golang-error-handling` skill for idiomatic Go error handling patterns
|
||||
- -> See `samber/cc-skills-golang@golang-safety` skill for nil-safety and defensive Go coding
|
||||
- -> See `samber/cc-skills-golang@golang-database` skill for database access patterns
|
||||
- -> See `samber/cc-skills-golang@golang-design-patterns` skill for functional options and other Go patterns
|
||||
@@ -0,0 +1,311 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "option-vs-pointer-for-nullable-db-field",
|
||||
"description": "Tests whether the model uses Option[T] instead of *T for nullable database columns when both SQL scanning and JSON marshaling are needed",
|
||||
"prompt": "I'm adding a REST API to an existing Go service. The User table has a nullable 'phone' column (VARCHAR) and a nullable 'bio' column (TEXT). The same User struct is used both for database row scanning and for JSON API responses. Right now I'm using *string for these fields. I have samber/mo available. Is there a better option and what does the struct look like?",
|
||||
"trap": "Without the skill, the model suggests keeping *string (it works for both DB and JSON), or proposes sql.NullString for DB + a separate DTO struct for JSON. The skill teaches that mo.Option[T] natively implements BOTH sql.Scanner/driver.Valuer AND json.Marshaler/Unmarshaler, eliminating the need for two types or custom serialization code.",
|
||||
"assertions": [
|
||||
{"id": "1.1", "text": "Recommends switching from *string to mo.Option[string] for the nullable fields"},
|
||||
{"id": "1.2", "text": "Explains that Option implements sql.Scanner and driver.Valuer so row.Scan works directly"},
|
||||
{"id": "1.3", "text": "Explains that Option implements json.Marshaler/Unmarshaler so the same struct works for JSON responses"},
|
||||
{"id": "1.4", "text": "Explicitly states that *string requires custom JSON handling to distinguish null vs absent, which Option avoids"},
|
||||
{"id": "1.5", "text": "Does NOT recommend maintaining two separate structs (one for DB, one for JSON) as the solution"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "result-vs-tuple-error-boundary",
|
||||
"description": "Tests whether the model knows when to use Result[T] vs (T, error)",
|
||||
"prompt": "I'm writing a Go function that reads a config file, parses YAML, validates the config, and returns the result. The function is part of a public API package. Should I use samber/mo Result[T] as the return type?",
|
||||
"trap": "Without the skill, the model either always uses Result or always uses (T, error). The correct answer is: use (T, error) at public API boundaries for Go idiom compliance, but use Result internally for chaining.",
|
||||
"assertions": [
|
||||
{"id": "2.1", "text": "Recommends returning (Config, error) at the public API boundary, not Result[Config]"},
|
||||
{"id": "2.2", "text": "Suggests using Result[T] internally for chaining the read-parse-validate pipeline"},
|
||||
{"id": "2.3", "text": "Shows TupleToResult to convert from Go-style to Result at the start of the chain"},
|
||||
{"id": "2.4", "text": "Shows .Get() or extraction at the end to convert back to (T, error) for the public return"},
|
||||
{"id": "2.5", "text": "Explains that Result is for internal composition, not public API signatures"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "either-vs-result-two-valid-types",
|
||||
"description": "Tests whether the model uses Either[L,R] when both outcomes are valid (not errors)",
|
||||
"prompt": "I have a Go function that looks up a user. It can return either a cached user (from Redis, includes cache metadata) or a fresh user (from the database, no cache metadata). Both outcomes are perfectly valid. What type should the return be?",
|
||||
"trap": "Without the skill, the model uses an interface, two separate return values, or Result. Either[CachedUser, FreshUser] is correct because neither outcome is an error.",
|
||||
"assertions": [
|
||||
{"id": "3.1", "text": "Uses mo.Either[CachedUser, FreshUser] or equivalent Either type"},
|
||||
{"id": "3.2", "text": "Does NOT use Result[T] (neither outcome is an error)"},
|
||||
{"id": "3.3", "text": "Explains that Either is for two valid alternatives, Result is for success/failure"},
|
||||
{"id": "3.4", "text": "Shows Left/Right constructors for the two outcomes"},
|
||||
{"id": "3.5", "text": "Shows Match or IsLeft/IsRight to handle both cases"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "sub-package-for-type-changing-map",
|
||||
"description": "Tests whether the model uses sub-package functions when Map needs to change types",
|
||||
"prompt": "I have an mo.Option[int] and I want to convert it to mo.Option[string] using strconv.Itoa. How do I do this with samber/mo?",
|
||||
"trap": "Without the skill, the model tries Option.Map which cannot change types (Map returns Option[T] not Option[R]). The sub-package option.Map is required for type-changing transforms.",
|
||||
"assertions": [
|
||||
{"id": "4.1", "text": "Uses option.Map from the github.com/samber/mo/option sub-package"},
|
||||
{"id": "4.2", "text": "Does NOT try to use the direct .Map method for type-changing transform"},
|
||||
{"id": "4.3", "text": "Imports github.com/samber/mo/option"},
|
||||
{"id": "4.4", "text": "Shows the curried form: option.Map(func(int) string)(opt)"},
|
||||
{"id": "4.5", "text": "Explains that Go methods cannot introduce new type parameters, hence sub-packages"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "do-notation-for-imperative-monadic",
|
||||
"description": "Tests knowledge of mo.Do for imperative-style monadic code",
|
||||
"prompt": "I have several mo.Option and mo.Result values in Go that I need to combine. I want to extract all their values, do some computation, and get a Result back. The FlatMap chaining is getting deeply nested. Is there a simpler way?",
|
||||
"trap": "Without the skill, the model doesn't know about mo.Do which catches MustGet panics and converts them to Result errors, enabling imperative-style monadic code.",
|
||||
"assertions": [
|
||||
{"id": "5.1", "text": "Suggests using mo.Do to wrap imperative-style code"},
|
||||
{"id": "5.2", "text": "Shows MustGet() calls inside the Do block (panics caught by Do)"},
|
||||
{"id": "5.3", "text": "Explains that mo.Do catches panics from MustGet and converts them to Err"},
|
||||
{"id": "5.4", "text": "The result of mo.Do is a Result[T]"},
|
||||
{"id": "5.5", "text": "Shows that this is cleaner than deeply nested FlatMap chains"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "pipe-composition-multi-step",
|
||||
"description": "Tests whether the model uses Pipe functions for multi-step type-changing pipelines",
|
||||
"prompt": "I need to transform an mo.Option[int] through 3 steps in Go: convert to string, then to []byte, then validate and return Option[ValidatedData]. Each step changes the type. Show me how to compose these.",
|
||||
"trap": "Without the skill, the model nests function calls or uses intermediate variables. Pipe3 from the option sub-package provides readable left-to-right flow.",
|
||||
"assertions": [
|
||||
{"id": "6.1", "text": "Uses option.Pipe3 (or equivalent PipeN) from github.com/samber/mo/option"},
|
||||
{"id": "6.2", "text": "Each step uses option.Map or option.FlatMap as a curried function"},
|
||||
{"id": "6.3", "text": "The pipeline reads top-to-bottom or left-to-right, not nested inside-out"},
|
||||
{"id": "6.4", "text": "Imports github.com/samber/mo/option sub-package"},
|
||||
{"id": "6.5", "text": "Uses option.FlatMap for the validation step that may return None"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "future-vs-task-eager-vs-lazy",
|
||||
"description": "Tests whether the model distinguishes Future (eager) from Task (lazy)",
|
||||
"prompt": "I'm building a Go system where I want to define an async computation that should NOT start executing until I explicitly trigger it. Later I'll run it and get the result. Should I use mo.Future or mo.Task?",
|
||||
"trap": "Without the skill, the model uses Future which starts executing immediately on creation. Task is lazy — it only runs when .Run() is called.",
|
||||
"assertions": [
|
||||
{"id": "7.1", "text": "Recommends mo.Task (not Future) because Task is lazy"},
|
||||
{"id": "7.2", "text": "Explains that Future starts executing immediately on construction"},
|
||||
{"id": "7.3", "text": "Explains that Task only executes when .Run() is called"},
|
||||
{"id": "7.4", "text": "Shows that task.Run() returns a *Future[T]"},
|
||||
{"id": "7.5", "text": "Demonstrates the deferred execution pattern with Task"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "tuple-to-result-wrapping-stdlib",
|
||||
"description": "Tests knowledge of TupleToResult for wrapping Go stdlib calls",
|
||||
"prompt": "I want to wrap os.ReadFile and strconv.Atoi calls into samber/mo Result types in Go so I can chain them. What's the most concise way?",
|
||||
"trap": "Without the skill, the model manually calls the function, checks error, and constructs Ok/Err. TupleToResult wraps (T, error) tuples directly.",
|
||||
"assertions": [
|
||||
{"id": "8.1", "text": "Uses mo.TupleToResult(os.ReadFile(path)) to wrap directly"},
|
||||
{"id": "8.2", "text": "Uses mo.TupleToResult(strconv.Atoi(s)) or mo.Try for the second call"},
|
||||
{"id": "8.3", "text": "Does NOT manually check err and construct Ok/Err separately"},
|
||||
{"id": "8.4", "text": "Chains the two results using Map or FlatMap"},
|
||||
{"id": "8.5", "text": "Shows that TupleToResult converts (T, error) to Result[T] in one call"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "when-not-to-use-monads",
|
||||
"description": "Tests whether the model correctly advises against monads for simple cases",
|
||||
"prompt": "I have a simple Go function that opens a file. If it fails, I log the error and return. There are no subsequent operations to chain. Should I use samber/mo Result for this?",
|
||||
"trap": "Without the skill, the model might over-apply monads to trivial cases. Plain if err != nil is clearer for single-step error handling.",
|
||||
"assertions": [
|
||||
{"id": "9.1", "text": "Advises against using Result for this simple one-step case"},
|
||||
{"id": "9.2", "text": "Recommends standard Go if err != nil pattern"},
|
||||
{"id": "9.3", "text": "Explains that Result shines with multi-step chains, not single operations"},
|
||||
{"id": "9.4", "text": "Does NOT wrap everything in Result just because the library is available"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "option-json-serialization-behavior",
|
||||
"description": "Tests knowledge of Option's JSON marshaling behavior",
|
||||
"prompt": "In my Go API response struct, I have a field that should be null in JSON when absent and the actual value when present. I'm currently using *string with json omitempty but it shows null instead of omitting. How should I handle this with samber/mo?",
|
||||
"trap": "Without the skill, the model doesn't know that Option marshals Some(x) to x and None to null, and that Go 1.24+ supports omitzero for full omission.",
|
||||
"assertions": [
|
||||
{"id": "10.1", "text": "Uses mo.Option[string] for the nullable JSON field"},
|
||||
{"id": "10.2", "text": "Explains that Some(x) marshals to the raw value x, None marshals to null"},
|
||||
{"id": "10.3", "text": "Mentions omitzero tag (Go 1.24+) or IsZero for omitting None fields entirely"},
|
||||
{"id": "10.4", "text": "Shows the struct definition with json tag"},
|
||||
{"id": "10.5", "text": "Does NOT require a custom MarshalJSON method — Option handles it natively"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "emptyable-to-option-zero-value",
|
||||
"description": "Tests knowledge of EmptyableToOption for zero-value detection",
|
||||
"prompt": "I'm receiving a Go struct from an external API where empty string means 'not provided'. I want to convert these empty strings to None and non-empty strings to Some. What's the most concise way with samber/mo?",
|
||||
"trap": "Without the skill, the model writes if/else to check for empty string. EmptyableToOption does this automatically for any comparable type.",
|
||||
"assertions": [
|
||||
{"id": "11.1", "text": "Uses mo.EmptyableToOption to convert zero values to None"},
|
||||
{"id": "11.2", "text": "Shows that EmptyableToOption returns None for zero value, Some for non-zero"},
|
||||
{"id": "11.3", "text": "Does NOT write a manual if s == \"\" check when EmptyableToOption exists"},
|
||||
{"id": "11.4", "text": "Mentions that this works for any comparable type (int 0, empty string, etc.)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "pointer-to-option-nil-handling",
|
||||
"description": "Tests knowledge of PointerToOption for nil pointer conversion",
|
||||
"prompt": "I'm parsing a Go JSON payload where fields are *int (pointer to int, nil when absent). I want to convert these to Option[int] for safer downstream processing. How?",
|
||||
"trap": "Without the skill, the model dereferences the pointer manually with nil check. PointerToOption does this in one call.",
|
||||
"assertions": [
|
||||
{"id": "12.1", "text": "Uses mo.PointerToOption(ptr) to convert *int to Option[int]"},
|
||||
{"id": "12.2", "text": "Explains that nil pointer becomes None, non-nil becomes Some(*ptr)"},
|
||||
{"id": "12.3", "text": "Does NOT manually check if ptr != nil before wrapping"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "result-map-vs-flatmap-choice",
|
||||
"description": "Tests understanding of when to use Map vs FlatMap on Result",
|
||||
"prompt": "I have a mo.Result[string] in Go. I need to chain two operations: (1) convert string to uppercase (infallible), (2) parse the string as an integer (fallible, returns Result[int]). Which methods should I use?",
|
||||
"trap": "Without the skill, the model uses Map for both or FlatMap for both. Map is for infallible transforms, FlatMap is for transforms that return Result.",
|
||||
"assertions": [
|
||||
{"id": "13.1", "text": "Uses MapValue (or Map returning nil error) for the infallible uppercase operation"},
|
||||
{"id": "13.2", "text": "Uses FlatMap for the fallible parse operation that returns Result[int]"},
|
||||
{"id": "13.3", "text": "Does NOT use FlatMap for the simple uppercase transform"},
|
||||
{"id": "13.4", "text": "Shows the chain: result.MapValue(toUpper).FlatMap(parse) or equivalent"},
|
||||
{"id": "13.5", "text": "Explains the distinction: Map wraps the return value, FlatMap takes a function returning Result"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "option-map-bool-semantics",
|
||||
"description": "Tests understanding of Option.Map's (T, bool) return type",
|
||||
"prompt": "I have an mo.Option[int] in Go and I want to keep the value only if it's greater than 10, otherwise turn it into None. How do I do this with Map?",
|
||||
"trap": "Without the skill, the model doesn't realize Option.Map returns (T, bool) where the bool controls Some/None, acting as both map and filter.",
|
||||
"assertions": [
|
||||
{"id": "14.1", "text": "Uses opt.Map(func(v int) (int, bool) { return v, v > 10 })"},
|
||||
{"id": "14.2", "text": "Explains that returning false from Map's callback converts to None"},
|
||||
{"id": "14.3", "text": "Shows the (T, bool) return signature of Map's callback"},
|
||||
{"id": "14.4", "text": "Does NOT use FlatMap with an if/else to accomplish filtering"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "fold-for-uniform-value-extraction",
|
||||
"description": "Tests knowledge of mo.Fold for extracting a value from Option, Result, or Either with two-callback pattern",
|
||||
"prompt": "I have an mo.Result[int] in Go. I want to convert it to a display string: if it's Ok, show the number formatted as 'value: N'; if it's Err, show 'error: <message>'. I'm currently using IsOk/IsError with an if/else. Is there a more declarative way with samber/mo?",
|
||||
"trap": "Without the skill, the model uses if/else with IsOk/IsError and manual extraction. mo.Fold accepts two callbacks — success and failure — and dispatches uniformly. The model often misses Fold entirely or confuses it with Match (which returns the same type as its receiver, requiring type-changing sub-package use).",
|
||||
"assertions": [
|
||||
{"id": "15.1", "text": "Uses mo.Fold to replace the if/else dispatch"},
|
||||
{"id": "15.2", "text": "Passes a success callback (func(int) string) as the first function argument"},
|
||||
{"id": "15.3", "text": "Passes a failure callback (func(error) string) as the second function argument"},
|
||||
{"id": "15.4", "text": "The output type (string) differs from the input type (int) — Fold is used for type-changing extraction"},
|
||||
{"id": "15.5", "text": "Does NOT use IsOk/IsError with separate MustGet/Error calls as the primary approach"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "io-for-testable-side-effects",
|
||||
"description": "Tests knowledge of IO for deferring and testing side effects",
|
||||
"prompt": "I have a Go function that reads from stdin, formats the input, and writes to stdout. I want to make this testable by separating the side effects from the logic. How can samber/mo help?",
|
||||
"trap": "Without the skill, the model uses interfaces or dependency injection for io.Reader/io.Writer. IO[T] provides a functional approach to defer side effects.",
|
||||
"assertions": [
|
||||
{"id": "16.1", "text": "Uses mo.IO or mo.IOEither to wrap the side-effecting operations"},
|
||||
{"id": "16.2", "text": "Explains that IO is lazy — the side effect only runs when Run() is called"},
|
||||
{"id": "16.3", "text": "Shows that IO1 or IO2 can parameterize the side effect for testing"},
|
||||
{"id": "16.4", "text": "Demonstrates composability of IO operations"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "result-to-either-conversion",
|
||||
"description": "Tests knowledge of Result.ToEither() conversion",
|
||||
"prompt": "I have a mo.Result[User] in Go and I need to pass it to a function that expects mo.Either[error, User]. How do I convert?",
|
||||
"trap": "Without the skill, the model manually matches on IsOk/IsError and constructs Left/Right. ToEither() does this directly.",
|
||||
"assertions": [
|
||||
{"id": "17.1", "text": "Uses result.ToEither() for direct conversion"},
|
||||
{"id": "17.2", "text": "Explains that Ok becomes Right, Err becomes Left"},
|
||||
{"id": "17.3", "text": "Does NOT manually check IsOk/IsError and construct Either"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "either3-for-multi-type-union",
|
||||
"description": "Tests knowledge of Either3+ for n-ary type unions",
|
||||
"prompt": "My Go API can return three different response types depending on the request: a UserResponse, an AdminResponse, or a SystemResponse. I want type-safe handling. What should I use?",
|
||||
"trap": "Without the skill, the model uses interface{}/any or defines a common interface. Either3 provides a type-safe discriminated union.",
|
||||
"assertions": [
|
||||
{"id": "18.1", "text": "Uses mo.Either3[UserResponse, AdminResponse, SystemResponse]"},
|
||||
{"id": "18.2", "text": "Shows NewEither3Arg1, NewEither3Arg2, NewEither3Arg3 constructors"},
|
||||
{"id": "18.3", "text": "Shows Match with handlers for all three types"},
|
||||
{"id": "18.4", "text": "Does NOT use interface{}/any which loses type safety"},
|
||||
{"id": "18.5", "text": "Mentions Either4/Either5 for 4-5 type variants"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "option-vs-zero-value-distinction",
|
||||
"description": "Tests understanding of when Option adds value vs when zero values suffice",
|
||||
"prompt": "In my Go struct, I have a 'count' field (int) and a 'nickname' field (string). For 'count', zero is a valid value meaning 'no items'. For 'nickname', empty string means 'not set'. Should I use Option for both?",
|
||||
"trap": "Without the skill, the model either uses Option for both or neither. Option should be used only for 'nickname' where absence is semantically different from empty.",
|
||||
"assertions": [
|
||||
{"id": "19.1", "text": "Recommends plain int for count (zero is a valid meaningful value)"},
|
||||
{"id": "19.2", "text": "Recommends mo.Option[string] for nickname (absence differs from empty)"},
|
||||
{"id": "19.3", "text": "Explains that Option is for when absence is semantically different from the zero value"},
|
||||
{"id": "19.4", "text": "Does NOT recommend Option[int] for count where 0 is meaningful"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "map-lookup-to-option",
|
||||
"description": "Tests converting Go map lookups to Option using TupleToOption",
|
||||
"prompt": "I have a Go map[string]User and I want to look up a key and chain transformations on the result if present. How do I bridge the map lookup with samber/mo?",
|
||||
"trap": "Without the skill, the model manually checks the ok bool from map access. TupleToOption wraps the (value, bool) tuple directly.",
|
||||
"assertions": [
|
||||
{"id": "20.1", "text": "Uses mo.TupleToOption with the map lookup: mo.TupleToOption(m[key])"},
|
||||
{"id": "20.2", "text": "Chains Map/FlatMap on the resulting Option"},
|
||||
{"id": "20.3", "text": "Does NOT manually check ok bool and construct Some/None"},
|
||||
{"id": "20.4", "text": "Shows the complete pattern: TupleToOption(m[key]).Map(...)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "result-try-catch-panics",
|
||||
"description": "Tests knowledge of mo.Try for wrapping a function that may return an error OR panic, and chaining the result",
|
||||
"prompt": "I'm integrating a third-party Go JSON schema validator that has two failure modes: it returns an error for invalid input, and it panics on malformed schemas. I want to call it, capture both failure modes, and then chain a transformation if validation succeeds — all without writing defer/recover boilerplate. I'm already using samber/mo in my project. Show the code.",
|
||||
"trap": "Without the skill, the model writes a defer/recover wrapper function manually, then handles the error. With the skill, mo.Try captures both (T, error) returns and panics into a Result[T] in one call, enabling direct chaining with Map/FlatMap. The model often misses mo.Try entirely (using a hand-rolled wrapper) or uses it but forgets it also catches panics (thinking it only handles errors).",
|
||||
"assertions": [
|
||||
{"id": "21.1", "text": "Uses mo.Try (not a manual defer/recover) to call the validator"},
|
||||
{"id": "21.2", "text": "Explicitly states that mo.Try catches panics AND returned errors, converting both to Err"},
|
||||
{"id": "21.3", "text": "Shows chaining Map or FlatMap on the Try result for the transformation step"},
|
||||
{"id": "21.4", "text": "The mo.Try callback matches the (T, error) return signature of the wrapped function"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "state-monad-for-accumulation",
|
||||
"description": "Tests knowledge of State monad for threading state",
|
||||
"prompt": "I'm writing a Go parser that needs to track position as it consumes tokens from an input string. Each parsing step reads from the current position and advances it. How can I model this with samber/mo?",
|
||||
"trap": "Without the skill, the model uses a mutable struct. State[S,A] threads state through pure computations.",
|
||||
"assertions": [
|
||||
{"id": "22.1", "text": "Uses mo.State or mo.NewState to model the stateful computation"},
|
||||
{"id": "22.2", "text": "State type parameters represent the state (position/input) and result (parsed token)"},
|
||||
{"id": "22.3", "text": "Shows Run(initialState) to execute and get (result, newState)"},
|
||||
{"id": "22.4", "text": "The state is threaded through rather than mutated in place"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "result-map-signature-understanding",
|
||||
"description": "Tests understanding that Result.Map callback returns (T, error) not just T",
|
||||
"prompt": "I want to double an integer inside a mo.Result[int] in Go. Write the Map call.",
|
||||
"trap": "Without the skill, the model writes .Map(func(v int) int { return v * 2 }) which won't compile. Result.Map requires (T, error) return.",
|
||||
"assertions": [
|
||||
{"id": "23.1", "text": "Map callback returns (int, error), e.g., func(v int) (int, error) { return v * 2, nil }"},
|
||||
{"id": "23.2", "text": "Does NOT write func(v int) int which misses the error return"},
|
||||
{"id": "23.3", "text": "Alternatively uses MapValue(func(v int) int { return v * 2 }) for infallible transforms"},
|
||||
{"id": "23.4", "text": "Shows awareness that Map and MapValue have different callback signatures"}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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