Golang Dependency Injection
samber/cc-skills-golang
Comprehensive guide for dependency injection (DI) in Golang.
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.
$ npx skills add IBM/fp-go --skill fp-go -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install IBM/fp-go fp-go --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ 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-srcUse ~/.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/
Install the "fp-go" agent skill from https://github.com/IBM/fp-go/tree/main/skills/fp-go into .claude/skills/fp-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fp-go", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/IBM/fp-go/tree/main/skills/fp-goType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add IBM/fp-go --skill fp-go -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install IBM/fp-go fp-go --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/IBM/fp-go.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/fp-go .agents/skills/fp-go && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "fp-go" agent skill from https://github.com/IBM/fp-go/tree/main/skills/fp-go into .agents/skills/fp-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fp-go", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add IBM/fp-go --skill fp-go -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install IBM/fp-go fp-go --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/IBM/fp-go.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/fp-go .cursor/skills/fp-go && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "fp-go" agent skill from https://github.com/IBM/fp-go/tree/main/skills/fp-go into .cursor/skills/fp-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fp-go", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/IBM/fp-go.git --path skills/fp-go--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add IBM/fp-go --skill fp-go -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install IBM/fp-go fp-go --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/IBM/fp-go.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/fp-go .gemini/skills/fp-go && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "fp-go" agent skill from https://github.com/IBM/fp-go/tree/main/skills/fp-go into .gemini/skills/fp-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fp-go", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install IBM/fp-go fp-goInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add IBM/fp-go --skill fp-go -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/IBM/fp-go.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/fp-go .github/skills/fp-go && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "fp-go" agent skill from https://github.com/IBM/fp-go/tree/main/skills/fp-go into .github/skills/fp-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fp-go", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add IBM/fp-go --skill fp-go -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install IBM/fp-go fp-go --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/IBM/fp-go.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/fp-go .opencode/skills/fp-go && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "fp-go" agent skill from https://github.com/IBM/fp-go/tree/main/skills/fp-go into .opencode/skills/fp-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fp-go", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
fp-goEntry 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. 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.
7 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 1c4245d. It shows what the files ask for, not the result of running them.
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.
Shell commands in SKILL.md call:
goFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
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.
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.
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.
The full file from IBM/fp-go at commit 1c4245d, republished under its Apache-2.0 licence (© IBM). 2,692 words, ~8,514 tokens.
.claude/skills/fp-go/SKILL.md (or your agent's skills folder).github.com/IBM/fp-go/v2/..., never github.com/IBM/fp-go/... (that is v1).option.Map(f)(value), never option.Map(value, f).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).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.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.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: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.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.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.R.Eitherize1(strconv.Atoi), RIO.Eitherize1(repo.FindUser), EF.Eitherize1(queryUser) instead of hand-written closures.func fetchAll() RIO.Kleisli[[]int, []User] { return RIO.TraverseArray(fetchUser) }, not func fetchAll(ids []int) … { return RIO.TraverseArray(fetchUser)(ids) }.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).
| Alias | Package | Alias | Package | |
|---|---|---|---|---|
F | function | RIO | context/readerioresult | |
A | array | RR | context/readerresult | |
O | option | RIOC | context/readerio | |
E | either | CR | context/reader | |
R | result | IRR | idiomatic/context/readerresult | |
IO | io | EF | effect | |
IOR | ioresult | L | optics/lens | |
IOE | ioeither | LO | optics/lens/option | |
RD | reader | LS | optics/lenses | |
RO | readeroption | N | number | |
P | predicate | S | string | |
EM | endomorphism | LZ | lazy | |
PA | pair | T | tuple | |
ER | errors | J | json | |
H | context/readerioresult/http | HD | http/headers | |
B | http/builder | C | http/content | |
RB | context/readerioresult/http/builder | FH | http | |
SRIO | context/statereaderioresult | EQ | eq | |
IOF | ioresult/file |
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:
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.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.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.
| Type | Package | Represents |
|---|---|---|
Option[A] | option | A value that may or may not be present (replaces nil) |
Either[E, A] | either | A value that is either a left error E or a right success A |
Result[A] | result | Either[error, A] — recommended default for error handling |
IO[A] | io | A lazy computation that produces A (possibly with side effects) |
IOResult[A] | ioresult | IO[Result[A]] — lazy computation that can fail |
ReaderIOResult[A] | context/readerioresult | func(context.Context) IOResult[A] — context-aware IO with errors |
Effect[C, A] | effect | func(C) ReaderIOResult[A] — typed dependency injection + IO + errors; recommended for services |
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) tuplesidiomatic/result — (A, error) tuplesidiomatic/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.
Every monad exports these operations (PascalCase for exported Go names):
| fp-go | fp-ts / Haskell | Description |
|---|---|---|
Of | of / pure | Lift a pure value into the monad |
Map | map / fmap | Transform the value inside without changing the context |
Chain | chain / >>= | Sequence a computation that itself returns a monadic value |
Ap | ap / <*> | Apply a wrapped function to a wrapped value |
Fold | fold / either | Eliminate the context — handle every case and extract a plain value |
GetOrElse | getOrElse / fromMaybe | Extract the value or use a default (Option/Result) |
Filter | filter / mfilter | Keep only values satisfying a predicate |
Flatten | flatten / join | Remove one level of nesting (M[M[A]] → M[A]) |
ChainFirst | chainFirst / >> | Sequence for side effects; keeps the original value (IO-based monads also export it as Tap; prefer that name for logging) |
Alt | alt / `< | >` |
FromPredicate | fromPredicate / guard | Build a monadic value from a predicate |
Sequence | sequence | Turn []M[A] into M[[]A] |
Traverse | traverse | Map and sequence in one step |
Curried (composable) vs. monadic (direct) form:
// 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.
// 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.
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.
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:
// 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:
// 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).
| Helper | Lifts |
|---|---|
Eitherize1..EitherizeN | func(args...) (B, error) → func(args...) Result[B] — primary bridge from Go to fp-go |
ChainEitherK / ChainResultK | func(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 |
FromPredicate | func(A) bool + error builder → func(A) Result[A] |
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") // Noneimport (
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)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 Goimport (
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 GoThe 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:
| Operator | Type | Purpose |
|---|---|---|
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 |
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.
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):
| Helper | Signature | Lifts |
|---|---|---|
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.
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]
)| Situation | Use |
|---|---|
| Value that might be absent | Option[A] |
| Operation that can fail with custom error type | Either[E, A] |
Operation that can fail with error | Result[A] |
| Lazy IO, side effects | IO[A] |
| IO that can fail | IOResult[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 services | Idiomatic 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.
Bind and ApSWhen 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.
| Function | Kind | What it does |
|---|---|---|
Do(empty S) | — | Lift an empty struct into the monad; starting point |
BindTo(setter) | monadic | Convert an existing M[T] into M[S]; alternative start |
Bind(setter, f) | monadic | Add a result; f receives the current state and returns M[T] |
ApS(setter, fa) | applicative | Add a result; fa is independent of the current state |
Let(setter, f) | pure | Add a value computed by a pure function of the state |
LetTo(setter, value) | pure | Add a constant value |
Lens variants (BindL, ApSL, LetL, LetToL) accept a Lens[S, T] instead of a manual setter.
Bind — Sequential, Dependent StepsBind sequences two monadic computations. f receives the full accumulated state so it can read anything gathered so far. Errors short-circuit.
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 StepsApS uses applicative semantics: fa is evaluated without access to state. Use when steps have no dependency on each other.
// 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 argument | func(S1) M[T] — function of state | M[T] — fixed monadic value |
| Can read prior state? | Yes | No |
| Semantics | Monadic (sequential) | Applicative (independent) |
Let, LetTo, BindToLet(setter, f) — add a value from a pure function of state (no monad, cannot fail)LetTo(setter, value) — add a constantBindTo(project) — start from an existing M[T] instead of Do(empty)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:
| Helper | f |
|---|---|
BindResultK / BindEitherK | func(S1) Result[T] |
BindIOResultK | func(S1) IOResult[T] |
BindIOK | func(S1) IO[T] |
BindReaderK | func(S1) Reader[context.Context, T] |
BindReaderIOK | func(S1) ReaderIO[T] |
For a plain Go func(S1) (T, error), compose with result.Eitherize1 first:
RIO.BindResultK(lens.Set, result.Eitherize1(parse)).
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 → BindToresult Monad with Lensesimport (
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)| Mistake | Fix |
|---|---|
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 error | Use ioresult instead; reserve ioeither for custom error types |
readConfig := IOR.Eitherize1(os.ReadFile) then using result directly | IOResult 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-notation | Use L.MakeLens + lens.Set; the signature already matches |
Using Bind when steps are independent | Use ApS for independent steps — clearer intent, potentially concurrent |
Using context/readerioresult with deps stuffed into context.Context | Use effect.Effect[Deps, A] — typed deps are compile-time checked and testable |
ctx.Value(k).(T) inside a Reader / FromReader / Asks | RIO.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 pipeline | RIO.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 Effect | Provide[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 functions | Go 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 pipeline | Name 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 monad | Produces 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 file | Different 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
Just SKILL.md in skills/fp-go of IBM/fp-go.
Open the folder on GitHubat commit 1c4245d
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.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| fp-go Functional Programming for Go this skillIBM/fp-go | 2k | — | ~8.5k | Automated safety check: Pass | Apache-2.0 | |
| Golang Dependency Injectionsamber/cc-skills-golang | 3.4k | — | ~3.2k | Automated safety check: Pass | MIT | |
| Idiomatic Elixirgeorgeguimaraes/elixir-agent-tools | 184 | — | ~2.1k | Automated safety check: Pass | Apache-2.0 | |
| Clean Codejd-solanki/slidev-theme-dracula | 161 | 1 repos | ~3.8k | Automated safety check: Pass | MIT | |
| Golang Samber Docontext-labs/whip | 1.1k | 1 repos | ~2.3k | Automated safety check: Pass | MIT | |
| Swiftui View RefactorDimillian/Skills | 4k | 5 repos | ~2k | Automated safety check: Pass | MIT |
samber/cc-skills-golang
Comprehensive guide for dependency injection (DI) in Golang.
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.
jd-solanki/slidev-theme-dracula
Write readable, maintainable code through disciplined naming, small functions, and clean error handling.
context-labs/whip
Dependency injection in Go using samber/do — service containers, lifecycle, scopes, health checks, graceful shutdown, module organization.
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.
farm-fe/farm
Guide for writing idiomatic Rust code based on Apollo GraphQL's best practices handbook.
IBM/fp-go
Covers handling Go's context.Context idiomatically in fp-go code: reading and scoping context through operators, timeouts, cancellation and converting ctx-first functions.
IBM/fp-go
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.
IBM/fp-go
Shows how to build composable, context-aware HTTP pipelines in Go using fp-go's ReaderIOResult monad instead of raw net/http calls.
IBM/fp-go
Generates or hand-writes composable lenses for the fp-go library so nested Go structs can be read and updated immutably.
IBM/fp-go
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.
IBM/fp-go
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.
Works with
Categories
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.