Library / SDK
IBM/fp-go avatar
IBM/fp-go

fp-go: typed functional programming for Go, from IBM

Functional programming library for Go 1.24+, inspired by fp-ts. Uses generic type aliases for a clean, composable API. Provides Option, Either, Result, IO, IOResult, Reader, and ReaderIOResult monads, plus optics (Lens, Prism, Traversal) for immutable data manipulation. Supports Functor, Applicative, and Monad abstractions with do-notation-style

2,031 stars87 forksGoApache-2.0

At a glance

What is it?
fp-go is an IBM-maintained library that brings fp-ts-style functional programming to Go generics. It provides Option, Either, IO, ReaderIOEither and eight other types with composable operations, but the root module requires only Go 1.20 while the v2 branch needs Go 1.24, and a work-in-progress banner remains on the README.
Who is it for?
fp-go fits a narrow audience: Go engineers who know FP abstractions from fp-ts or Haskell and want those same patterns in Go. Teams without that background will pay a steep learning curve for data types that conflict with every existing Go library returning (T, error) pairs.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Eight typed containers and a single consistent operation vocabulary

fp-go is a direct port of the fp-ts mental model into Go generics. The library supplies 8 primary types: Option[A] for values that may be absent, Either[E, A] for computations that can fail with a typed error, IO[A] for lazy effects that always succeed, IOEither[E, A] for lazy effects that can fail, Reader[R, A] for computations that depend on an injected environment, ReaderIOEither[R, E, A] for the full combination of those three concerns, Task[A] for asynchronous computations, and State[S, A] for stateful computations.

Across all 8, 5 operation names are consistent: Map transforms the value inside a context, Chain (FlatMap) flattens nested contexts, Ap applies a function-in-context to a value-in-context, Of lifts a plain value into a context, and Fold extracts a value from a context. A developer who learns how Map works on Option will find the same name and the same semantics on Either, IO and Reader. That naming contract is one of the design goals the repository states explicitly.

The library also ships 3 optics types: Lens, Prism and Traversal for immutable data manipulation. These are separate from the monad stack and cover the structural access case, where you want to read or update a nested field without copying entire data structures by hand.

Root go.mod declares 1.20; the v2 branch requires 1.24

The project description calls this a library for Go 1.24 or newer, but the go.mod at the repository root says `go 1.20`. The two claims refer to different versions of the library. The homepage links to the v2 subdirectory at `https://github.com/IBM/fp-go/tree/main/v2`, and the v2 README states that v2 requires Go 1.24 or newer. The root module is the v1 line, and its go.mod sets the minimum at 1.20.

A project below Go 1.24 can install v1 but cannot use v2. A project on Go 1.24 or newer can pick either line. The repository holds both in the same tree, which makes the initial read confusing for anyone who arrives at the top-level README and assumes a single version requirement. If you plan to use v2, verify the Go toolchain version before adding the dependency.

go get and a first Either operation

Adding the library to a module takes one command:

bash
go get github.com/IBM/fp-go

The quick example in the README divides two integers. Passing zero as the divisor returns a Left, which carries the error. Any other divisor returns a Right with the result. After the definition, the code pipes the result through Map to double it and through GetOrElse to extract it with a fallback value:

go
import (
    "errors"
    "github.com/IBM/fp-go/either"
    "github.com/IBM/fp-go/function"
)

func divide(a, b int) either.Either[error, int] {
    if b == 0 {
        return either.Left[int](errors.New("division by zero"))
    }
    return either.Right[error](a / b)
}

result := function.Pipe2(
    divide(10, 2),
    either.Map(func(x int) int { return x * 2 }),
    either.GetOrElse(func() int { return 0 }),
)
// result = 10

function.Pipe2 takes an initial value and two transformation functions and threads the value through them in order. Each data type lives in its own package: `github.com/IBM/fp-go/either` and `github.com/IBM/fp-go/function` are two examples. Every top-level directory in the repository corresponds to one package and one data type, so the import path always mirrors the type name.

EitherizeXXX and UneitherizeXXX translate between idiomatic and functional Go

Most existing Go code returns (T, error) rather than Either[error, T]. fp-go provides two families of conversion functions to bridge that gap. `either.EitherizeXXX` converts an idiomatic Go function returning (T, error) into a function returning Either[error, T]. In the opposite direction, `either.UneitherizeXXX` converts an Either-returning function back into idiomatic multi-return form.

For functions that perform IO and can fail, the README gives `ioeither.EitherizeXXX` instead. The readFile example in the repository shows this approach:

go
func readFile(path string) ioeither.IOEither[error, []byte] {
    return func() either.Either[error, []byte] {
        data, err := os.ReadFile(path)
        if err != nil {
            return either.Left[[]byte](err)
        }
        return either.Right[error](data)
    }
}

This converts `os.ReadFile` into an IOEither, making it composable with other functional operations. The XXX suffix in the conversion function names stands for arity: separate versions exist for different numbers of arguments, and the specific function name encodes the parameter count. The API documentation at pkg.go.dev/github.com/IBM/fp-go lists the available arities for each conversion family.

IO captures side effects as deferred closures

The IO type is a zero-argument function. When you write `func getCurrentTime() io.IO[time.Time]` and return a closure that calls `time.Now()`, the call to `time.Now()` is deferred until someone invokes the IO value. Nothing executes at the point of construction.

This matters for composition. You can build a chain of IO operations, pass them around, and run the whole chain exactly once at the program boundary. The README describes this as isolating side effects into lazily executed functions. The design goal is that the body of your business logic stays pure, and only the outermost layer actually executes the IO.

IOEither extends that pattern to operations that can fail. A file read, a network call, or a database query becomes an IOEither[error, T] rather than a raw (T, error) return. Once wrapped, it participates in the same Map and Chain operations as the rest of the type hierarchy. The cost is that any library function not already wrapped in fp-go terms requires a manual wrapping step before it enters the functional pipeline, and the standard library provides no wrapped equivalents.

ReaderIOEither threads environment, failure and effects through one type

ReaderIOEither[R, E, A] is the library's highest-arity composite type. It combines Reader, which takes an environment R and returns a computation, with IO for lazy execution, and Either, which can fail with error type E and succeed with value A. The README lists it as the type for effectful computations with dependencies.

In practice this type appears when a service function needs access to a configuration object or a database handle (the Reader part), performs an IO side effect, and can fail at runtime. Instead of threading these three concerns through separate parameters, fp-go packages them into one type with a single consistent operation set.

Using ReaderIOEither across a codebase means every function that combines any of those three properties gets the same container. That uniformity simplifies composition but makes the type signatures harder to read for engineers not already familiar with the pattern. The samples/readerioeither/ directory in the repository contains runnable examples that show the type in use.

The work-in-progress note and its API stability implications

The README carries a prominent work-in-progress note. It states that the library is at major version 1 due to semantic-release limitations and commits to minimizing breaking changes. The repository's own tags are already at v2, however, so the note's claim of being at major version 1 no longer matches the version numbering. The note has not been updated.

The practical weight of this is the commitment to minimize, not eliminate, breaking changes. If your code depends on specific function signatures, review the API documentation before upgrading between minor versions.

The Zen of Go alignment table in the README covers 11 principles. Nine are marked as aligned, one is marked partial (the goroutine lifecycle rule), and one is yellow (test coverage, described as growing). That last entry means the test suite does not yet lock in the full behavior of the library. That assessment comes from the README's own table, not from an outside evaluation.

Where fp-go is the wrong choice for a Go team

fp-go is not a drop-in addition to an existing Go codebase. The Either type replaces (T, error) returns across the boundary, and every function from the standard library or a third-party package that returns idiomatic errors needs wrapping before it enters the functional pipeline. For a codebase with many calls to functions like os.ReadFile or http.Get, that wrapping cost is real and ongoing.

The idiom also diverges from what Go developers typically write. Code reviews, onboarding and pair programming all become more expensive when part of the team has never worked with monads. The README's own comparison table shows idiomatic Go and functional Go side by side as two different styles, not as one replacing the other, which is an honest description of the trade-off.

For greenfield services where the whole team comes from a functional programming background and is already comfortable with fp-ts or similar abstractions, the uniformity of the operation set across eight types pays off. For Go shops that chose Go for its direct error handling and straightforward concurrency primitives, the overhead of wrapping the standard library into fp-go terms is unlikely to be worth it.

Editorial conclusion

fp-go fits a narrow audience: Go engineers who know FP abstractions from fp-ts or Haskell and want those same patterns in Go. Teams without that background will pay a steep learning curve for data types that conflict with every existing Go library returning (T, error) pairs. The EitherizeXXX helpers ease the gap but do not remove the conceptual shift. Before adopting, confirm whether v1 or v2 fits your Go version (root go.mod sets 1.20; v2 needs 1.24), take the work in progress banner in the README at face value, and run the samples/ directory to check whether the composition style is something your team can sustain.

Frequently asked questions

What is fp-go?

fp-go is a Go library from IBM that brings functional programming data types and operations from fp-ts into Go generics. It provides Option, Either, IO, IOEither, Reader, ReaderIOEither, Task and State types along with optics for immutable data manipulation.

What Go version does fp-go require?

The root module's go.mod declares go 1.20. The v2 branch, linked from the repository homepage, requires Go 1.24 or newer.

How do I convert an idiomatic Go function to use fp-go Either?

The library provides either.EitherizeXXX functions, where XXX encodes the arity, to wrap a function returning (T, error) into one returning Either[error, T]. For IO operations that can fail, ioeither.EitherizeXXX does the same job.

What is the difference between fp-go IO and Task?

IO[A] is a lazy synchronous computation. Task[A] is an asynchronous computation. Both are part of the type hierarchy and share Map, Chain and Ap operations, but Task is for async contexts and supports cancellation via context.

What dependencies does fp-go add to a Go module?

The root go.mod lists two direct dependencies: github.com/stretchr/testify at v1.12.1 for testing, and github.com/urfave/cli/v2 at v2.27.7 for CLI utilities.

Official sources

  1. IBM/fp-go on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ibm-fp-go.svg)](https://hysenlabs.com/projects/ibm-fp-go)