Monadic types for Go - Result and Option
The mon package provides functional programming monads: Result[T] for error handling and Option[T] for optional values. These types make error handling explicit and composable.
By Category:
- Result Type - Error handling monad
- Option Type - Optional values
go get github.com/modfin/henry/monResult[T] represents either a success value or an error. It forces explicit error handling.
Create a success result.
r := mon.Ok(42)Create an error result.
r := mon.Err[int](errors.New("something went wrong"))Convert (value, error) tuple to Result.
// From function that returns (T, error)
val, err := strconv.Atoi("42")
r := mon.TupleToResult(val, err)
// r is Ok(42) if err is nil, otherwise Err(err)
// Shorthand
r := mon.From(strconv.Atoi("42"))Check state.
r := mon.Ok(42)
r.IsOk() // true
r.IsErr() // falseGet value or panic on error.
val := r.Unwrap() // 42
// Error case - panics!
bad := mon.Err[int](errors.New("oops"))
bad.Unwrap() // panic!Get value or default.
val := r.UnwrapOr(0) // 42
bad.UnwrapOr(0) // 0 (default)Get value or compute default.
val := bad.UnwrapOrElse(func() int {
return expensiveFallback()
})Unwrap with custom panic message.
val := r.Expect("should have a value") // 42
bad.Expect("should have a value") // panics with messageTransform success value.
r := mon.Ok(5)
doubled := r.Map(func(n int) int {
return n * 2
})
// doubled = Ok(10)
// Error results pass through unchanged
bad := mon.Err[int](errors.New("oops"))
result := bad.Map(func(n int) int { return n * 2 })
// result still contains the errorTransform error.
bad := mon.Err[int](errors.New("original"))
wrapped := bad.MapErr(func(err error) error {
return fmt.Errorf("wrapped: %w", err)
})Chain operations that return Results.
r := mon.Ok(5)
result := r.FlatMap(func(n int) mon.Result[int] {
if n < 0 {
return mon.Err[int](errors.New("negative"))
}
return mon.Ok(n * 2)
})
// result = Ok(10)Use alternative if error.
r1 := mon.Err[int](errors.New("failed"))
r2 := mon.Ok(42)
result := r1.Or(r2)
// result = Ok(42)Compute alternative if error.
result := r1.OrElse(func() mon.Result[int] {
return fallbackOperation()
})Split into successes and failures.
results := []mon.Result[int]{
mon.Ok(1),
mon.Err[int](errors.New("bad")),
mon.Ok(3),
mon.Err[int](errors.New("worse")),
}
oks, errs := mon.Partition(results)
// oks = []int{1, 3}
// errs = []error{error1, error2}Extract values or return first error.
results := []mon.Result[int]{
mon.Ok(1),
mon.Ok(2),
mon.Ok(3),
}
vals, err := mon.Unwrap(results)
// vals = []int{1, 2, 3}, err = nil
// With an error
badResults := []mon.Result[int]{
mon.Ok(1),
mon.Err[int](errors.New("failed")),
mon.Ok(3),
}
vals, err := mon.Unwrap(badResults)
// vals = nil, err = errorOption[T] represents either a value or nothing (like nullable types).
Create with a value.
o := mon.Some(42)Create empty.
o := mon.None[int]()Create from pointer.
val := 42
ptr := &val
o := mon.FromPtr(ptr) // Some(42)
var nilPtr *int
o = mon.FromPtr(nilPtr) // None[int]()Check state.
o := mon.Some(42)
o.IsSome() // true
o.IsNone() // falseGet value or panic.
val := o.Unwrap() // 42
// None case
none := mon.None[int]()
none.Unwrap() // panic!Get value or default.
o.UnwrapOr(0) // 42
mon.None[int]().UnwrapOr(0) // 0
// With computation
mon.None[int]().UnwrapOrElse(func() int {
return expensiveDefault()
})Unwrap with message.
val := o.Expect("should have value")Transform value if present.
o := mon.Some(5)
doubled := o.Map(func(n int) int {
return n * 2
})
// doubled = Some(10)
// None passes through
mon.None[int]().Map(func(n int) int { return n * 2 })
// still NoneChain optional operations.
o := mon.Some(5)
result := o.FlatMap(func(n int) mon.Option[int] {
if n < 0 {
return mon.None[int]()
}
return mon.Some(n * 2)
})
// result = Some(10)Convert to None if predicate fails.
o := mon.Some(5)
even := o.Filter(func(n int) bool {
return n%2 == 0
})
// even = None[int]() (5 is odd)Provide alternative.
none := mon.None[int]()
alt := mon.Some(42)
result := none.Or(alt)
// result = Some(42)import (
"github.com/modfin/henry/mon"
"github.com/modfin/henry/slicez"
)
// Parse URLs safely
func parseURLs(urls []string) ([]*url.URL, error) {
results := slicez.Map(urls, func(s string) mon.Result[*url.URL] {
u, err := url.Parse(s)
return mon.From(u, err)
})
return mon.Unwrap(results)
}
// Usage
urls := []string{
"https://example.com",
"https://github.com",
"bad url",
}
parsed, err := parseURLs(urls)
// parsed contains valid URLs, err is first parse errorfunc findUser(id int) mon.Option[User] {
user, err := db.GetUser(id)
if err != nil {
return mon.None[User]()
}
return mon.Some(user)
}
// Usage
userOpt := findUser(42)
if user, ok := userOpt.UnwrapOr(User{}); ok {
fmt.Println(user.Name)
}
// Or with default
user := findUser(42).UnwrapOr(guestUser)result := fetchUser(id).
FlatMap(func(u User) mon.Result[Profile] {
return fetchProfile(u.ProfileID)
}).
Map(func(p Profile) Profile {
p.LastSeen = time.Now()
return p
})
if result.IsErr() {
log.Printf("Failed: %v", result.Err())
return
}
profile := result.Unwrap()Standard Go:
val1, err := step1()
if err != nil {
return nil, err
}
val2, err := step2(val1)
if err != nil {
return nil, err
}
return step3(val2)With Result:
return step1().
FlatMap(step2).
FlatMap(step3)Use Result when:
- Error handling is the primary concern
- You want to defer error checking
- Building complex pipelines
Use Option when:
- Nullable values
- Optional configuration
- Caching/memoization
Use standard Go when:
- Simple sequential code
- Early returns are needed
- Team prefers idiomatic Go