monadz

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 18 Imported by: 0

README

monadz

Monads for Go 1.27: Option, Result, Either, Validation, Patch, Lazy, Task, State, Reader, and Writer.

Try it in your browser: the interactive playground runs the real library, compiled to WebAssembly. Change an input and watch each step.

Monadz matches the Option and Result APIs of Rust and the core monads of Haskell. It uses Go 1.27 generic methods, so a pipeline reads left to right and can change the type at each step:

port := monadz.Lookup(env, "PORT"). // Option[string]
	OkOr(ErrNoPort).               // Result[string]
	Then(strconv.Atoi).            // Result[int]
	Ensure(validPort, ErrBadPort)  // Result[int]
go get github.com/zkrebbekx/monadz

Monadz needs Go 1.27 or later with the jsonv2 experiment, which is on by default in Go 1.27. Do not set GOEXPERIMENT=nojsonv2. The module has no dependencies. The tests use only the standard testing package.

Why monadz

Goal What monadz does
Use the shapes Go code already has Result.Then takes func(T) (U, error). Option.Then takes func(T) (U, bool). Functions such as strconv.Atoi, url.Parse, and os.LookupEnv work directly, with no adapter.
A bug must crash Do recovers only its own early-exit signal. A nil dereference still crashes, unless a Try runs in a deferred function (see Limits). DoRecover is the explicit opt-in.
Context first A Task receives a context.Context. The concurrency functions cancel the work that is no longer necessary, and wait until it stops. Task.Timeout is the one exception.
Panics keep their stack A panic in a task that runs in another goroutine is raised again in the waiting goroutine, with the original stack trace.
Standard library integration iter.Seq, encoding/json v1 and v2, database/sql, log/slog, and fmt.
No silent wrong answers Err(nil) is still a failure. omitzero keeps Some(0). A PATCH field can tell null from "not sent".

See docs/parity.md for a method-by-method comparison with Rust and Haskell, and docs/from-mo.md if you use samber/mo today.

The types

Type Holds Rust / Haskell equivalent
Option[T] a T, or nothing Option<T> / Maybe a
Result[T] a T, or an error Result<T, E> / Either e a
Either[L, R] an L or an R, both valid Either a b
Validation[T] a T, or ALL the errors Data.Validation
Patch[T] not sent, null, or a value —
Lazy[T] a T computed on first use, then cached LazyLock<T> / lazy evaluation
Task[T] work that takes a context and can fail async fn / ReaderT Context (ExceptT error IO) a
State[S, A] a step that reads and writes a state State s a
Reader[R, A] a computation that reads an environment Reader r a
Writer[W, A] a value plus a log Writer [w] a

Option

planOf := map[string]string{"acme": "pro"}
seats := map[string]int{"pro": 25}

n := monadz.Lookup(planOf, "acme"). // Option[string]
	AndThen(func(plan string) monadz.Option[int] {
		return monadz.Lookup(seats, plan) // Option[int]
	}).
	Filter(func(n int) bool { return n > 0 }).
	ValueOr(5)
fmt.Println(n) // 25

// Then takes func(T) (U, bool), so os.LookupEnv works directly.
home := monadz.Some("HOME").Then(os.LookupEnv) // Option[string]

// In-place changes, like Rust: Take, Replace, Insert, GetOrInsertWith.
var cache monadz.Option[string]
cfg := cache.GetOrInsertWith(func() string { return "loaded" }) // *string
fmt.Println(*cfg, cache) // loaded Some(loaded)

Option implements json.Marshaler, json.Unmarshaler, the JSON v2 MarshalerTo and UnmarshalerFrom, sql.Scanner, driver.Valuer, slog.LogValuer, and fmt.Stringer. In Go 1.27, encoding/json (v1) calls MarshalJSONTo and UnmarshalJSONFrom first. MarshalJSON and UnmarshalJSON exist for code that calls them directly.

Only None is zero, so json:",omitzero" drops None and keeps Some(0). Use omitzero, not omitempty (see Limits).

Result

func LoadInvoice(ctx context.Context, id string) monadz.Result[Invoice] {
	return monadz.Of(repo.Find(ctx, id)). // Result[Row]
		Wrap("load invoice").           // add context to the error
		Then(decodeRow).                // func(Row) (Invoice, error)
		Ensure(Invoice.IsOpen, ErrClosed).
		InspectErr(logFailure)
}

status := res.Match(
	func(inv Invoice) int { return http.StatusOK },
	func(err error) int { return statusFor(err) },
)

errors.Is and errors.As work on res.Err() through every step.

Do and DoRecover: Go's ? operator

Inside Do, call Try() on a Result or an Option. On a failure, Try stops the block and Do returns the error:

inv := monadz.Do(func() Invoice {
	cust := repo.Customer(ctx, id).Try()
	plan := monadz.Lookup(plans, cust.PlanID).OkOr(ErrNoPlan).Try()
	return rate(cust, plan)
})

Monadz has two variants. They differ only in how they treat a panic that is NOT a Try signal:

Function A failed Try A real panic (a bug)
Do returns Err crashes, as a bug should
DoRecover returns Err returns Err with a *PanicError and its stack

Use Do by default. Use DoRecover at a boundary where a crash costs more than one failed request, for example an HTTP handler or a queue consumer.

Monadz does not use the name MustDo. In Go, Must means "panic if this fails" (regexp.MustCompile). A MustDo that recovers panics would mean the opposite. Must() stays a method with the normal meaning: return the value, or panic.

DoOption is the same idea for Option: Try on None makes it return None.

Either

Use Either when both outcomes are valid and carry data:

func CheckFeature(t Tenant, f Feature) monadz.Either[Paywall, Grant] { ... }

resp := CheckFeature(tenant, SSO).Match(
	func(p Paywall) Response { return upgradeOffer(p) }, // HTTP 402
	func(g Grant) Response { return allowed(g) },
)

Either is right-biased: Map, AndThen, MapOr, and Inspect work on Right. MapLeft, InspectLeft, BiMap, OrElse, LeftOrElse, and Swap work on Left. LeftOr and RightOr are Haskell's fromLeft and fromRight.

Validation

Result stops at the first error. Validation collects all of them:

signup := validations.Map3(
	email(req.Email).Field("email"),
	password(req.Password).Field("password"),
	company(req.Company).Field("company"),
	NewSignup,
)
// Invalid(email: not an email; password: too short)

Each error is a *monadz.FieldError, so an API can map it to the field.

ValidIf(v, ok, err) makes one check. ZipWith combines two Validation values and keeps the errors of both. Match, ValueOr, ValueOrElse, ValueOrZero, Must, Expect, Inspect, and MapErr work as they do on Result.

Patch

A JSON PATCH body has three states per field. An Option cannot tell null from "not sent". Patch can:

type UpdateTenant struct {
	Name monadz.Patch[string] `json:"name,omitzero"`
	Logo monadz.Patch[string] `json:"logo,omitzero"`
}

// {"logo": null}  ->  Name is Missing (keep), Logo is Null (clear)
req.Name.Apply(&tenant.Name)
req.Logo.ApplyOption(&tenant.Logo)

Patch works with JSON v1 and v2. Option() returns Some for a set value and None for the other two states. Map changes a set value, and Match handles each of the three states.

Task

A Task describes work. Nothing runs until Run. You can run it again, so retry and timeout are methods:

charge := monadz.NewTask(func(ctx context.Context) (ChargeID, error) {
	return payments.Charge(ctx, req) // same idempotency key on every attempt
}).
	Timeout(5 * time.Second).
	Retry(monadz.RetryPolicy{
		MaxAttempts: 4,
		Backoff:     monadz.ExponentialBackoff(200*time.Millisecond, 5*time.Second),
	})

id := charge.Run(ctx)

A nil RetryIf retries every error, including the timeout of one attempt. Retry stops when its own ctx ends.

Wrap, Wrapf, Ensure, MapErr, and Recover work as they do on Result. A task that calls runtime.Goexit gives Err(monadz.ErrGoexit). Once caches the first result and a panic, as sync.OnceValue does. It does not cache a context error.

Package tasks runs many tasks at once:

Function Returns On a failure
tasks.All(ts...) all values, in input order cancels the rest, returns the failure
tasks.AllLimit(n, ts...) same as All, at most n at once same
tasks.Traverse(items, n, f) f over each item, at most n at once same
tasks.AllSettled(ts...) every Result does not cancel
tasks.Any(ts...) the first success joins all errors if every task fails
tasks.Race(ts...) the result of the first task that stops —
tasks.Zip(a, b) both values cancels the other

Every function returns only after every task that it started has stopped. Task.Timeout is the one exception to this rule. It returns at the deadline, and it does not wait for the work, even work that observes ctx.

State, Reader, Writer

// A pure, replayable rate-limiter step: refill one token, then take one.
type Bucket struct{ Tokens int }

refill := monadz.ModifyState(func(b Bucket) Bucket { b.Tokens++; return b })
take := monadz.NewState(func(b Bucket) (bool, Bucket) {
	if b.Tokens == 0 {
		return false, b
	}
	b.Tokens--
	return true, b
})
allowed, next := refill.And(take).Run(Bucket{})
fmt.Println(allowed, next.Tokens) // true 0

// Dependencies through a Reader.
type Config struct{ Brand string }

greeting := monadz.NewReader(func(c Config) string { return c.Brand }).
	Map(func(b string) string { return "Welcome to " + b })
fmt.Println(greeting.Run(Config{Brand: "Acme"})) // Welcome to Acme

// An audit log through a Writer.
w := monadz.Tell("plan changed").AndThen(func(monadz.Unit) monadz.Writer[string, int] {
	return monadz.NewWriter(25, "seats set")
})
seatCount, entries := w.Run()
fmt.Println(seatCount, entries) // 25 [plan changed seats set]

Helper packages

The plural packages hold functions over many values, in the style of the standard slices and maps packages:

Package Functions
options Zip, Zip3, Map3, Unzip, Flatten, Transpose, Collect, Traverse, Values, FilterMap, First, Compose, Equal, Contains, Find, Head, Last, Min, Max
results Zip, Zip3, Map3, Flatten, Transpose, FromFunc, Compose, Collect, FromSeq2, Seq, Traverse, TraverseFunc, Fold, Partition, Oks, Errs, First
eithers Partition, Lefts, Rights, Flatten, ToResult, Collect
validations Map2 … Map6, Collect, Traverse, Errors
tasks All, AllLimit, AllSettled, Any, Race, Traverse, Zip, Zip3

The collection functions work on iter.Seq, so they connect to slices.Values, maps.Keys, and your own iterators. results.FromSeq2 collects a (row, error) iterator in one call.

Limits

  • Do not call Try in a deferred function. During a panic, the Try signal replaces the panic, so Do returns Err and the program does not crash.
  • Do not call Try under code that recovers panics, for example a String method that fmt calls, or a text/template function. That code recovers the signal, so the failure is lost.
  • Use omitzero, not omitempty. JSON v2 omitempty drops every field that encodes as null or "". It drops a Null Patch and Some("").
  • Some(None) and Some of a nil pointer encode as null, and decode as None. Rust serde does the same. Use Patch when you must keep the difference between null and a missing field.

Go 1.27 notes

Go 1.27 generic methods have limits that shape this API. See docs/go127.md.

  1. An interface method cannot have type parameters. Go has no Monad interface, so each type has its own methods.
  2. A method of a generic type cannot mention a type that grows from its own type parameter ("instantiation cycle"). This rule dates from Go 1.18 and applies to every method. Generic methods only make you meet it more often. Zip would return Option[Pair[T, U]], so Zip is a function in each helper package. The ZipWith methods are allowed.
  3. A receiver cannot be Option[Option[T]], because a receiver type parameter must be an identifier. So Flatten and Transpose are functions.

License

MIT

Documentation

Overview

Package monadz provides monadic types for Go 1.27 and later: Option, Result, Either, Validation, Patch, Lazy, Task, State, Reader, and Writer.

Monadz needs Go 1.27 or later with the jsonv2 experiment, which is on by default in Go 1.27. Do not set GOEXPERIMENT=nojsonv2.

Monadz uses Go 1.27 generic methods. A method can change the type of the value, so a pipeline reads left to right:

port := monadz.Lookup(env, "PORT"). // Option[string]
	OkOr(ErrNoPort).               // Result[string]
	Then(strconv.Atoi).            // Result[int]
	Ensure(validPort, ErrBadPort)  // Result[int]

Inside Do, Try gives the value of a Result or an Option, or stops the block and makes Do return the error:

inv := monadz.Do(func() Invoice {
	cust := repo.Customer(ctx, id).Try()
	plan := monadz.Lookup(plans, cust.PlanID).OkOr(ErrNoPlan).Try()
	return rate(cust, plan)
})

Design rules

  • Accept the shapes that Go code already has. Result.Then takes func(T) (U, error), and Option.Then takes func(T) (U, bool). Functions such as strconv.Atoi, url.Parse, and os.LookupEnv work directly.
  • A bug must crash. Do recovers only its own early-exit signal. Use DoRecover when you want to turn other panics into errors, and you get a *PanicError with the stack trace.
  • Be context-aware. A Task receives a context.Context, and the functions in package tasks cancel the work that is no longer necessary.
  • Integrate with the standard library: iter.Seq, encoding/json (v1 and v2), database/sql, log/slog, and fmt.

Limits

  • Do not call Try in a deferred function. During a panic, the Try signal replaces the panic, so Do returns Err and the program does not crash.
  • Do not call Try under code that recovers panics, such as a String method that fmt calls, or a text/template function. The failure is lost.
  • Tag Option and Patch fields with omitzero, not omitempty. JSON v2 omitempty drops a Null Patch and Some("").
  • Some(None) and Some of a nil pointer encode as null and decode as None. Use Patch to tell null from a missing field.

Packages

Package monadz holds the types and their methods. The plural packages hold functions that work on many values, in the style of the slices and maps packages:

  • options: Collect, Traverse, Values, FilterMap, First, Flatten, Zip, ...
  • results: Collect, Traverse, Partition, FromSeq2, Fold, Map3, Zip, ...
  • eithers: Partition, Lefts, Rights, Collect, ...
  • validations: Map2 ... Map6, Collect, Traverse (error accumulation)
  • tasks: All, AllLimit, AllSettled, Any, Race, Traverse (with a concurrency limit)

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrGoexit = errors.New("monadz: task goroutine called runtime.Goexit")

ErrGoexit is the error of a Task whose goroutine called runtime.Goexit, for example through testing.T.FailNow.

View Source
var ErrNilError = errors.New("monadz: failure constructed with a nil error")

ErrNilError is the error that Err, Invalid, and similar constructors store when you give them a nil error. A nil error cannot mean failure, so monadz replaces it with this error instead of returning a success by mistake.

View Source
var ErrNone = errors.New("monadz: value is None")

ErrNone is the error for a missing value: Option.Must on None, and Option.Try on None inside Do.

View Source
var ErrZeroTask = errors.New("monadz: Run on a zero Task; use NewTask")

ErrZeroTask is the error from Run on a zero Task.

Functions

func ConstantBackoff

func ConstantBackoff(d time.Duration) func(int) time.Duration

ConstantBackoff waits d before every retry.

func ExponentialBackoff

func ExponentialBackoff(base, limit time.Duration) func(int) time.Duration

ExponentialBackoff waits base, 2*base, 4*base, ..., up to limit.

func Sleep

func Sleep(ctx context.Context, d time.Duration) error

Sleep waits for d, or until ctx ends. It returns ctx.Err() if ctx ends first.

Types

type Either

type Either[L, R any] struct {
	// contains filtered or unexported fields
}

Either holds a value of type L (Left) or a value of type R (Right).

Use Either when both outcomes are valid and carry their own data, for example Either[Paywall, Grant]. For "a value or an error", use Result.

Either is right-biased: Map and AndThen work on the Right value, as in Haskell and Scala. The zero value is Left(zero L).

func Left

func Left[L, R any](l L) Either[L, R]

Left returns an Either that holds the Left value l. Go cannot infer R, so write both type arguments: monadz.Left[Paywall, Grant](p).

func Right[L, R any](r R) Either[L, R]

Right returns an Either that holds the Right value r. Go cannot infer L, so write it: monadz.Right[Paywall](grant).

func (Either[L, R]) AndThen

func (e Either[L, R]) AndThen[R2 any](f func(R) Either[L, R2]) Either[L, R2]

AndThen applies f to the Right value and returns its Either. This is the monad bind. (Haskell: >>=.)

func (Either[L, R]) BiMap

func (e Either[L, R]) BiMap[L2, R2 any](onLeft func(L) L2, onRight func(R) R2) Either[L2, R2]

BiMap applies onLeft or onRight to the value. (Haskell Bifunctor: bimap.)

func (Either[L, R]) Inspect

func (e Either[L, R]) Inspect(f func(R)) Either[L, R]

Inspect calls f with the Right value, if any, and returns the Either unchanged.

func (Either[L, R]) InspectLeft

func (e Either[L, R]) InspectLeft(f func(L)) Either[L, R]

InspectLeft calls f with the Left value, if any, and returns the Either unchanged.

func (Either[L, R]) IsLeft

func (e Either[L, R]) IsLeft() bool

IsLeft reports whether the Either holds a Left value.

func (Either[L, R]) IsLeftAnd

func (e Either[L, R]) IsLeftAnd(pred func(L) bool) bool

IsLeftAnd reports whether the Either holds a Left value that satisfies pred.

func (Either[L, R]) IsRight

func (e Either[L, R]) IsRight() bool

IsRight reports whether the Either holds a Right value.

func (Either[L, R]) IsRightAnd

func (e Either[L, R]) IsRightAnd(pred func(R) bool) bool

IsRightAnd reports whether the Either holds a Right value that satisfies pred.

func (Either[L, R]) Left

func (e Either[L, R]) Left() Option[L]

Left returns the Left value as an Option. (Haskell: leftToMaybe, in the either package.)

func (Either[L, R]) LeftOr

func (e Either[L, R]) LeftOr(fallback L) L

LeftOr returns the Left value, or fallback. (Haskell: fromLeft.)

func (Either[L, R]) LeftOrElse

func (e Either[L, R]) LeftOrElse(f func(R) L) L

LeftOrElse returns the Left value, or f applied to the Right value.

func (Either[L, R]) Map

func (e Either[L, R]) Map[R2 any](f func(R) R2) Either[L, R2]

Map applies f to the Right value. (Haskell: fmap.)

func (Either[L, R]) MapLeft

func (e Either[L, R]) MapLeft[L2 any](f func(L) L2) Either[L2, R]

MapLeft applies f to the Left value. (Haskell Bifunctor: first.)

func (Either[L, R]) MapOr

func (e Either[L, R]) MapOr[U any](fallback U, f func(R) U) U

MapOr applies f to the Right value, or returns fallback for a Left. (Rust: Option::map_or; Haskell: either (const fallback) f.)

func (Either[L, R]) Match

func (e Either[L, R]) Match[U any](onLeft func(L) U, onRight func(R) U) U

Match calls onLeft or onRight and returns the result. (Haskell: either.)

Example

Use Either when both outcomes are valid and carry data. Match folds both sides to one type.

package main

import (
	"fmt"
	"strconv"

	"github.com/zkrebbekx/monadz"
)

func main() {
	check := func(plan string) monadz.Either[string, int] {
		if plan == "free" {
			return monadz.Left[string, int]("upgrade to use SSO")
		}
		return monadz.Right[string](10)
	}

	for _, plan := range []string{"free", "pro"} {
		status := check(plan).Match(
			func(offer string) string { return "402: " + offer },
			func(seats int) string { return "200: " + strconv.Itoa(seats) + " SSO seats" },
		)
		fmt.Println(status)
	}
}
Output:
402: upgrade to use SSO
200: 10 SSO seats

func (Either[L, R]) OrElse

func (e Either[L, R]) OrElse[L2 any](f func(L) Either[L2, R]) Either[L2, R]

OrElse applies f to the Left value and returns its Either. Use it to recover from a Left.

func (Either[L, R]) Right

func (e Either[L, R]) Right() Option[R]

Right returns the Right value as an Option. (Haskell: rightToMaybe, in the either package.)

func (Either[L, R]) RightOr

func (e Either[L, R]) RightOr(fallback R) R

RightOr returns the Right value, or fallback. (Haskell: fromRight.)

func (Either[L, R]) RightOrElse

func (e Either[L, R]) RightOrElse(f func(L) R) R

RightOrElse returns the Right value, or f applied to the Left value.

func (Either[L, R]) String

func (e Either[L, R]) String() string

String returns "Left(<value>)" or "Right(<value>)".

func (Either[L, R]) Swap

func (e Either[L, R]) Swap() Either[R, L]

Swap returns the Either with Left and Right exchanged.

func (Either[L, R]) Unpack

func (e Either[L, R]) Unpack() (L, R, bool)

Unpack returns the Left value, the Right value, and true if the Either holds a Right value. The value on the other side is its zero value.

type FieldError

type FieldError struct {
	Field string
	Err   error
}

FieldError is an error about one input field.

func (*FieldError) Error

func (e *FieldError) Error() string

Error returns "<field>: <err>", or "<field>: <nil>" for a nil Err.

func (*FieldError) Unwrap

func (e *FieldError) Unwrap() error

Unwrap returns the wrapped error.

type Future

type Future[T any] struct {
	// contains filtered or unexported fields
}

Future is a handle to a Task that runs in its own goroutine. Get one from Task.Start; the zero Future holds no Task.

func (*Future[T]) Await

func (f *Future[T]) Await() Result[T]

Await waits for the Task and returns its Result. If the Task panicked, Await panics in the caller's goroutine with a *PanicError that holds the original stack trace.

func (*Future[T]) AwaitContext

func (f *Future[T]) AwaitContext(ctx context.Context) Result[T]

AwaitContext is Await with a limit: it returns ctx.Err() if ctx ends first. It does not cancel the Task.

func (*Future[T]) Cancel

func (f *Future[T]) Cancel()

Cancel cancels the context of the Task. The Task stops only if its work observes the context.

func (*Future[T]) Done

func (f *Future[T]) Done() <-chan struct{}

Done returns a channel. Start closes it when the Task stops.

func (*Future[T]) Poll

func (f *Future[T]) Poll() Option[Result[T]]

Poll returns the Result if the Task has stopped, else None. It does not wait. If the Task panicked, Poll panics as Await does. (Rust: Future::poll.)

type Lazy

type Lazy[T any] struct {
	// contains filtered or unexported fields
}

Lazy holds a value that the first Get computes. Later calls to Get return the cached value. Get is safe for concurrent use; the function runs at most one time. If the function panics, every Get panics with the same value.

If the function calls runtime.Goexit, Get panics with a *runtime.PanicNilError, and every later Get panics too. This is the behaviour of sync.OnceValue.

Equivalents: Rust LazyLock, Haskell lazy evaluation.

func LazyOf

func LazyOf[T any](f func() (T, error)) Lazy[Result[T]]

LazyOf returns a Lazy for a function that can fail. The Lazy caches the error too, so a failed computation does not run again.

func NewLazy

func NewLazy[T any](f func() T) Lazy[T]

NewLazy returns a Lazy that computes f on the first Get.

func (Lazy[T]) AndThen

func (l Lazy[T]) AndThen[U any](f func(T) Lazy[U]) Lazy[U]

AndThen returns a Lazy that applies f to this value and gets the result.

func (Lazy[T]) Get

func (l Lazy[T]) Get() T

Get computes the value on the first call, and returns the cached value after that.

func (Lazy[T]) Map

func (l Lazy[T]) Map[U any](f func(T) U) Lazy[U]

Map returns a Lazy that applies f to this value. Nothing runs until Get.

func (Lazy[T]) ZipWith

func (l Lazy[T]) ZipWith[U, V any](other Lazy[U], f func(T, U) V) Lazy[V]

ZipWith returns a Lazy that combines both values with f.

type Option

type Option[T any] struct {
	// contains filtered or unexported fields
}

Option holds a value of type T (Some) or no value (None).

The zero value is None. Equivalents: Rust Option<T>, Haskell Maybe a.

Example

An Option holds a value or nothing. Map, Filter, and ValueOr work on it without a check for nil or a second bool.

package main

import (
	"fmt"

	"github.com/zkrebbekx/monadz"
)

func main() {
	seats := map[string]int{"pro": 25, "free": 0}

	pro := monadz.Lookup(seats, "pro").
		Filter(func(n int) bool { return n > 0 }).
		ValueOr(5)
	free := monadz.Lookup(seats, "free").
		Filter(func(n int) bool { return n > 0 }).
		ValueOr(5)
	missing := monadz.Lookup(seats, "team")

	fmt.Println(pro, free, missing)
}
Output:
25 5 None

func At

func At[T any](s []T, i int) Option[T]

At returns s[i], or None if i is out of range. A negative i is out of range: At does not count from the end. (Rust: slice::get.)

func DoOption

func DoOption[T any](f func() T) (res Option[T])

DoOption runs f and returns its value as Some. Inside f, Option.Try on None stops f and DoOption returns None.

Result.Try inside DoOption is a programming error, because None cannot carry the error. Its signal passes through as a panic. Convert the Result first with Result.Ok.

func FromPtr

func FromPtr[T any](p *T) Option[T]

FromPtr returns None for a nil pointer, else Some of the pointed-to value.

func FromTuple

func FromTuple[T any](v T, ok bool) Option[T]

FromTuple converts a "comma ok" pair: FromTuple(m[k]) is not legal Go, so write v, ok := m[k]; FromTuple(v, ok). See also Lookup.

func FromZero

func FromZero[T comparable](v T) Option[T]

FromZero returns None for the zero value of T, else Some(v). Use it for APIs where "" or 0 means "not set".

func Lookup

func Lookup[K comparable, V any](m map[K]V, k K) Option[V]

Lookup returns the value of key k in map m, or None.

func None

func None[T any]() Option[T]

None returns an Option with no value.

func Some

func Some[T any](v T) Option[T]

Some returns an Option that holds v.

func When

func When[T any](cond bool, v T) Option[T]

When returns Some(v) if cond is true, else None. (Rust: bool::then_some.)

func WhenFunc

func WhenFunc[T any](cond bool, f func() T) Option[T]

WhenFunc returns Some(f()) if cond is true, else None. f runs only when cond is true. (Rust: bool::then.)

func (Option[T]) All

func (o Option[T]) All() iter.Seq[T]

All returns an iterator over the value: one element for Some, none for None. Use it in a range loop. (Rust: iter; Haskell: maybeToList.)

func (Option[T]) And

func (o Option[T]) And[U any](other Option[U]) Option[U]

And returns other if the Option is Some, else None. (Rust: and; Haskell: *>.)

func (Option[T]) AndThen

func (o Option[T]) AndThen[U any](f func(T) Option[U]) Option[U]

AndThen applies f to the value and returns its Option. This is the monad bind. (Rust: and_then; Haskell: >>=.)

func (Option[T]) Expect

func (o Option[T]) Expect(msg string) T

Expect returns the value, or panics with msg. (Rust: expect.)

func (Option[T]) Filter

func (o Option[T]) Filter(pred func(T) bool) Option[T]

Filter returns the Option if its value satisfies pred, else None.

func (Option[T]) Get

func (o Option[T]) Get() (T, bool)

Get returns the value and true, or the zero value and false.

func (*Option[T]) GetOrInsert

func (o *Option[T]) GetOrInsert(v T) *T

GetOrInsert stores v if the Option is None, and returns a pointer to the stored value. (Rust: get_or_insert.)

func (*Option[T]) GetOrInsertWith

func (o *Option[T]) GetOrInsertWith(f func() T) *T

GetOrInsertWith stores f() if the Option is None, and returns a pointer to the stored value. f runs only for None. (Rust: get_or_insert_with.)

func (*Option[T]) Insert

func (o *Option[T]) Insert(v T) *T

Insert puts Some(v) in place and returns a pointer to the stored value. (Rust: insert.)

func (Option[T]) Inspect

func (o Option[T]) Inspect(f func(T)) Option[T]

Inspect calls f with the value, if any, and returns the Option unchanged.

func (Option[T]) IsNone

func (o Option[T]) IsNone() bool

IsNone reports whether the Option holds no value.

func (Option[T]) IsNoneOr

func (o Option[T]) IsNoneOr(pred func(T) bool) bool

IsNoneOr reports whether the Option is None or holds a value that satisfies pred.

func (Option[T]) IsSome

func (o Option[T]) IsSome() bool

IsSome reports whether the Option holds a value.

func (Option[T]) IsSomeAnd

func (o Option[T]) IsSomeAnd(pred func(T) bool) bool

IsSomeAnd reports whether the Option holds a value that satisfies pred.

func (Option[T]) IsZero

func (o Option[T]) IsZero() bool

IsZero reports whether the Option is None. Some(0), Some(""), and Some(false) are NOT zero, so `json:",omitzero"` drops only None.

Use omitzero, not omitempty. Under encoding/json/v2, omitempty drops every value that encodes as null, "", [] or {}, including a Null Patch and Some("").

func (Option[T]) LogValue

func (o Option[T]) LogValue() slog.Value

LogValue makes slog log the value itself, or null for None.

func (Option[T]) Map

func (o Option[T]) Map[U any](f func(T) U) Option[U]

Map applies f to the value. (Rust: map; Haskell: fmap.)

func (Option[T]) MapOr

func (o Option[T]) MapOr[U any](fallback U, f func(T) U) U

MapOr applies f to the value, or returns fallback. (Rust: map_or.)

func (Option[T]) MapOrElse

func (o Option[T]) MapOrElse[U any](fallback func() U, f func(T) U) U

MapOrElse applies f to the value, or returns fallback(). (Rust: map_or_else.)

func (Option[T]) MarshalJSON

func (o Option[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler: the value, or null for None.

encoding/json in Go 1.27 calls MarshalJSONTo first. This method is for direct callers.

func (Option[T]) MarshalJSONTo

func (o Option[T]) MarshalJSONTo(enc *jsontext.Encoder) error

MarshalJSONTo implements the encoding/json/v2 MarshalerTo interface.

func (Option[T]) Match

func (o Option[T]) Match[U any](onSome func(T) U, onNone func() U) U

Match calls onSome with the value, or onNone, and returns the result. (Haskell: maybe.)

func (Option[T]) Must

func (o Option[T]) Must() T

Must returns the value, or panics with ErrNone. (Rust: unwrap.)

func (Option[T]) OkOr

func (o Option[T]) OkOr(err error) Result[T]

OkOr converts Some(v) to Ok(v), and None to Err(err). (Rust: ok_or.)

func (Option[T]) OkOrElse

func (o Option[T]) OkOrElse(f func() error) Result[T]

OkOrElse converts Some(v) to Ok(v), and None to Err(f()). f runs only for None. (Rust: ok_or_else.)

func (Option[T]) Or

func (o Option[T]) Or(other Option[T]) Option[T]

Or returns the Option if it is Some, else other. (Rust: or; Haskell: <|>.)

func (Option[T]) OrElse

func (o Option[T]) OrElse(f func() Option[T]) Option[T]

OrElse returns the Option if it is Some, else f(). f runs only for None.

func (Option[T]) Ptr

func (o Option[T]) Ptr() *T

Ptr returns a pointer to a copy of the value, or nil.

func (*Option[T]) Replace

func (o *Option[T]) Replace(v T) Option[T]

Replace puts Some(v) in place and returns the old Option. (Rust: replace.)

func (*Option[T]) Scan

func (o *Option[T]) Scan(src any) error

Scan implements sql.Scanner: a NULL column gives None.

func (Option[T]) String

func (o Option[T]) String() string

String returns "Some(<value>)" or "None".

func (*Option[T]) Take

func (o *Option[T]) Take() Option[T]

Take returns the Option and leaves None in its place. (Rust: take.)

func (*Option[T]) TakeIf

func (o *Option[T]) TakeIf(pred func(T) bool) Option[T]

TakeIf takes the value only if it satisfies pred. (Rust: take_if.)

func (Option[T]) Then

func (o Option[T]) Then[U any](f func(T) (U, bool)) Option[U]

Then applies a "comma ok" function to the value: a func(T) (U, bool), such as os.LookupEnv.

func (Option[T]) Try

func (o Option[T]) Try() T

Try returns the value. On None, it stops the enclosing Do or DoOption. See Do.

func (*Option[T]) UnmarshalJSON

func (o *Option[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler: null gives None. To tell null from a missing field, use Patch.

Some(v) where v encodes as null (a None, or a nil pointer) decodes as None. Use Patch to keep the difference. Rust serde behaves the same.

encoding/json in Go 1.27 calls UnmarshalJSONFrom first. This method is for direct callers.

func (*Option[T]) UnmarshalJSONFrom

func (o *Option[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error

UnmarshalJSONFrom implements the encoding/json/v2 UnmarshalerFrom interface: null gives None. As with UnmarshalJSON, Some(v) where v encodes as null decodes as None.

func (Option[T]) Value

func (o Option[T]) Value() (driver.Value, error)

Value implements driver.Valuer: None gives NULL.

Value accepts only the value types of database/sql's DefaultParameterConverter, or a T that implements driver.Valuer. For any other T, Value returns an error.

func (Option[T]) ValueOr

func (o Option[T]) ValueOr(fallback T) T

ValueOr returns the value, or fallback. (Rust: unwrap_or; Haskell: fromMaybe.)

func (Option[T]) ValueOrElse

func (o Option[T]) ValueOrElse(f func() T) T

ValueOrElse returns the value, or the result of f. f runs only for None. (Rust: unwrap_or_else.)

func (Option[T]) ValueOrZero

func (o Option[T]) ValueOrZero() T

ValueOrZero returns the value, or the zero value of T. (Rust: unwrap_or_default.)

func (Option[T]) Xor

func (o Option[T]) Xor(other Option[T]) Option[T]

Xor returns the Option that is Some if exactly one of the two is Some, else None.

func (Option[T]) ZipWith

func (o Option[T]) ZipWith[U, V any](other Option[U], f func(T, U) V) Option[V]

ZipWith combines both values with f if both are Some, else None. (Haskell: liftA2.)

type Pair

type Pair[A, B any] struct {
	First  A
	Second B
}

Pair holds two values. The Zip functions in the plural packages return a Pair.

func PairOf

func PairOf[A, B any](a A, b B) Pair[A, B]

PairOf returns a Pair.

func (Pair[A, B]) String

func (p Pair[A, B]) String() string

String formats the Pair as "(a, b)".

func (Pair[A, B]) Swap

func (p Pair[A, B]) Swap() Pair[B, A]

Swap returns the Pair with the values in the other order.

func (Pair[A, B]) Unpack

func (p Pair[A, B]) Unpack() (A, B)

Unpack returns the two values.

type PanicError

type PanicError struct {
	// Value is the argument of the panic call.
	Value any
	// Stack is the stack trace of the goroutine at the time of the panic.
	Stack []byte
	// contains filtered or unexported fields
}

PanicError holds a recovered panic. DoRecover returns it inside an Err. A Task that runs in another goroutine captures a panic in a PanicError, and Await raises it again in the waiting goroutine.

func (*PanicError) Error

func (p *PanicError) Error() string

Error returns "panic: <value>". After the panic crossed goroutines, the text also holds the stack of the goroutine that panicked.

func (*PanicError) Unwrap

func (p *PanicError) Unwrap() error

Unwrap returns the panic value if it is an error (for example a runtime.Error), so errors.Is and errors.As see it.

type Patch

type Patch[T any] struct {
	// contains filtered or unexported fields
}

Patch is a field of a JSON PATCH request. It has three states:

field not in the JSON   -> Missing: do not change the stored value
"field": null           -> Null:    clear the stored value
"field": <value>        -> Set:     store the value

An Option cannot tell Missing from Null. Patch can. The zero value is Missing. Tag the field with `json:",omitzero"` so that a Missing field is not written.

Use omitzero, not omitempty. Under encoding/json/v2, omitempty drops every value that encodes as null, "", [] or {}, including a Null Patch and Some("").

Example

A Patch field tells a missing field from null. Apply keeps the old value for a missing field and clears it for null.

package main

import (
	"encoding/json"
	"fmt"

	"github.com/zkrebbekx/monadz"
)

func main() {
	type update struct {
		Name monadz.Patch[string] `json:"name,omitzero"`
		Logo monadz.Patch[string] `json:"logo,omitzero"`
	}

	var req update
	if err := json.Unmarshal([]byte(`{"logo": null}`), &req); err != nil {
		panic(err)
	}

	name, logo := "Acme", "acme.png"
	req.Name.Apply(&name)
	req.Logo.Apply(&logo)

	fmt.Printf("%v %v %q %q\n", req.Name, req.Logo, name, logo)
}
Output:
Missing Null "Acme" ""

func PatchMissing

func PatchMissing[T any]() Patch[T]

PatchMissing returns a Patch that leaves the value unchanged.

func PatchNull

func PatchNull[T any]() Patch[T]

PatchNull returns a Patch that clears the value.

func PatchSet

func PatchSet[T any](v T) Patch[T]

PatchSet returns a Patch that sets the value v.

func (Patch[T]) Apply

func (p Patch[T]) Apply(dst *T)

Apply changes *dst: Set stores the value, Null stores the zero value, and Missing does nothing.

func (Patch[T]) ApplyOption

func (p Patch[T]) ApplyOption(dst *Option[T])

ApplyOption changes *dst: Set stores Some, Null stores None, and Missing does nothing.

func (Patch[T]) IsMissing

func (p Patch[T]) IsMissing() bool

IsMissing reports whether the field was not in the request.

func (Patch[T]) IsNull

func (p Patch[T]) IsNull() bool

IsNull reports whether the field was null.

func (Patch[T]) IsSet

func (p Patch[T]) IsSet() bool

IsSet reports whether the field holds a value.

func (Patch[T]) IsZero

func (p Patch[T]) IsZero() bool

IsZero reports whether the Patch is Missing. It makes omitzero work.

func (Patch[T]) Map

func (p Patch[T]) Map[U any](f func(T) U) Patch[U]

Map applies f to the value of a Set Patch. A Null Patch stays Null, and a Missing Patch stays Missing.

func (Patch[T]) MarshalJSON

func (p Patch[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. Missing and Null both write null; use omitzero to drop a Missing field.

encoding/json in Go 1.27 calls MarshalJSONTo first. This method is for direct callers.

func (Patch[T]) MarshalJSONTo

func (p Patch[T]) MarshalJSONTo(enc *jsontext.Encoder) error

MarshalJSONTo implements the encoding/json/v2 MarshalerTo interface. Missing and Null both write null.

func (Patch[T]) Match

func (p Patch[T]) Match[U any](onSet func(T) U, onNull func() U, onMissing func() U) U

Match calls onSet with the value, or onNull, or onMissing, and returns the result.

func (Patch[T]) Option

func (p Patch[T]) Option() Option[T]

Option returns the value as an Option: Some for Set, None otherwise.

func (Patch[T]) String

func (p Patch[T]) String() string

String returns "Set(<value>)", "Null", or "Missing".

func (*Patch[T]) UnmarshalJSON

func (p *Patch[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler: null gives Null, and a value gives Set. As with Option, a value that decodes from null gives Null.

encoding/json in Go 1.27 calls UnmarshalJSONFrom first. This method is for direct callers.

func (*Patch[T]) UnmarshalJSONFrom

func (p *Patch[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error

UnmarshalJSONFrom implements the encoding/json/v2 UnmarshalerFrom interface: null gives Null, and a value gives Set.

type Reader

type Reader[R, A any] struct {
	// contains filtered or unexported fields
}

Reader is a computation that reads a shared environment R, such as a config or a set of dependencies. Nothing runs until Run. (Haskell: Reader r a.)

The zero Reader has no function. Run on a zero Reader panics; use NewReader.

func Ask

func Ask[R any]() Reader[R, R]

Ask returns a Reader whose value is the environment. (Haskell: ask.)

func NewReader

func NewReader[R, A any](f func(R) A) Reader[R, A]

NewReader wraps a function of the environment. (Haskell: asks.)

func ReaderOf

func ReaderOf[R, A any](a A) Reader[R, A]

ReaderOf returns a Reader that ignores the environment and returns a.

func (Reader[R, A]) AndThen

func (m Reader[R, A]) AndThen[B any](f func(A) Reader[R, B]) Reader[R, B]

AndThen passes the value to f and runs the Reader that f returns with the same environment. (Haskell: >>=.)

func (Reader[R, A]) Local

func (m Reader[R, A]) Local(f func(R) R) Reader[R, A]

Local runs the Reader with a changed environment. (Haskell: local.)

func (Reader[R, A]) Map

func (m Reader[R, A]) Map[B any](f func(A) B) Reader[R, B]

Map applies f to the value.

func (Reader[R, A]) Run

func (m Reader[R, A]) Run(r R) A

Run runs the Reader with the environment r. Run panics on a zero Reader.

type Result

type Result[T any] struct {
	// contains filtered or unexported fields
}

Result holds a value of type T (Ok) or an error (Err).

The zero value is Ok(zero T), which matches Go's (zero, nil) convention. Equivalents: Rust Result<T, E> with E = error, Haskell Either error a. For a typed failure value, use Either.

func Do

func Do[T any](f func() T) (res Result[T])

Do runs f and returns its value as Ok. Inside f, call Try on a Result or an Option to get its value. If the Result is Err or the Option is None, Try stops f at once and Do returns the error. This is Go's version of Rust's "?" operator and Haskell's do notation:

monadz.Do(func() Invoice {
	cust := repo.Customer(ctx, id).Try()
	plan := monadz.Lookup(plans, cust.PlanID).OkOr(ErrNoPlan).Try()
	return rate(cust, plan)
})

Do recovers ONLY the signal from Try. Any other panic (a nil dereference, an index out of range, an explicit panic) passes through, so a bug still crashes. Use DoRecover to turn every panic into an error.

An Option.Try on None returns ErrNone. Add context with OkOr first.

Call Try only in the goroutine that runs f. A Try in another goroutine is not inside Do, and its panic crashes the program.

Call Try only where no code between Try and Do recovers panics, for example a fmt String method or a text/template function. That code catches the Try signal, and Do does not see the failure.

Do not call Try in a deferred function. If a panic is in flight, the Try signal replaces it, and Do returns Err instead of crashing. In a defer, join the error into a named result with errors.Join.

Example

Inside Do, Try returns the value of a Result or an Option. On a failure, Try stops the block and Do returns the error.

package main

import (
	"errors"
	"fmt"

	"github.com/zkrebbekx/monadz"
)

func main() {
	errNoPlan := errors.New("no plan")
	plans := map[string]int{"pro": 25}

	seats := func(plan string) monadz.Result[int] {
		return monadz.Do(func() int {
			n := monadz.Lookup(plans, plan).OkOr(errNoPlan).Try()
			return n * 2
		})
	}

	fmt.Println(seats("pro"))
	fmt.Println(seats("team"))
}
Output:
Ok(50)
Err(no plan)

func DoRecover

func DoRecover[T any](f func() T) (res Result[T])

DoRecover is Do plus a recover for every other panic. A panic that is not a Try signal becomes an Err that holds a *PanicError with the stack trace.

Use DoRecover at a boundary where a crash costs more than a failed request: an HTTP handler, a queue consumer, or a plugin call.

Example

Do lets a real panic (a bug) crash. DoRecover changes it into an Err that holds a *monadz.PanicError with the stack trace.

package main

import (
	"errors"
	"fmt"

	"github.com/zkrebbekx/monadz"
)

func main() {
	res := monadz.DoRecover(func() int {
		var m map[string]int
		m["x"] = 1 // a bug: assignment to a nil map
		return 0
	})

	var pe *monadz.PanicError
	fmt.Println(res.IsErr(), errors.As(res.Err(), &pe))
}
Output:
true true

func Err

func Err[T any](err error) Result[T]

Err returns a failed Result. A nil err becomes ErrNilError, so Err never returns a success by mistake.

func Errf

func Errf[T any](format string, args ...any) Result[T]

Errf returns a failed Result with fmt.Errorf(format, args...).

func FromErr

func FromErr(err error) Result[Unit]

FromErr converts a function that returns only an error: monadz.FromErr(db.PingContext(ctx)).

func Of

func Of[T any](v T, err error) Result[T]

Of converts a Go (value, error) pair: monadz.Of(strconv.Atoi(s)).

func Ok

func Ok[T any](v T) Result[T]

Ok returns a successful Result.

func (Result[T]) All

func (r Result[T]) All() iter.Seq[T]

All returns an iterator over the value: one element for Ok, none for Err.

func (Result[T]) And

func (r Result[T]) And[U any](other Result[U]) Result[U]

And returns other if the Result is Ok, else the failure. (Rust: and.)

func (Result[T]) AndThen

func (r Result[T]) AndThen[U any](f func(T) Result[U]) Result[U]

AndThen applies f to the value and returns its Result. This is the monad bind. (Rust: and_then; Haskell: >>=.)

func (Result[T]) Either

func (r Result[T]) Either() Either[error, T]

Either converts the Result to Either[error, T].

func (Result[T]) Ensure

func (r Result[T]) Ensure(pred func(T) bool, err error) Result[T]

Ensure returns Err(err) if the value does not satisfy pred. (Scala: filterOrElse; Haskell: mfilter.)

func (Result[T]) Err

func (r Result[T]) Err() error

Err returns the error, or nil for a success.

func (Result[T]) Expect

func (r Result[T]) Expect(msg string) T

Expect returns the value, or panics with msg and the error. (Rust: expect.)

func (Result[T]) Get

func (r Result[T]) Get() (T, error)

Get returns the Go pair (value, error). On a failure, the value is the zero value of T.

func (Result[T]) Inspect

func (r Result[T]) Inspect(f func(T)) Result[T]

Inspect calls f with the value, if any, and returns the Result unchanged.

func (Result[T]) InspectErr

func (r Result[T]) InspectErr(f func(error)) Result[T]

InspectErr calls f with the error, if any, and returns the Result unchanged. Use it to log a failure in the middle of a chain.

func (Result[T]) IsErr

func (r Result[T]) IsErr() bool

IsErr reports whether the Result is a failure.

func (Result[T]) IsErrAnd

func (r Result[T]) IsErrAnd(pred func(error) bool) bool

IsErrAnd reports whether the Result is a failure with an error that satisfies pred.

func (Result[T]) IsOk

func (r Result[T]) IsOk() bool

IsOk reports whether the Result is a success.

func (Result[T]) IsOkAnd

func (r Result[T]) IsOkAnd(pred func(T) bool) bool

IsOkAnd reports whether the Result is a success with a value that satisfies pred.

func (Result[T]) LogValue

func (r Result[T]) LogValue() slog.Value

LogValue makes slog log the value, or a group with the error message.

func (Result[T]) Map

func (r Result[T]) Map[U any](f func(T) U) Result[U]

Map applies f to the value. (Rust: map; Haskell: fmap.)

func (Result[T]) MapErr

func (r Result[T]) MapErr(f func(error) error) Result[T]

MapErr applies f to the error. (Rust: map_err.)

func (Result[T]) MapOr

func (r Result[T]) MapOr[U any](fallback U, f func(T) U) U

MapOr applies f to the value, or returns fallback. (Rust: map_or.)

func (Result[T]) MapOrElse

func (r Result[T]) MapOrElse[U any](onErr func(error) U, onOk func(T) U) U

MapOrElse applies onOk to the value, or onErr to the error. (Rust: map_or_else.)

func (Result[T]) Match

func (r Result[T]) Match[U any](onOk func(T) U, onErr func(error) U) U

Match calls onOk with the value or onErr with the error, and returns the result. Unlike mo's result.Match, onErr receives the error. (Haskell: either.)

func (Result[T]) Must

func (r Result[T]) Must() T

Must returns the value, or panics with the error. (Rust: unwrap.)

func (Result[T]) MustErr

func (r Result[T]) MustErr() error

MustErr returns the error, or panics for a success. (Rust: unwrap_err.)

func (Result[T]) Ok

func (r Result[T]) Ok() Option[T]

Ok converts the Result to an Option, and discards the error. (Rust: ok.)

func (Result[T]) Or

func (r Result[T]) Or(other Result[T]) Result[T]

Or returns the Result if it is Ok, else other. (Rust: or.)

func (Result[T]) OrElse

func (r Result[T]) OrElse(f func(error) Result[T]) Result[T]

OrElse returns the Result if it is Ok, else f(err). (Rust: or_else.)

func (Result[T]) Recover

func (r Result[T]) Recover(f func(error) (T, error)) Result[T]

Recover is OrElse with the Go shape func(error) (T, error). Return a value to recover, or return an error to stay on the failure track.

func (Result[T]) String

func (r Result[T]) String() string

String returns "Ok(<value>)" or "Err(<error>)".

func (Result[T]) Then

func (r Result[T]) Then[U any](f func(T) (U, error)) Result[U]

Then applies a Go-shaped step func(T) (U, error) to the value. strconv.Atoi, url.Parse, and your own functions work directly.

Example

Then takes a Go-shaped function func(T) (U, error), such as strconv.Atoi. Wrap adds context to the error, and Match folds the Result to one value.

package main

import (
	"fmt"
	"strconv"

	"github.com/zkrebbekx/monadz"
)

func main() {
	parse := func(s string) monadz.Result[int] {
		return monadz.Ok(s).
			Then(strconv.Atoi).
			Wrap("parse port")
	}

	for _, s := range []string{"8080", "http"} {
		msg := parse(s).Match(
			func(port int) string { return "port " + strconv.Itoa(port) },
			func(err error) string { return "error: " + err.Error() },
		)
		fmt.Println(msg)
	}
}
Output:
port 8080
error: parse port: strconv.Atoi: parsing "http": invalid syntax

func (Result[T]) Try

func (r Result[T]) Try() T

Try returns the value. On a failure, it stops the enclosing Do, and Do returns the error. See Do.

func (Result[T]) Validation

func (r Result[T]) Validation() Validation[T]

Validation converts the Result to a Validation, so that it can collect errors together with other Validations.

func (Result[T]) ValueOr

func (r Result[T]) ValueOr(fallback T) T

ValueOr returns the value, or fallback for a failure. (Rust: unwrap_or.)

func (Result[T]) ValueOrElse

func (r Result[T]) ValueOrElse(f func(error) T) T

ValueOrElse returns the value, or f(err). (Rust: unwrap_or_else.)

func (Result[T]) ValueOrZero

func (r Result[T]) ValueOrZero() T

ValueOrZero returns the value, or the zero value of T. (Rust: unwrap_or_default.)

func (Result[T]) Wrap

func (r Result[T]) Wrap(msg string) Result[T]

Wrap adds context to the error: "msg: <err>". The original error stays reachable with errors.Is and errors.As. (Rust anyhow: context.)

func (Result[T]) Wrapf

func (r Result[T]) Wrapf(format string, args ...any) Result[T]

Wrapf is Wrap with a format string.

func (Result[T]) ZipWith

func (r Result[T]) ZipWith[U, V any](other Result[U], f func(T, U) V) Result[V]

ZipWith combines both values with f if both are Ok, else the first failure. (Haskell: liftA2.)

type RetryPolicy

type RetryPolicy struct {
	// MaxAttempts is the total number of runs, including the first.
	// A value below 1 means 1.
	MaxAttempts int
	// Backoff returns the wait before attempt n+1, after attempt n failed
	// (n starts at 1). Nil means no wait.
	Backoff func(attempt int) time.Duration
	// RetryIf reports whether an error is worth a retry. Nil means every
	// error. Retry always stops when its own ctx ends, so a nil RetryIf
	// still retries a timeout from an inner Timeout.
	RetryIf func(error) bool
}

RetryPolicy says when and how often Retry runs a Task again.

type State

type State[S, A any] struct {
	// contains filtered or unexported fields
}

State is a step that reads a state S and returns a value A and a new state. Nothing runs until Run. (Haskell: State s a.)

Build a large computation from small steps with AndThen, then run it against any start state. If the steps are pure, the run is a pure function, so you can replay it in a test.

The zero State has no step. Run on a zero State panics; use NewState.

func GetState

func GetState[S any]() State[S, S]

GetState returns a step whose value is the current state. (Haskell: get.)

func GetStateWith

func GetStateWith[S, A any](f func(S) A) State[S, A]

GetStateWith returns a step whose value is f(state). (Haskell: gets.)

func ModifyState

func ModifyState[S any](f func(S) S) State[S, Unit]

ModifyState returns a step that applies f to the state. (Haskell: modify.)

func NewState

func NewState[S, A any](f func(S) (A, S)) State[S, A]

NewState wraps a state function.

func PutState

func PutState[S any](s S) State[S, Unit]

PutState returns a step that replaces the state. (Haskell: put.)

func StateOf

func StateOf[S, A any](a A) State[S, A]

StateOf returns a step that leaves the state alone and returns a. (Haskell: pure.)

func (State[S, A]) And

func (m State[S, A]) And[B any](next State[S, B]) State[S, B]

And runs this step and then next, and keeps the value of next. (Haskell: >>.)

func (State[S, A]) AndThen

func (m State[S, A]) AndThen[B any](f func(A) State[S, B]) State[S, B]

AndThen runs this step, passes its value to f, and runs the step that f returns with the new state. This is the monad bind. (Haskell: >>=.)

Example

AndThen chains State steps. The next step can use the value of the step before it.

package main

import (
	"fmt"

	"github.com/zkrebbekx/monadz"
)

func main() {
	next := monadz.NewState(func(n int) (int, int) { return n, n + 1 })

	pair := next.AndThen(func(a int) monadz.State[int, string] {
		return next.Map(func(b int) string { return fmt.Sprint(a, "-", b) })
	})

	id, counter := pair.Run(41)
	fmt.Println(id, counter)
}
Output:
41-42 43

func (State[S, A]) Eval

func (m State[S, A]) Eval(s S) A

Eval runs the step and returns only the value. (Haskell: evalState.)

func (State[S, A]) Exec

func (m State[S, A]) Exec(s S) S

Exec runs the step and returns only the final state. (Haskell: execState.)

func (State[S, A]) Map

func (m State[S, A]) Map[B any](f func(A) B) State[S, B]

Map applies f to the value.

func (State[S, A]) Run

func (m State[S, A]) Run(s S) (A, S)

Run runs the step and returns the value and the final state. Run panics on a zero State.

type Task

type Task[T any] struct {
	// contains filtered or unexported fields
}

Task is a description of work that can fail and that takes a context. Nothing runs until you call Run or Start. You can run a Task many times; each run does the work again. That makes a Task the right input for retry, timeout, and concurrency combinators.

Equivalents: Haskell ReaderT Context (ExceptT error IO) a, Rust async fn, Scala ZIO.

Panics: Run does not recover panics, so a bug crashes in the caller's goroutine. Start, ZipWith, and the functions in package tasks run a Task in another goroutine. There, monadz captures a panic and raises it again in the goroutine that waits for the result. The message holds the original stack trace.

func NewTask

func NewTask[T any](f func(ctx context.Context) (T, error)) Task[T]

NewTask returns a Task that calls f.

func NewTaskResult

func NewTaskResult[T any](f func(ctx context.Context) Result[T]) Task[T]

NewTaskResult returns a Task that calls f, which returns a Result.

func TaskErr

func TaskErr[T any](err error) Task[T]

TaskErr returns a Task that fails with err.

func TaskOk

func TaskOk[T any](v T) Task[T]

TaskOk returns a Task that succeeds with v.

func (Task[T]) AndThen

func (t Task[T]) AndThen[U any](f func(T) Task[U]) Task[U]

AndThen runs the Task that f returns. This is the monad bind.

func (Task[T]) Delay

func (t Task[T]) Delay(d time.Duration) Task[T]

Delay waits for d, then runs the Task. The wait stops if ctx ends.

func (Task[T]) Ensure

func (t Task[T]) Ensure(pred func(T) bool, err error) Task[T]

Ensure fails the Task with err if its value does not satisfy pred.

func (Task[T]) Inspect

func (t Task[T]) Inspect(f func(T)) Task[T]

Inspect calls f with the value after a success.

func (Task[T]) InspectErr

func (t Task[T]) InspectErr(f func(error)) Task[T]

InspectErr calls f with the error after a failure.

func (Task[T]) Map

func (t Task[T]) Map[U any](f func(T) U) Task[U]

Map applies f to the value.

func (Task[T]) MapErr

func (t Task[T]) MapErr(f func(error) error) Task[T]

MapErr applies f to the error.

func (Task[T]) Once

func (t Task[T]) Once() Task[T]

Once returns a Task that runs the work one time and then returns the cached result. Concurrent runs wait for the first one. A caller that waits stops waiting when its own context ends.

A failure with a context error (context.Canceled or context.DeadlineExceeded, also from an inner Timeout) is not cached, so the next run tries again. A panic is cached: every later run panics with the same value, as sync.OnceValue does.

func (Task[T]) OrElse

func (t Task[T]) OrElse(f func(error) Task[T]) Task[T]

OrElse runs the Task that f returns after a failure. Use it for a fallback source.

func (Task[T]) Recover

func (t Task[T]) Recover(f func(error) (T, error)) Task[T]

Recover handles a failure with a Go-shaped function.

func (Task[T]) Retry

func (t Task[T]) Retry(p RetryPolicy) Task[T]

Retry runs the Task again after a failure, as the policy allows. The waits stop when ctx ends. The final error tells how many attempts ran.

Example

A Task does nothing until Run, so Retry can run it again. A nil RetryIf retries every error.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/zkrebbekx/monadz"
)

func main() {
	attempts := 0
	flaky := monadz.NewTask(func(ctx context.Context) (string, error) {
		attempts++
		if attempts < 3 {
			return "", errors.New("unavailable")
		}
		return "charged", nil
	})

	res := flaky.Retry(monadz.RetryPolicy{MaxAttempts: 4}).Run(context.Background())
	fmt.Println(res, attempts)
}
Output:
Ok(charged) 3

func (Task[T]) Run

func (t Task[T]) Run(ctx context.Context) Result[T]

Run runs the Task in the caller's goroutine. If ctx already ended, Run returns ctx.Err() and does not start the work.

func (Task[T]) Start

func (t Task[T]) Start(ctx context.Context) *Future[T]

Start runs the Task in a new goroutine and returns a Future for the result. Future.Cancel cancels the context that the Task receives.

func (Task[T]) Then

func (t Task[T]) Then[U any](f func(context.Context, T) (U, error)) Task[U]

Then runs a Go-shaped step func(ctx, T) (U, error) after the Task.

func (Task[T]) Timeout

func (t Task[T]) Timeout(d time.Duration) Task[T]

Timeout fails the Task with context.DeadlineExceeded if it does not finish within d. The Task's context is canceled at the deadline.

Timeout returns at the deadline and does NOT wait for the work to stop, even if the work observes its context. Code in the work that runs after the deadline (cleanup, a late write) runs at the same time as the caller. This is the one exception to the structured-concurrency rule of package tasks. If the work panics after the deadline, the panic crashes the program, as an unrecovered goroutine panic does.

func (Task[T]) Wrap

func (t Task[T]) Wrap(msg string) Task[T]

Wrap adds context to the error: "msg: <err>".

func (Task[T]) Wrapf

func (t Task[T]) Wrapf(format string, args ...any) Task[T]

Wrapf is Wrap with a format string.

func (Task[T]) ZipWith

func (t Task[T]) ZipWith[U, V any](other Task[U], f func(T, U) V) Task[V]

ZipWith runs both Tasks at the same time and combines the values with f. If one fails, ZipWith cancels the other, waits for it to stop, and returns the first failure. For a Pair, use tasks.Zip.

type Triple

type Triple[A, B, C any] struct {
	First  A
	Second B
	Third  C
}

Triple holds three values.

func TripleOf

func TripleOf[A, B, C any](a A, b B, c C) Triple[A, B, C]

TripleOf returns a Triple.

func (Triple[A, B, C]) String

func (t Triple[A, B, C]) String() string

String formats the Triple as "(a, b, c)".

func (Triple[A, B, C]) Unpack

func (t Triple[A, B, C]) Unpack() (A, B, C)

Unpack returns the three values.

type Unit

type Unit struct{}

Unit is the type with one value. Use it where a type parameter needs a type, but the value carries no data.

type Validation

type Validation[T any] struct {
	// contains filtered or unexported fields
}

Validation holds a value of type T (Valid) or one or more errors (Invalid).

Result stops at the first error. Validation collects ALL the errors, so a form can report every bad field in one response. Combine Validations with the Map2 ... Map6 functions in package validations, which run every check and join the errors. (Haskell: Data.Validation.)

AndThen is fail-fast, because the next check needs the value. Use the validations package for independent checks.

The zero value is Valid(zero T).

func Invalid

func Invalid[T any](errs ...error) Validation[T]

Invalid returns a Validation that holds errs. Invalid drops nil errors. If no error is left, the Validation holds ErrNilError.

func Valid

func Valid[T any](v T) Validation[T]

Valid returns a Validation that holds v.

func ValidIf

func ValidIf[T any](v T, ok bool, err error) Validation[T]

ValidIf returns Valid(v) if ok is true, else Invalid(err).

func (Validation[T]) AndThen

func (v Validation[T]) AndThen[U any](f func(T) Validation[U]) Validation[U]

AndThen applies f to the value and returns its Validation. It is fail-fast: f does not run for an Invalid value.

func (Validation[T]) Ensure

func (v Validation[T]) Ensure(pred func(T) bool, err error) Validation[T]

Ensure returns Invalid(err) if the Validation is Valid and its value does not satisfy pred. Ensure returns an Invalid Validation unchanged, and pred does not run.

func (Validation[T]) Err

func (v Validation[T]) Err() error

Err returns nil for a Valid value. If the Validation holds exactly one error, Err returns that error itself. Else Err returns the errors joined with errors.Join.

func (Validation[T]) Errors

func (v Validation[T]) Errors() []error

Errors returns a copy of the errors. It is empty for a Valid value.

func (Validation[T]) Expect

func (v Validation[T]) Expect(msg string) T

Expect returns the value, or panics with msg and Err(). (Rust: expect.)

func (Validation[T]) Field

func (v Validation[T]) Field(name string) Validation[T]

Field wraps every error in a *FieldError with the given field name, so an API can report which field failed.

func (Validation[T]) Get

func (v Validation[T]) Get() (T, error)

Get returns the value and nil, or the zero value and Err().

func (Validation[T]) Inspect

func (v Validation[T]) Inspect(f func(T)) Validation[T]

Inspect calls f with the value, if any, and returns the Validation unchanged.

func (Validation[T]) IsInvalid

func (v Validation[T]) IsInvalid() bool

IsInvalid reports whether the Validation holds errors.

func (Validation[T]) IsValid

func (v Validation[T]) IsValid() bool

IsValid reports whether the Validation holds a value.

func (Validation[T]) Map

func (v Validation[T]) Map[U any](f func(T) U) Validation[U]

Map applies f to the value.

func (Validation[T]) MapErr

func (v Validation[T]) MapErr(f func(error) error) Validation[T]

MapErr applies f to each error. If f returns nil, MapErr stores ErrNilError. f does not run for a Valid value.

func (Validation[T]) Match

func (v Validation[T]) Match[U any](onValid func(T) U, onInvalid func([]error) U) U

Match calls onValid with the value or onInvalid with the errors, and returns the result. (Haskell: validation, in package Data.Validation.)

func (Validation[T]) Must

func (v Validation[T]) Must() T

Must returns the value, or panics with Err(). (Rust: unwrap.)

func (Validation[T]) Result

func (v Validation[T]) Result() Result[T]

Result converts the Validation to a Result with the joined errors.

func (Validation[T]) String

func (v Validation[T]) String() string

String returns "Valid(<value>)" or "Invalid(<err>; <err>)".

func (Validation[T]) ValueOr

func (v Validation[T]) ValueOr(fallback T) T

ValueOr returns the value, or fallback for an Invalid value. (Rust: unwrap_or.)

func (Validation[T]) ValueOrElse

func (v Validation[T]) ValueOrElse(f func([]error) T) T

ValueOrElse returns the value, or f applied to the errors. f runs only for an Invalid value. (Rust: unwrap_or_else.)

func (Validation[T]) ValueOrZero

func (v Validation[T]) ValueOrZero() T

ValueOrZero returns the value, or the zero value of T. (Rust: unwrap_or_default.)

func (Validation[T]) ZipWith

func (v Validation[T]) ZipWith[U, V any](other Validation[U], f func(T, U) V) Validation[V]

ZipWith combines both values with f if both are Valid. Else ZipWith returns Invalid with the errors of both, in order. Unlike AndThen, it collects every error. (Haskell: liftA2 for Data.Validation.)

type Writer

type Writer[W, A any] struct {
	// contains filtered or unexported fields
}

Writer is a computation that returns a value A and appends to a log of W values, such as audit entries or metrics. (Haskell: Writer w a, with w = [W].)

The zero Writer has no value. Run on a zero Writer panics; use NewWriter.

func NewWriter

func NewWriter[W, A any](a A, w ...W) Writer[W, A]

NewWriter returns a Writer with the value a and the log entries w. NewWriter copies w, so a later change to the caller's slice does not change the log.

func Tell

func Tell[W any](w ...W) Writer[W, Unit]

Tell returns a Writer that only appends w to the log. (Haskell: tell.)

func (Writer[W, A]) AndThen

func (m Writer[W, A]) AndThen[B any](f func(A) Writer[W, B]) Writer[W, B]

AndThen passes the value to f, and joins the two logs in order. (Haskell: >>=.)

func (Writer[W, A]) Censor

func (m Writer[W, A]) Censor(f func([]W) []W) Writer[W, A]

Censor applies f to the log. f receives a copy, so it can change the slice in place. (Haskell: censor.)

func (Writer[W, A]) Map

func (m Writer[W, A]) Map[B any](f func(A) B) Writer[W, B]

Map applies f to the value. The log does not change.

func (Writer[W, A]) Run

func (m Writer[W, A]) Run() (A, []W)

Run returns the value and a copy of the full log. Run panics on a zero Writer.

Directories

Path Synopsis
Package eithers has functions that work on many monadz.Either values.
Package eithers has functions that work on many monadz.Either values.
Package options has functions that work on many monadz.Option values, or that cannot be methods because of Go's type rules.
Package options has functions that work on many monadz.Option values, or that cannot be methods because of Go's type rules.
playground
scenarios
Package scenarios holds the stories that the monadz playground shows.
Package scenarios holds the stories that the monadz playground shows.
wasm command
Command wasm is the bridge between the monadz playground page and the scenarios package.
Command wasm is the bridge between the monadz playground page and the scenarios package.
Package results has functions that work on many monadz.Result values, or that cannot be methods because of Go's type rules.
Package results has functions that work on many monadz.Result values, or that cannot be methods because of Go's type rules.
Package tasks runs many monadz.Task values at the same time.
Package tasks runs many monadz.Task values at the same time.
Package validations combines monadz.Validation values and collects every error, instead of stopping at the first one.
Package validations combines monadz.Validation values and collects every error, instead of stopping at the first one.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL