Official agent skill

fp-go Functional Programming for Go

by IBM in IBM/fp-go

Entry point for writing and reviewing Go code with the fp-go v2 library: core monad types, data-last composition, type parameter order and import conventions.

OfficialApache-2.0Auto-check passedDevelopment

Install fp-go Functional Programming for Go

skills CLI
$ npx skills add IBM/fp-go --skill fp-go -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install IBM/fp-go fp-go --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/IBM/fp-go.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/fp-go .claude/skills/fp-go && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
fp-go
GitHub stars
2k
Token cost
~8.5k tokens
SKILL.md length
2,692 words
Files
1
Skills in repo
10
Repo updated
First seen
Licence
Apache-2.0

At a glance

Entry point for writing and reviewing Go code with the fp-go v2 library: core monad types, data-last composition, type parameter order and import conventions.

  • Works in 7 steps: Import path is v2: always… → Data-last: all operations return a… → Non-inferrable type parameters come… → …
  • Writing new Go code with fp-go's Option, Result or IOResult types
  • SKILL.md covers Critical Rules for Code…, Canonical Import Aliases, Generation Workflow and Overview, plus 9 more sections
  • Calls go

What it does

Main skill for IBM's fp-go library at its v2 import path. It sets critical rules for generated code: always use the v2 path, write operations data-last so each call returns a function waiting for the data, and keep type parameters the compiler cannot infer first so you annotate only that prefix, as in option.Ap[int](fa).

It prefers Result over Either when the error type is Go's error, and explains that IO values are lazy functions you must call, with an IOResult or ReaderIOResult producing one Result that result.Unwrap turns into the usual value and error pair. Other topics are lifting Go functions with Eitherize, do-notation, canonical import aliases and choosing the right monad, with point-free composition through Flow and Pipe as the encouraged style. Focused subjects are passed to sibling skills for pipes, lenses, effects, context, HTTP, logging, pattern matching and PR review.

When your agent uses it

  • Writing new Go code with fp-go's Option, Result or IOResult types
  • Converting idiomatic Go error handling into functional pipelines
  • Reviewing or refactoring Go code that already uses fp-go
  • Choosing which fp-go monad fits a given function

Example prompts

  • “Rewrite this function that returns (User, error) as an IOResult pipeline with fp-go.”
  • “Which fp-go type fits a lookup that can fail and needs a context.Context?”
  • “Fix the compile error from assigning pipeline(ctx)() to value, err.”

Requirements

  • Go 1.24 or later with the github.com/IBM/fp-go/v2 module

Workflow steps

7 steps, taken from the first numbered list in SKILL.md.

  1. Import path is v2: always github.com/IBM/fp-go/v2/..., never github.com/IBM/fp-go/... (that is v1).
  2. Data-last: all operations return a function waiting for data. Write option.Map(f)(value), never option.Map(value, f).
  3. Non-inferrable type parameters come first. Whatever the compiler cannot recover from the arguments is declared first, so you can annotate…
  4. Prefer Result over Either when the error type is Go's error. Result[A] is Either[error, A]. Same for ioresult over ioeither…
  5. IO values are lazy: IO[A] is func() A. They describe a computation — you must call () to execute. Don't forget the trailing (). Running an…
  6. Point-free style: compose with F.Flow and F.Pipe instead of writing inline anonymous functions. If a transformation can be expressed as a…
  7. Canonical import aliases: use the aliases in the table below in every file, so the same letter never means two packages.

What it can do on your machine

Read from SKILL.md and the folder at commit 1c4245d. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • go

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

fp-go Functional Programming for Go loads about 8.5k tokens when it runs. Until then it costs about 204 tokens; SKILL.md has 2,692 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~204
When it runs · the whole SKILL.md, loaded when a task matches
~8.5k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from IBM/fp-go at commit 1c4245d, republished under its Apache-2.0 licence (© IBM). 2,692 words, ~8,514 tokens.

Download SKILL.mdSave it as .claude/skills/fp-go/SKILL.md (or your agent's skills folder).
name
fp-go
description
Use this skill whenever writing, reviewing, or refactoring Go code that uses the fp-go library (github.com/IBM/fp-go/v2). It is the entry point: core types (Option, Either, Result, IO, IOResult, ReaderIOResult, Effect), data-last composition, type-parameter order, lifting Go functions with Eitherize, do-notation, the canonical import aliases, and choosing the right monad. Trigger on any mention of fp-go, functional programming in Go, monads in Go, Option/Either/Result types in Go, IOResult, ReaderIOResult, Effect, or converting idiomatic Go error handling into functional pipelines. For focused topics also load fp-go-pipe-flow (Pipe/Flow, reader, do-notation), fp-go-lens (optics), fp-go-effect (typed dependencies), fp-go-context, fp-go-http, fp-go-logging, fp-go-pattern-matching or fp-go-pr-review.

fp-go v2 — Functional Programming for Go

Critical Rules for Code Generation

  1. Import path is v2: always github.com/IBM/fp-go/v2/..., never github.com/IBM/fp-go/... (that is v1).
  2. Data-last: all operations return a function waiting for data. Write option.Map(f)(value), never option.Map(value, f).
  3. Non-inferrable type parameters come first. Whatever the compiler cannot recover from the arguments is declared first, so you can annotate just that prefix. Concretely:
    • Map[A, B](f func(A) B) and Chain[A, B](f Kleisli[A, B]) — both params are inferable from f; write option.Map(f) with no annotation.
    • Ap[B, A](fa M[A]) — B is not recoverable from fa, so it leads: option.Ap[int](fa).
    • In either/reader/readerio*, the error or environment type leads: either.Map[E, A, B], reader.Map[R, A, B]. Annotate only that head: either.Map[error](f), reader.Map[context.Context](f).
    • Result, Option, IOResult and the other error-specialized monads have no leading param, which is one more reason to prefer them.
  4. Prefer Result over Either when the error type is Go's error. Result[A] is Either[error, A]. Same for ioresult over ioeither, readerioresult over readerioeither.
  5. IO values are lazy: IO[A] is func() A. They describe a computation — you must call () to execute. Don't forget the trailing (). Running an IOResult[A] or ReaderIOResult[A] produces one value, a Result[A] — not a (A, error) pair. Use result.Unwrap to reach idiomatic Go:
    go
    res := pipeline(ctx)()             // Result[A] — a single value
    value, err := result.Unwrap(res)   // (A, error)
    value, err := pipeline(ctx)() is a compile error. Alternatively, stay functional and eliminate the Result with result.Fold, or use one of the idiomatic/ packages, whose types are (A, error) tuples end to end.
  6. Point-free style: compose with F.Flow and F.Pipe instead of writing inline anonymous functions. If a transformation can be expressed as a composition of named functions, it should be. Point-free pipelines are idiomatic fp-go.
    • Lambdas belong only at the leaves: a struct field accessor (or better, lens.Get), a lens setter passed to MakeLens, a multi-field formatter, or the one function that touches raw IO / ctx.Done(). Everything composed on top of the leaves — every Map, Chain, Bind, Filter, Fold argument — is a named function, a combinator (N.MoreThan, S.Format, F.Constant1, F.Bind2nd, …) or an F.FlowN of those.
    • Prefer lifting over wrapping: R.Eitherize1(strconv.Atoi), RIO.Eitherize1(repo.FindUser), EF.Eitherize1(queryUser) instead of hand-written closures.
    • An argument that is only used to feed a pipeline is a sign the function should return the pipeline instead: func fetchAll() RIO.Kleisli[[]int, []User] { return RIO.TraverseArray(fetchUser) }, not func fetchAll(ids []int) … { return RIO.TraverseArray(fetchUser)(ids) }.
  7. Canonical import aliases: use the aliases in the table below in every file, so the same letter never means two packages.

Canonical Import Aliases

All fp-go skills use these aliases. Packages not listed are imported unaliased (prism, logging, readerresult, …), and so is the standard library (net/http stays http).

AliasPackageAliasPackage
FfunctionRIOcontext/readerioresult
AarrayRRcontext/readerresult
OoptionRIOCcontext/readerio
EeitherCRcontext/reader
RresultIRRidiomatic/context/readerresult
IOioEFeffect
IORioresultLoptics/lens
IOEioeitherLOoptics/lens/option
RDreaderLSoptics/lenses
ROreaderoptionNnumber
PpredicateSstring
EMendomorphismLZlazy
PApairTtuple
ERerrorsJjson
Hcontext/readerioresult/httpHDhttp/headers
Bhttp/builderChttp/content
RBcontext/readerioresult/http/builderFHhttp
SRIOcontext/statereaderioresultEQeq
IOFioresult/file

Generation Workflow

Two habits that matter more for fp-go than for idiomatic Go, because the library is low-frequency in training data and easy to misremember:

  1. Retrieve before generating. For any pattern not fully covered below — optics beyond simple lenses, traversals, concurrent combinators, the less-common monads — query the fp-go MCP server's search_examples / get_example tools (see the fp-go-mcp skill) for a real signature instead of recalling names like Chain / FlatMap / Bind from memory. Retrieve-then-generate beats generate-then-fix here.
  2. Compile before presenting. After writing fp-go code, run go build ./... and go vet ./..., then fix any import, type-parameter, or argument-order error and re-run until clean. The compiler is precise, low-ambiguity feedback, and most fp-go mistakes (wrong leading type param, data-first vs data-last) surface immediately.

Overview

fp-go (import path github.com/IBM/fp-go/v2) brings type-safe functional programming to Go using generics. Every monad follows a consistent interface: once you know the pattern in one monad, it transfers to all others.

All functions use the data-last principle: the data being transformed is always the last argument, enabling partial application and pipeline composition.

Core Types

TypePackageRepresents
Option[A]optionA value that may or may not be present (replaces nil)
Either[E, A]eitherA value that is either a left error E or a right success A
Result[A]resultEither[error, A] — recommended default for error handling
IO[A]ioA lazy computation that produces A (possibly with side effects)
IOResult[A]ioresultIO[Result[A]] — lazy computation that can fail
ReaderIOResult[A]context/readerioresultfunc(context.Context) IOResult[A] — context-aware IO with errors
Effect[C, A]effectfunc(C) ReaderIOResult[A] — typed dependency injection + IO + errors; recommended for services
Idiomatic Packages (High-Performance, Tuple-Based)

The idiomatic/ packages use Go-native tuples instead of struct wrappers, offering 2–10× better performance and zero allocations. Use them in hot paths; use standard packages when you need the richer API surface.

  • idiomatic/option — (A, bool) tuples
  • idiomatic/result — (A, error) tuples
  • idiomatic/ioresult — func() (A, error)
  • idiomatic/readerresult — func(R) (A, error)
  • idiomatic/readerioresult — func(R) func() (A, error)
  • idiomatic/context/readerresult — func(context.Context) (A, error)

Because idiomatic operators have the shape func(A, bool) (B, bool) / func(A, error) (B, error) — two arguments — F.Pipe cannot start them. Compose with F.FlowN and spread the multi-return into the call: v, ok := F.Flow2(Map(f), Filter(p))(Do(seed)). Never mix an idiomatic and a standard package for the same monad in one file; they are different types.

Standard Operations

Every monad exports these operations (PascalCase for exported Go names):

fp-gofp-ts / HaskellDescription
Ofof / pureLift a pure value into the monad
Mapmap / fmapTransform the value inside without changing the context
Chainchain / >>=Sequence a computation that itself returns a monadic value
Apap / <*>Apply a wrapped function to a wrapped value
Foldfold / eitherEliminate the context — handle every case and extract a plain value
GetOrElsegetOrElse / fromMaybeExtract the value or use a default (Option/Result)
Filterfilter / mfilterKeep only values satisfying a predicate
Flattenflatten / joinRemove one level of nesting (M[M[A]] → M[A])
ChainFirstchainFirst / >>Sequence for side effects; keeps the original value (IO-based monads also export it as Tap; prefer that name for logging)
Altalt / `<>`
FromPredicatefromPredicate / guardBuild a monadic value from a predicate
SequencesequenceTurn []M[A] into M[[]A]
TraversetraverseMap and sequence in one step

Curried (composable) vs. monadic (direct) form:

go
// Curried — data last, returns a transformer function
option.Map(strings.ToUpper)              // func(Option[string]) Option[string]

// Monadic — data first, immediate execution
option.MonadMap(option.Some("hello"), strings.ToUpper)

Use curried form for pipelines; use Monad* form when you already have all arguments.

Key Type Aliases (Defined Per Monad)

go
// A Kleisli arrow: a function from A to a monadic B
type Kleisli[A, B any] = func(A) M[B]

// An operator: transforms one monadic value into another
type Operator[A, B any] = func(M[A]) M[B]

Chain takes a Kleisli, Map returns an Operator. The naming is consistent across all monads.

Function Composition with Flow and Pipe (Point-Free Style)

fp-go is designed for point-free programming: compose named functions with Flow and Pipe rather than writing inline anonymous functions. This makes pipelines more readable and eliminates intermediate variable naming.

go
import (
    F  "github.com/IBM/fp-go/v2/function"
    N  "github.com/IBM/fp-go/v2/number"
    O  "github.com/IBM/fp-go/v2/option"
    R  "github.com/IBM/fp-go/v2/result"
    S  "github.com/IBM/fp-go/v2/string"
    LZ "github.com/IBM/fp-go/v2/lazy"
)

// ✅ GOOD: point-free — compose named functions, no lambda noise
pipeline := F.Flow3(
    R.Eitherize1(strconv.Atoi),
    R.Map(N.Mul(2)),
    R.GetOrElse(F.Constant1[error](0)), // Result's GetOrElse takes func(error) A
)

// ❌ AVOID: wrapping in unnecessary anonymous functions
pipeline := F.Flow3(
    func(s string) R.Result[int] { return R.Eitherize1(strconv.Atoi)(s) },
    func(r R.Result[int]) R.Result[int] { return R.Map(func(n int) int { return n * 2 })(r) },
    func(r R.Result[int]) int { return R.GetOrElse(func(error) int { return 0 })(r) },
)

Watch the GetOrElse shape: option.GetOrElse takes func() A (use LZ.Of(v)), while result.GetOrElse / either.GetOrElse take func(error) A / func(E) A (use F.Constant1[error](v)).

The data-last design means every fp-go operation already returns a function — so you almost never need to wrap them in a lambda. When you do need to adapt arguments, use F.Flow2 to compose:

go
// Point-free: compose a lens getter with a Kleisli arrow
RIO.Bind(configLens.Set, F.Flow2(userLens.Get, fetchConfigForUser))

// Instead of:
RIO.Bind(configLens.Set, func(s Pipeline) RIO.ReaderIOResult[Config] {
    return fetchConfigForUser(userLens.Get(s))
})

Two forms:

go
// Flow: compose functions left-to-right, returns a new function
transform := F.Flow3(
    O.Map(strings.TrimSpace),
    O.Filter(S.IsNonEmpty),
    O.GetOrElse(LZ.Of("default")),
)
value := transform(O.Some("  hello  ")) // "hello"

// Pipe: apply a value through a pipeline immediately
value := F.Pipe3(
    O.Some("  hello  "),
    O.Map(strings.TrimSpace),
    O.Filter(S.IsNonEmpty),
    O.GetOrElse(LZ.Of("default")),
)

Pipe1–Pipe20 and Flow1–Flow20 are available (the number = number of transformation steps).

Lifting Go Functions Into Monadic Context

HelperLifts
Eitherize1..EitherizeNfunc(args...) (B, error) → func(args...) Result[B] — primary bridge from Go to fp-go
ChainEitherK / ChainResultKfunc(A) Result[B] → works inside the monad. It does not accept a bare func(A) (B, error) — wrap that in result.Eitherize1 first.
ChainOptionK(onNone)func(A) Option[B] → works inside the monad; takes the func() error fallback first
TapIOK (= ChainFirstIOK)func(A) IO[B] for side effects such as logging, keeps original value
FromPredicatefunc(A) bool + error builder → func(A) Result[A]

Examples

Option — Nullable Values Without Nil
go
import (
    O  "github.com/IBM/fp-go/v2/option"
    F  "github.com/IBM/fp-go/v2/function"
    N  "github.com/IBM/fp-go/v2/number"
    S  "github.com/IBM/fp-go/v2/string"
    "github.com/IBM/fp-go/v2/optics/prism"
)

parseAndDouble := F.Flow3(
    O.FromPredicate(S.IsNonEmpty),
    O.Chain(prism.ParseInt().GetOption),
    O.Map(N.Mul(2)),
)

parseAndDouble("21")  // Some(42)
parseAndDouble("")    // None
parseAndDouble("abc") // None
Result — Error Handling Without If-Err Boilerplate
go
import (
    R  "github.com/IBM/fp-go/v2/result"
    F  "github.com/IBM/fp-go/v2/function"
    N  "github.com/IBM/fp-go/v2/number"
    P  "github.com/IBM/fp-go/v2/predicate"
    ER "github.com/IBM/fp-go/v2/errors"
    "strconv"
)

parse := R.Eitherize1(strconv.Atoi)  // lifts (int, error) → Result[int]

validate := R.FromPredicate(
    P.Not(N.LessThan(0)),
    ER.OnSome[int]("%d must not be negative"),
)

pipeline := F.Flow2(parse, R.Chain(validate))

pipeline("42")   // Ok(42)
pipeline("-1")   // Error("-1 must not be negative")
pipeline("abc")  // Error(strconv parse error)
IOResult — Lazy IO with Error Handling
go
import (
    IOR "github.com/IBM/fp-go/v2/ioresult"
    F   "github.com/IBM/fp-go/v2/function"
    J   "github.com/IBM/fp-go/v2/json"
    R   "github.com/IBM/fp-go/v2/result"
    "os"
)

readConfig := F.Flow2(
    IOR.Eitherize1(os.ReadFile),           // func(string) IOResult[[]byte]
    IOR.ChainEitherK(J.Unmarshal[Config]), // parse JSON, propagate errors
)

res := readConfig("config.json")()       // Result[Config] — note the trailing ()
cfg, err := R.Unwrap(res)                // bridge back to idiomatic Go
go
import (
    RIO "github.com/IBM/fp-go/v2/context/readerioresult"
    F   "github.com/IBM/fp-go/v2/function"
    IO  "github.com/IBM/fp-go/v2/io"
    R   "github.com/IBM/fp-go/v2/result"
)

// type ReaderIOResult[A any] = func(context.Context) func() Result[A]

// Lift the context-first Go function instead of hand-writing the nested closures:
// repo.FindUser is func(context.Context, int) (User, error)
fetchUser := RIO.Eitherize1(repo.FindUser)             // func(int) ReaderIOResult[User]

// validateUser is a plain Go func(User) (User, error) — Eitherize it first
pipeline := F.Pipe3(
    fetchUser(42),
    RIO.ChainResultK(R.Eitherize1(validateUser)),      // Kleisli: User → Result[User]
    RIO.Map(enrichUser),                               // lift pure User → User function
    RIO.TapIOK(IO.Logf[User]("Fetched: %v")),   // side-effect logging
)

res := pipeline(ctx)()               // Result[User] — ONE value, not (User, error)
user, err := R.Unwrap(res)           // bridge back to idiomatic Go
Reading and scoping the context

The context is the Reader environment: never thread ctx by hand, call ctx.Value(k).(T), or write ctx, cancel := context.WithTimeout(...); defer cancel() inside a pipeline. Use the operators — same names in context/readerio, context/readerresult, context/readerioresult, context/statereaderioresult (extra leading S type parameter) and idiomatic/context/readerresult:

OperatorTypePurpose
RIO.Ask() / RIO.FromReader(f)ReaderIOResult[context.Context] / [A]whole context / pure projection of it
RIO.AskValue[V](key)ReaderIOResult[Option[V]]typed value; None if absent or wrong type — never panics, never fails
RIO.WithValue[A](key, v)Operator[A, A]run with key → v; caller's context untouched
RIO.WithTimeout[A](d) / RIO.WithDeadline[A](t)Operator[A, A]bound in time; cancel func always released
RIO.Local[A](f)Operator[A, A]general form, f: ctx → Pair[CancelFunc, ctx]; prefer the above
go
type ctxKey string                         // unexported key type, never plain strings
const userKey ctxKey = "user"

requireUser := F.Pipe1(                    // required: None → error
    RIO.AskValue[string](userKey),
    RIO.Chain(RIO.FromOption[string](F.Constant(errNoUser))),
)
userOrAnon := F.Pipe1(                     // optional: None → default
    RIO.AskValue[string](userKey),
    RIO.Map(O.GetOrElse(LZ.Of("anonymous"))),
)

handler := F.Pipe3(                        // scoping composes like any operator
    processRequest(),
    RIO.WithTimeout[Response](5*time.Second),
    RIO.WithValue[Response](userKey, user),
    RIO.Local[Response](logging.WithLogger(reqLogger)), // WithLogger already has Local's shape
)

Outside a pipeline (building a context in main or a test) use context/reader (CR): CR.AskValue[V](k)(ctx), CR.WithValue[V](k)(v)(ctx), CR.NopCancel(ctx). Keep only request-scoped data (IDs, principal, logger) in the context; real dependencies belong in Effect. For the full guide (key accessors, cancellation semantics, Effect scoping, testing) use the fp-go-context skill.

Effect[C, A] adds a typed dependency parameter C on top of ReaderIOResult. While context/readerioresult hardcodes context.Context as the environment, Effect lets you define a custom dependencies struct — making dependencies explicit, compile-time checked, and trivially mockable in tests.

Effect[C, A] is literally func(C) ReaderIOResult[A] (an alias for context/readerreaderioresult.ReaderReaderIOResult[C, A]). A function of that exact shape is already an Effect — do not wrap it in Asks. EF.Asks(f) is for a pure projection func(C) A and returns Effect[C, A]; passing it a func(C) ReaderIOResult[A] silently yields the nested Effect[C, ReaderIOResult[A]].

Use Effect when your service has dependencies beyond context.Context (database connections, HTTP clients, config, loggers). It is the recommended top-level monad for production service code. The type parameter C is meant for these dependencies; request-scoped data (IDs, principal, deadlines) stays in context.Context. For designing dependency types (capability interfaces, Local, dependencies built by an effect, testing with fakes) use the fp-go-effect skill.

go
import (
    EF "github.com/IBM/fp-go/v2/effect"
    F  "github.com/IBM/fp-go/v2/function"
    L  "github.com/IBM/fp-go/v2/optics/lens"
)

// 1. Define your dependencies as a struct
type Deps struct {
    DB     DBClient
    Logger Logger
    Config AppConfig
}

// Leaf accessors (or generated lenses' .Get)
func getConfig(d Deps) AppConfig          { return d.Config }
func getDisplayName(u EnrichedUser) string { return u.DisplayName }

// 2. Write effects that declare exactly what they need.
// Lift an idiomatic function that takes deps and ctx — no hand-written closure:
// queryUser is func(Deps, context.Context, int) (User, error)
fetchUser := EF.Eitherize1(queryUser)      // func(int) Effect[Deps, User]

// Asks is for PURE projections of the deps: func(Deps) A → Effect[Deps, A].
// applyConfig is curried: func(User) func(AppConfig) EnrichedUser
enrichWithConfig := func(user User) EF.Effect[Deps, EnrichedUser] {
    return EF.Asks(F.Flow2(getConfig, applyConfig(user)))
}

// 3. Compose effects — same Map/Chain/Bind/ApS API as every other monad.
// C leads the type-parameter list and is not always inferable: annotate EF.Map[Deps].
pipeline := F.Pipe2(
    fetchUser(42),
    EF.Chain(enrichWithConfig),
    EF.Map[Deps](getDisplayName),
)

// 4. Provide dependencies once at the edge, then run.
// Provide[A, C]: A cannot be inferred through the returned function — annotate it.
thunk := EF.Provide[string](Deps{
    DB:     realDB,
    Logger: zapLogger,
    Config: loadedConfig,
})(pipeline) // ReaderIOResult[string]

value, err := EF.RunSync(thunk)(ctx) // RunSync gives back idiomatic (A, error)
// or: result := thunk(ctx)()        // Result[string]

Why Effect over ReaderIOResult: dependencies are typed (compiler catches missing deps), each function's signature declares what it needs (Effect[Deps, A]), testability is trivial (swap Deps{DB: mockDB}), and EF.Local/EF.Provide narrow or eliminate deps for subsystems.

Lifting into Effect (all take C as a leading, usually explicit, type parameter):

HelperSignatureLifts
EF.Ask[C]()Effect[C, C]the full dependency struct
EF.Asks(f)func(C) A → Effect[C, A]a pure projection of the deps
EF.Of[C](a) / EF.Succeed[C](a)A → Effect[C, A]a pure value
EF.Fail[C, A](err)error → Effect[C, A]an error
EF.FromResult[C](r)Result[A] → Effect[C, A]a Result
EF.FromIO[C](io)IO[A] → Effect[C, A]a plain IO
EF.FromThunk[C](t)ReaderIOResult[A] → Effect[C, A]a dep-free ReaderIOResult
EF.FromReader(r)Reader[C, A] → Effect[C, A]same as Asks
EF.Eitherize1(f)func(C, context.Context, A) (T, error) → Kleisli[C, A, T]an idiomatic method taking deps and ctx
EF.FromIdiomatic(f)KleisliI[C, A, B] → Kleisli[C, A, B]a service method func(A) func(context.Context, C) (B, error)

Running: EF.Provide[A](deps) → ReaderIOResult[A], then EF.RunSync(thunk)(ctx) → (A, error). EF.Local(f) narrows an outer dep struct to an inner one for a subsystem.

Show full SKILL.md (948 more words)Show less
Traversal — Process Slices Monadically
go
import (
    A   "github.com/IBM/fp-go/v2/array"
    RIO "github.com/IBM/fp-go/v2/context/readerioresult"
    F   "github.com/IBM/fp-go/v2/function"
)

// Fetch all users, stop on first error
fetchAll := F.Pipe1(
    A.MakeBy(10, userID),
    RIO.TraverseArray(fetchUser),  // []ReaderIOResult[User] → ReaderIOResult[[]User]
)

When to Use Which Monad

SituationUse
Value that might be absentOption[A]
Operation that can fail with custom error typeEither[E, A]
Operation that can fail with errorResult[A]
Lazy IO, side effectsIO[A]
IO that can failIOResult[A]
IO + context (cancellation, deadlines)ReaderIOResult[A] from context/readerioresult
IO + context + typed dependencies (services, DI)Effect[C, A] — recommended for production services
High-performance servicesIdiomatic packages in idiomatic/

Escalation path: Option → Result → IOResult → ReaderIOResult → Effect. Start with the simplest monad that covers your needs. For real-world services with database clients, HTTP clients, or config — go straight to Effect; it provides compile-time dependency safety that ReaderIOResult with raw context.Context cannot.

Do-Notation: Accumulating State with Bind and ApS

When a pipeline needs to carry multiple intermediate results forward, Chain/Map becomes unwieldy because each step only threads one value. Do-notation solves this by accumulating results into a growing struct at each step.

Every monad that supports do-notation exports the same family. Examples below use context/readerioresult (RIO), but the identical API is available in result, option, ioresult, readerioresult, and others.

The Function Family
FunctionKindWhat it does
Do(empty S)—Lift an empty struct into the monad; starting point
BindTo(setter)monadicConvert an existing M[T] into M[S]; alternative start
Bind(setter, f)monadicAdd a result; f receives the current state and returns M[T]
ApS(setter, fa)applicativeAdd a result; fa is independent of the current state
Let(setter, f)pureAdd a value computed by a pure function of the state
LetTo(setter, value)pureAdd a constant value

Lens variants (BindL, ApSL, LetL, LetToL) accept a Lens[S, T] instead of a manual setter.

Bind — Sequential, Dependent Steps

Bind sequences two monadic computations. f receives the full accumulated state so it can read anything gathered so far. Errors short-circuit.

go
import (
    RIO "github.com/IBM/fp-go/v2/context/readerioresult"
    F   "github.com/IBM/fp-go/v2/function"
    L   "github.com/IBM/fp-go/v2/optics/lens"
    R   "github.com/IBM/fp-go/v2/result"
)

type Pipeline struct {
    User   User
    Config Config
    Posts  []Post
}

var (
    userLens   = L.MakeLens(func(s Pipeline) User   { return s.User },   func(s Pipeline, u User)   Pipeline { s.User = u; return s })
    configLens = L.MakeLens(func(s Pipeline) Config { return s.Config }, func(s Pipeline, c Config) Pipeline { s.Config = c; return s })
    postsLens  = L.MakeLens(func(s Pipeline) []Post { return s.Posts },  func(s Pipeline, p []Post) Pipeline { s.Posts = p; return s })
)

assembled := F.Pipe3(
    RIO.Do(Pipeline{}),
    RIO.ApS(userLens.Set,    fetchUser(42)),                          // independent of state
    RIO.Bind(configLens.Set, F.Flow2(userLens.Get, fetchConfigForUser)), // reads User
    RIO.Bind(postsLens.Set,  F.Flow2(userLens.Get, fetchPostsForUser)),
)

parsed, err := R.Unwrap(assembled(ctx)())

The setter signature is func(T) func(S1) S2. lens.Set already has this shape. F.Flow2(lens.Get, f) composes the field getter with any Kleisli arrow point-free. Never write Bind(setter, func(_ S) M[T] { return m }) — a step that ignores the state is an ApS(setter, m).

ApS — Independent, Applicative Steps

ApS uses applicative semantics: fa is evaluated without access to state. Use when steps have no dependency on each other.

go
// Using same lens pattern as Bind — but steps are independent
summary := F.Pipe2(
    RIO.Do(Summary{}),
    RIO.ApS(userLens.Set,    fetchUser(42)),     // no access to state
    RIO.ApS(weatherLens.Set, fetchWeather("NYC")), // no access to state
)

Key difference:

Bind(setter, f)ApS(setter, fa)
Second argumentfunc(S1) M[T] — function of stateM[T] — fixed monadic value
Can read prior state?YesNo
SemanticsMonadic (sequential)Applicative (independent)
Let, LetTo, BindTo
  • Let(setter, f) — add a value from a pure function of state (no monad, cannot fail)
  • LetTo(setter, value) — add a constant
  • BindTo(project) — start from an existing M[T] instead of Do(empty)
Lifted Variants for Mixed Monads

Bind*K helpers lift simpler computations into the do-chain. Each takes the same setter plus a Kleisli arrow in that monad — not a raw Go (T, error) function:

Helperf
BindResultK / BindEitherKfunc(S1) Result[T]
BindIOResultKfunc(S1) IOResult[T]
BindIOKfunc(S1) IO[T]
BindReaderKfunc(S1) Reader[context.Context, T]
BindReaderIOKfunc(S1) ReaderIO[T]

For a plain Go func(S1) (T, error), compose with result.Eitherize1 first: RIO.BindResultK(lens.Set, result.Eitherize1(parse)).

Do-Notation Decision Guide
Does the new step need to read prior accumulated state?
    YES  →  Bind   (monadic, sequential; f receives current S)
    NO   →  ApS    (applicative, independent; fa is a fixed M[T])

Is the new value derived purely from state, with no monad?
    YES  →  Let    (pure function of S)

Is the new value a compile-time or runtime constant?
    YES  →  LetTo

Starting from an existing M[T] rather than an empty struct?
    YES  →  BindTo
Complete Example — result Monad with Lenses
go
import (
    R    "github.com/IBM/fp-go/v2/result"
    F    "github.com/IBM/fp-go/v2/function"
    L    "github.com/IBM/fp-go/v2/optics/lens"
    N    "github.com/IBM/fp-go/v2/number"
    "strconv"
)

type Parsed struct {
    Raw    string
    Number int
    Double int
}

var (
    rawLens    = L.MakeLens(
        func(s Parsed) string { return s.Raw },
        func(s Parsed, v string) Parsed { s.Raw = v; return s },
    )
    numberLens = L.MakeLens(
        func(s Parsed) int { return s.Number },
        func(s Parsed, v int) Parsed { s.Number = v; return s },
    )
    doubleLens = L.MakeLens(
        func(s Parsed) int { return s.Double },
        func(s Parsed, v int) Parsed { s.Double = v; return s },
    )
)

var atoi = R.Eitherize1(strconv.Atoi) // func(string) Result[int]

parse := func(input string) R.Result[Parsed] {
    return F.Pipe3(
        R.Do(Parsed{}),
        R.LetTo(rawLens.Set, input),
        R.Bind(numberLens.Set, F.Flow2(rawLens.Get, atoi)),
        R.Let(doubleLens.Set, F.Flow2(numberLens.Get, N.Mul(2))),
    )
}

parse("21")  // Ok(Parsed{Raw:"21", Number:21, Double:42})
parse("abc") // Error(strconv parse error)

Common Mistakes

MistakeFix
import "github.com/IBM/fp-go/result"Use v2: "github.com/IBM/fp-go/v2/result"
option.Map(myOption, f)Data-last: option.Map(f)(myOption)
either.Map[A, B](f)E leads in the either package: either.Map[error](f), or just either.Map(f) when E is inferable from context. In option/result the order is the natural Map[A, B] and no annotation is needed.
Using ioeither with errorUse ioresult instead; reserve ioeither for custom error types
readConfig := IOR.Eitherize1(os.ReadFile) then using result directlyIOResult is lazy — call readConfig("path")() with trailing ()
value, err := pipeline(ctx)()Running a ReaderIOResult[A] gives one Result[A]. Unwrap it: value, err := result.Unwrap(pipeline(ctx)())
Writing inline setter lambdas for Do-notationUse L.MakeLens + lens.Set; the signature already matches
Using Bind when steps are independentUse ApS for independent steps — clearer intent, potentially concurrent
Using context/readerioresult with deps stuffed into context.ContextUse effect.Effect[Deps, A] — typed deps are compile-time checked and testable
ctx.Value(k).(T) inside a Reader / FromReader / AsksRIO.AskValue[T](k) → Option[T], then O.GetOrElse (optional) or RIO.Chain(RIO.FromOption[T](...)) (required). The bare assertion panics on a missing key.
ctx, cancel := context.WithTimeout(ctx, d); defer cancel() or context.WithValue around a pipelineRIO.WithTimeout[A](d), RIO.WithDeadline[A](t), RIO.WithValue[A](k, v) as pipeline operators — scoped to the wrapped computation, cancel always released. A hand-written Local that discards cancel leaks a timer.
EF.Asks(func(d Deps) EF.ReaderIOResult[A] {...})That yields Effect[Deps, ReaderIOResult[A]]. Effect[C, A] is func(C) ReaderIOResult[A] — lift a func(C, ctx, …) (A, error) with EF.Eitherize1, or return the closure directly. Asks is for pure func(C) A.
EF.Provide(deps)(eff) / EF.Map(f) on an EffectProvide[A, C] cannot infer A through its returned function, and Map[C, A, B] often cannot infer C: write EF.Provide[string](deps) and EF.Map[Deps](f).
result.Right[error](v)result.Right[A any](v A) — the param is the success type: result.Right(v) or result.Of(v). Only result.Left[A](err) needs the annotation.
Wrapping fp-go operations in anonymous functionsGo point-free: option.Filter(S.IsNonEmpty) not option.Filter(func(s string) bool { return s != "" }), option.GetOrElse(LZ.Of("x")) not option.GetOrElse(func() string { return "x" })
M.Map(func(u User) string { return u.Name }) inside a pipelineName the leaf once (getName or nameLens.Get) and pass it: M.Map(getName)
Bind(setter, func(_ S) M[T] { return m })The step ignores the state, so it is ApS(setter, m)
Chain(func(a A) M[B] { return F.Pipe1(f(a.X), op) })Chain(F.Flow3(getX, f, op))
Hand-written func(ctx) func() Result[A] around a func(ctx, …) (A, error)RIO.Eitherize1(f) (or EF.Eitherize1 for func(C, ctx, …))
Same letter aliasing two packages (R for result in one file, reader in another)Use the canonical alias table above
Map with a function that returns Option / Result / another monadProduces nested M[M[A]]. Use Chain (or Flatten) for A → M[B]; reserve Map for plain A → B.
Closure passed to Map / Chain mutates a captured variable (slice append, counter ++)Keep it pure — derive and return new values. A mutating closure silently defeats fp-go's guarantees and breaks under Traverse / concurrency.
Mixing idiomatic/result and standard result (or option / ioresult) in one fileDifferent types — struct wrapper vs (A, error) tuple — that do not interoperate. Pick one representation per file.

Requires Go 1.24+ for generic type alias support.

© IBM, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in skills/fp-go of IBM/fp-go.

Open the folder on GitHubat commit 1c4245d

Compare with similar skills

fp-go Functional Programming for Go next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

fp-go Functional Programming for Go compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
fp-go Functional Programming for Go this skillIBM/fp-go2k—~8.5kAutomated safety check: PassApache-2.0
Golang Dependency Injectionsamber/cc-skills-golang3.4k—~3.2kAutomated safety check: PassMIT
Idiomatic Elixirgeorgeguimaraes/elixir-agent-tools184—~2.1kAutomated safety check: PassApache-2.0
Clean Codejd-solanki/slidev-theme-dracula1611 repos~3.8kAutomated safety check: PassMIT
Golang Samber Docontext-labs/whip1.1k1 repos~2.3kAutomated safety check: PassMIT
Swiftui View RefactorDimillian/Skills4k5 repos~2kAutomated safety check: PassMIT

Similar skills

  • Golang Dependency Injection

    samber/cc-skills-golang

    Comprehensive guide for dependency injection (DI) in Golang.

    3.4k GitHub stars~3.2k tokensUpdated 7 days ago
    DevelopmentAuto-check passed
  • Idiomatic Elixir

    georgeguimaraes/elixir-agent-tools

    Writes and refactors idiomatic Elixir modules and functions, with rules for pattern matching, error handling, protocols and when a process is really needed.

    184 GitHub stars~2.1k tokensUpdated 18 days ago
    DevelopmentAuto-check passed
  • Clean Code

    jd-solanki/slidev-theme-dracula

    Write readable, maintainable code through disciplined naming, small functions, and clean error handling.

    161 GitHub starsUsed in 1 repo~3.8k tokens
    DevelopmentAuto-check passed
  • Golang Samber Do

    context-labs/whip

    Dependency injection in Go using samber/do — service containers, lifecycle, scopes, health checks, graceful shutdown, module organization.

    1.1k GitHub starsUsed in 1 repo~2.3k tokens
    DevelopmentAuto-check passed
  • Swiftui View Refactor

    Dimillian/Skills

    Refactor and review SwiftUI view files with strong defaults for small dedicated subviews, MV-over-MVVM data flow, stable view trees, explicit dependency injection, and correct Observation usage.

    4k GitHub starsUsed in 5 repos~2k tokens
    DevelopmentAuto-check passed
  • Guide for writing idiomatic Rust code based on Apollo GraphQL's best practices handbook.

    5.6k GitHub starsUsed in 3 repos~1.1k tokens
    DevelopmentAuto-check passed

More from IBM/fp-go

All 10 skills in this repo
  • Official

    Covers handling Go's context.Context idiomatically in fp-go code: reading and scoping context through operators, timeouts, cancellation and converting ctx-first functions.

    2k GitHub stars~4.2k tokensUpdated yesterday
    Auto-check passed
  • Teaches an agent to write fp-go v2 services with the Effect type, carrying dependencies in its type parameter instead of in context.Context or parameters.

    2k GitHub stars~4.3k tokensUpdated yesterday
    Auto-check passed
  • Official

    Shows how to build composable, context-aware HTTP pipelines in Go using fp-go's ReaderIOResult monad instead of raw net/http calls.

    2k GitHub stars~4.4k tokensUpdated yesterday
    Auto-check passed
  • Official

    Generates or hand-writes composable lenses for the fp-go library so nested Go structs can be read and updated immutably.

    2k GitHub stars~4.3k tokensUpdated yesterday
    Auto-check passed
  • Official

    Adds logging to fp-go functional pipelines with Tap operators, entry and exit logs and error context, so that logging never changes the value or error flowing through.

    2k GitHub stars~3.7k tokensUpdated yesterday
    Auto-check passed
  • Official

    Configures and queries the fp-go MCP server so Claude Code, Claude Desktop or another MCP client can search fp-go's examples and skills directly.

    2k GitHub stars~3.3k tokensUpdated yesterday
    Auto-check passed

Works with

Categories

Questions about fp-go Functional Programming for Go

What does fp-go Functional Programming for Go do?

Entry point for writing and reviewing Go code with the fp-go v2 library: core monad types, data-last composition, type parameter order and import conventions. Main skill for IBM's fp-go library at its v2 import path.Ap[int](fa).

When should I use fp-go Functional Programming for Go?

fp-go Functional Programming for Go fits situations like: writing new Go code with fp-go's Option, Result or IOResult types; converting idiomatic Go error handling into functional pipelines; reviewing or refactoring Go code that already uses fp-go; choosing which fp-go monad fits a given function.

How do I install fp-go Functional Programming for Go in Claude Code?

Run `npx skills add IBM/fp-go --skill fp-go -a claude-code`. Or copy the skill folder (skills/fp-go in IBM/fp-go) into .claude/skills/fp-go in your project. Claude Code loads it when a task matches its description.

How do I install fp-go Functional Programming for Go in Codex?

Run `npx skills add IBM/fp-go --skill fp-go -a codex`. Or copy the skill folder (skills/fp-go in IBM/fp-go) into .agents/skills/fp-go in your project. Codex loads it when a task matches its description.

Can I use fp-go Functional Programming for Go in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add IBM/fp-go --skill fp-go -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/fp-go, .gemini/skills/fp-go, .github/skills/fp-go and .opencode/skills/fp-go in your project.

What does fp-go Functional Programming for Go need to run?

Going by SKILL.md and its folder, fp-go Functional Programming for Go needs the command-line tools its instructions call (go). Our summary lists: Go 1.24 or later with the github.com/IBM/fp-go/v2 module.

Does fp-go Functional Programming for Go access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is fp-go Functional Programming for Go safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does fp-go Functional Programming for Go use?

fp-go Functional Programming for Go is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does fp-go Functional Programming for Go use?

About 8.5k tokens (SKILL.md is roughly 34k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to fp-go Functional Programming for Go?

Skills that share tags, products or a category with fp-go Functional Programming for Go: Golang Dependency Injection (samber/cc-skills-golang, 3.4k stars), Idiomatic Elixir (georgeguimaraes/elixir-agent-tools, 184 stars), Clean Code (jd-solanki/slidev-theme-dracula, 161 stars) and Golang Samber Do (context-labs/whip, 1.1k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains fp-go Functional Programming for Go?

IBM (a GitHub organization, an official publisher) maintains it in IBM/fp-go, which has 2,029 GitHub stars. The repository holds 10 skills in this directory. The repository was last updated on October 7, 2026.

Source: IBM/fp-go on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.