A Go module providing monadic structures for functional programming patterns. It offers type-safe containers for handling optionality, errors, state, dependencies, and computations in a composable manner.
Whilst Go is primarily an imperative language, certain patterns from functional programming can reduce boilerplate, improve type safety, and make complex control flow more explicit. This library provides battle-tested abstractions for developers who want these benefits without sacrificing Go's simplicity.
You will need the following things properly installed on your computer:
With Go module support (Go 1.11+), simply add the following import
import "github.com/tomasbasham/gofp"to your code, and then go [build|run|test] will automatically fetch the
necessary dependencies.
Otherwise, to install the gofp module, run the following command:
go get -u github.com/tomasbasham/gofpThis library embraces functional programming patterns whilst respecting Go's pragmatic nature. It's a tool, not a religion. Use it where it adds clarity and safety, and reach for standard Go patterns where they're clearer.
Monads provide:
- Explicit handling of edge cases - No more forgotten nil checks
- Composable operations - Chain transformations declaratively
- Type safety - The compiler ensures you handle all cases
- Reduced boilerplate - Especially for error handling pipelines
But remember: Go is not Haskell. This library works best when integrated thoughtfully into Go codebases, not when used to fight against the language's idioms.
To use this module, import the relevant packages into your Go code. The core
types (Option, Result, Either) are available directly from the main
package, whilst more specialised monads (Reader, State, Writer) are in
their own subpackages:
import (
"github.com/tomasbasham/gofp"
"github.com/tomasbasham/gofp/reader"
"github.com/tomasbasham/gofp/state"
"github.com/tomasbasham/gofp/writer"
)Each monad provides a consistent interface with Map and FlatMap methods for
transforming and composing computations. The library follows common functional
programming conventions, where Map transforms the contained value and
FlatMap (also called "bind" in other languages) chains operations that
themselves return monadic values.
Begin by identifying which monad fits your problem domain, then compose operations using the provided combinators. The examples below demonstrate typical usage patterns for each monad.
Represents an optional value that may or may not exist. Eliminates nil pointer errors and makes optionality explicit in your type signatures.
Mathematical form:
Option T = Some T | None
When to use:
- Database queries that may return no results
- Configuration values that might be missing
- Function parameters that are genuinely optional
- Parsing operations that might fail
Example:
import "github.com/tomasbasham/gofp"
func FindUser(id string) gofp.Option[User] {
user, found := db.Get(id)
if !found {
return gofp.None[User]()
}
return gofp.Some(user)
}
// Chain operations without nil checks
result := FindUser("123").
Map(func(u User) User {
u.LastAccessed = time.Now()
return u
}).
UnwrapOr(DefaultUser)Key functions:
Some(value)- Create an Option containing a valueNone[T]()- Create an empty OptionMap(fn)- Transform the contained valueFlatMap(fn)- Chain operations that return OptionsUnwrapOr(default)- Extract value with fallbackFilter(predicate)- Convert Some to None if predicate fails
Represents a computation that may succeed with a value or fail with an error. Provides structured error handling with stack traces and error wrapping.
Mathematical form:
Result T = Ok T | Err error
When to use:
- Operations that can fail (file I/O, network calls, parsing)
- Validation pipelines
- Replacing multiple
if err != nilchecks - Building error contexts
Example:
func ProcessData(filename string) gofp.Result[Data] {
return ReadFile(filename).
FlatMap(ParseJSON).
FlatMap(ValidateSchema).
Map(Transform).
Wrap("failed to process data")
}
func ReadFile(path string) gofp.Result[[]byte] {
data, err := os.ReadFile(path)
return gofp.FromReturn(data, err)
}
// Use the result
result := ProcessData("config.json")
if result.IsErr() {
log.Printf("Error: %v\n%s", result.UnwrapErr(), result.StackTrace())
return
}
data := result.Unwrap()Key functions:
Ok(value)- Create a successful ResultErr[T](error)- Create a failed ResultFromReturn(value, err)- Convert Go's(T, error)patternMap(fn)- Transform success valuesFlatMap(fn)- Chain fallible operationsWrap(msg)- Add error contextEnsure(err, predicate)- Validate and fail if predicate is falseRecover(fn)- Convert errors to values
Represents a value that can be one of two types. By convention, Left represents failure and Right represents success, but both can hold any type.
Mathematical form:
Either T U = Left T | Right U
When to use:
- Multiple error types that need different handling
- Accumulating validation errors
- Representing mutually exclusive outcomes
- When Result's error type is too restrictive
Example:
type ValidationError struct {
Field string
Message string
}
func ValidateAge(age int) gofp.Either[ValidationError, int] {
if age < 0 {
return gofp.Left[ValidationError, int](ValidationError{
Field: "age",
Message: "must be non-negative",
})
}
return gofp.Right[ValidationError](age)
}
// Accumulate multiple validation errors
func ValidateUser(user User) gofp.Either[[]ValidationError, User] {
validations := []gofp.Either[ValidationError, gofp.Unit]{
ValidateAge(user.Age).Map(func(int) gofp.Unit {
return gofp.UnitValue
}),
ValidateEmail(user.Email).Map(func(string) gofp.Unit {
return gofp.UnitValue
}),
}
// Use EitherSequence to collect all errors or succeed
// Implementation depends on your error handling strategy
}Key functions:
Left[T, U](value)- Create a Left valueRight[T, U](value)- Create a Right valueFromResult(result)- Convert Result to EitherMap(fn)- Transform Right valuesMapLeft(fn)- Transform Left valuesFlatMap(fn)- Chain Either-returning operationsSwap()- Exchange Left and RightEitherFold(leftFn, rightFn)- Handle both cases
Represents a computation that reads from a shared environment. Provides dependency injection without explicit parameter passing.
Mathematical form:
Reader E A = E -> A
When to use:
- Dependency injection
- Configuration that flows through many functions
- Testing with different environments
- Avoiding global state
Example:
import "github.com/tomasbasham/gofp/reader"
type Config struct {
Database string
APIKey string
Debug bool
}
func GetConnection() reader.Reader[Config, *sql.DB] {
return reader.Map(
reader.Ask[Config](),
func(cfg Config) *sql.DB {
db, _ := sql.Open("postgres", cfg.Database)
return db
},
)
}
func FetchUsers() reader.Reader[Config, []User] {
return reader.FlatMap(
GetConnection(),
func(db *sql.DB) reader.Reader[Config, []User] {
return reader.Pure[Config](queryUsers(db))
},
)
}
// Execute with configuration
config := Config{Database: "postgres://...", Debug: true}
users := FetchUsers().Run(config)Key functions:
Pure[E, A](value)- Lift value into ReaderAsk[E]()- Access the environmentMap(fn)- Transform the resultFlatMap(fn)- Chain Reader operationsLocal(reader, fn)- Temporarily modify environment
Represents a computation that maintains and transforms state. Provides pure functional state management without mutable variables.
Mathematical form:
State S A = S -> (A, S)
When to use:
- Parser combinators
- State machines
- Game loops
- Any computation requiring sequential state updates
Example:
import "github.com/tomasbasham/gofp/state"
type GameState struct {
Score int
Lives int
Level int
}
func AddPoints(points int) state.State[GameState, gofp.Unit] {
return state.Modify(func(s GameState) GameState {
s.Score += points
return s
})
}
func LoseLife() state.State[GameState, bool] {
return state.FlatMap(
state.Modify(func(s GameState) GameState {
s.Lives--
return s
}),
func(_ gofp.Unit) state.State[GameState, bool] {
return state.Gets(func(s GameState) bool {
return s.Lives > 0
})
},
)
}
// Compose state operations
gameLoop := AddPoints(100).
FlatMap(func(_ gofp.Unit) state.State[GameState, bool] {
return LoseLife()
})
initialState := GameState{Score: 0, Lives: 3, Level: 1}
stillAlive, finalState := gameLoop.Run(initialState)Key functions:
Pure[S, A](value)- Lift value without changing stateGet[S]()- Access current stateGets(fn)- Extract value from statePut(state)- Replace stateModify(fn)- Transform stateMap(fn)- Transform the resultFlatMap(fn)- Chain state operations
Represents a computation that accumulates output (logs, events, metrics) alongside producing a value. Requires a Monoid instance for combining outputs.
Mathematical form:
Writer W A = () -> (A, W) where W is a Monoid
When to use:
- Collecting logs during computation
- Audit trails
- Gathering metrics
- Accumulating warnings
Example:
import "github.com/tomasbasham/gofp/writer"
// Define a Monoid for combining string slices.
type SliceMonoid[T any] struct{}
func (SliceMonoid[T]) Empty() []T {
return []T{}
}
func (SliceMonoid[T]) Append(a, b []T) []T {
return append(a, b...)
}
func ProcessItem(item string) writer.Writer[[]string, int] {
return writer.TellWithValue(
len(item),
[]string{fmt.Sprintf("processed: %s", item)},
SliceMonoid[string]{},
)
}
func ProcessBatch(items []string) writer.Writer[[]string, int] {
total := writer.Pure(0, SliceMonoid[string]{})
for _, item := range items {
total = writer.FlatMap(total, func(s int) writer.Writer[[]string, int] {
return writer.Map(ProcessItem(item), func(length int) int {
return s + length
})
})
}
return total
}
result, logs := ProcessBatch([]string{"hello", "world"}).Run()
// result = 10
// logs = ["processed: hello", "processed: world"]Key functions:
Pure(value, monoid)- Create Writer without outputTell(output, monoid)- Create Writer with only outputTellWithValue(value, output, monoid)- Create Writer with bothMap(fn)- Transform the valueFlatMap(fn)- Chain Writer operationsListen(writer)- Include output in the value
A Monoid is a mathematical structure that defines how to combine values of the same type. It consists of:
- An identity element (empty) - A value that, when combined with any other value, returns that value unchanged
- An associative binary operation (append) - A way to combine two values
that satisfies:
append(append(a, b), c) = append(a, append(b, c))
In this module, Monoids are represented by an interface:
type Monoid[A any] interface {
Empty() A
Append(a, b A) A
}The Writer monad uses Monoids to combine outputs from multiple computations. For example, if you're accumulating log messages (strings), you'd use a string concatenation Monoid. If you're collecting events (slices), you'd use a slice concatenation Monoid.
All monads provide Sequence functions to transform slices of monadic values:
// Options: returns None if any element is None
options := []gofp.Option[int]{gofp.Some(1), gofp.Some(2), gofp.Some(3)}
result := gofp.OptionSequence(options) // Some([]int{1, 2, 3})
// Results: returns Err if any operation fails
results := []gofp.Result[int]{gofp.Ok(1), gofp.Ok(2), gofp.Ok(3)}
combined := gofp.ResultSequence(results) // Ok([]int{1, 2, 3})
// Eithers: returns Left if any element is Left
eithers := []gofp.Either[string, int]{
gofp.Right[string](1),
gofp.Right[string](2),
}
sequenced := gofp.EitherSequence(eithers) // Right([]int{1, 2})Extract values by handling both success and failure cases:
// Option
value := gofp.OptionFold(
maybeUser,
func() string { return "No user found" },
func(u User) string { return u.Name },
)
// Result
message := gofp.ResultFold(
operation,
func(err error) string { return fmt.Sprintf("Error: %v", err) },
func(val int) string { return fmt.Sprintf("Success: %d", val) },
)
// Either
output := gofp.EitherFold(
validation,
func(err ValidationError) string { return err.Message },
func(data Data) string { return "Valid" },
)Use Apply functions to combine multiple monadic values:
// Combine two Options
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
opt1 := gofp.Some(5)
opt2 := gofp.Some(3)
optFn := gofp.Some(add)
// This is typically done with curried functions or helper combinators
result := gofp.OptionApply(opt1, gofp.OptionMap(opt2, add))Functional programming patterns aren't always the right choice for Go projects. Consider avoiding this library when:
- Your team is unfamiliar with functional concepts - The learning curve can slow development and reduce code maintainability if the team doesn't understand monads.
- Simple error handling suffices - For straightforward operations, Go's
standard
if err != nilpattern is clearer and more idiomatic. - Performance is critical - Monadic composition introduces additional function calls and allocations. Benchmark first if you're in a hot path.
- You're writing library code for the Go community - Most Go developers expect idiomatic Go patterns. Using monads in public APIs creates friction.
- The problem domain is simple - Don't add abstraction layers when direct, imperative code would be clearer.
- You can't justify the abstraction - If you find yourself wrapping and unwrapping frequently, or if the monadic code is harder to read than imperative code, reconsider.
- Complex error handling pipelines with multiple failure points
- Configuration-heavy applications where Reader monad reduces parameter passing
- Parser combinators where State monad shines
- Validation logic that accumulates errors
- Code that benefits from explicit optionality beyond nil checks
// DON'T: Use Result for simple operations
func Add(a, b int) gofp.Result[int] {
return gofp.Ok(a + b) // Unnecessary wrapper
}
// DO: Use Result when operations can genuinely fail
func Divide(a, b int) gofp.Result[int] {
if b == 0 {
return gofp.Err[int](errors.New("division by zero"))
}
return gofp.Ok(a / b)
}
// DON'T: Wrap every nullable value in Option
func GetName(user *User) gofp.Option[string] {
if user == nil {
return gofp.None[string]()
}
return gofp.Some(user.Name) // Just check for nil normally
}
// DO: Use Option when optionality is semantically meaningful
func GetMiddleName(user User) gofp.Option[string] {
// Middle name is genuinely optional in the domain
if user.MiddleName == "" {
return gofp.None[string]()
}
return gofp.Some(user.MiddleName)
}This project is licensed under the MIT License.