| name | fp-go-pipe-flow |
| description | Guides writing, refactoring, and reviewing fp-go v2 code that uses functional composition via Pipe and Flow. Apply this skill whenever the user asks to write new fp-go code, refactor imperative Go into functional style, explain a Pipe/Flow pipeline, add do-notation (Do/Bind/ApS), use the reader monad, create lenses for struct fields, or generate unit tests for functional pipelines. Trigger on any mention of: Pipe, Flow, reader monad, kleisli, do-notation, Bind, ApS, fp-go pipeline, point-free style, lens composition in Go.
|
fp-go Pipe and Flow Patterns
All imports must come from github.com/IBM/fp-go/v2, never from
github.com/IBM/fp-go (the v1 path).
Before you generate
fp-go is low-frequency in training data, so signatures are easy to misremember.
For any combinator not shown below, look it up via the fp-go MCP server's
search_examples / get_example tools (see the fp-go-mcp skill) instead of
guessing. After writing code, run go build ./... and go vet ./... and fix any
type-parameter or argument-order errors before presenting it.
Core concepts
Pipe — data-first composition
Pipe takes an initial value and threads it through a sequence of functions.
Use it when you already have a value to start from.
import F "github.com/IBM/fp-go/v2/function"
result := F.Pipe3(initialValue, step1, step2, step3)
The number suffix matches the number of transformation steps (Pipe1 … Pipe26).
Flow — function-first composition
Flow composes N functions into a single function that awaits its input.
Use it to build reusable pipeline functions, especially as arguments to Map,
Chain, or TraverseArray.
pipeline := F.Flow3(step1, step2, step3)
result := pipeline(initialValue)
Rule of thumb: prefer Pipe when you have the starting value; use Flow
when you are building a reusable function.
Prefer functions over variables
Go does not eliminate dead variables, but unused functions are zero-cost.
Always wrap a Pipe/Flow result in a named function rather than storing it
in a package-level var.
var processUser = F.Flow2(getName, strings.ToUpper)
func processUser() func(User) string {
return F.Flow2(getName, strings.ToUpper)
}
Use var only for lenses and pre-bound combinator helpers (like lens.Get
assigned to a named getter), not for full pipeline results.
Point-free style
Avoid explicit argument names wherever a named combinator or Flow can express
the same thing.
func isAdult(u User) bool { return getAge(u) > 18 }
func isAdult() P.Predicate[User] {
return F.Flow2(getAge, N.MoreThan(18))
}
Type aliases to use
| Type | Package | Meaning |
|---|
P.Predicate[A] | github.com/IBM/fp-go/v2/predicate | func(A) bool |
E.Endomorphism[A] | github.com/IBM/fp-go/v2/endomorphism | func(A) A |
Use these as return types for functions that act as predicates or
self-transformations — they communicate intent and enable direct use in
combinators like A.Filter, A.Map, F.Ternary.
import (
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
P "github.com/IBM/fp-go/v2/predicate"
E "github.com/IBM/fp-go/v2/endomorphism"
A "github.com/IBM/fp-go/v2/array"
)
func isAdult() P.Predicate[User] {
return F.Flow2(getAge, N.MoreThan(18))
}
func doubleAll() E.Endomorphism[[]int] {
return A.Map[int, int](N.Mul(2))
}
Numeric combinators
Prefer N.MoreThan, N.LessThan, N.Mul, N.Add etc. over inline
comparisons or arithmetic in lambdas:
N.MoreThan(18)
N.LessThan(100)
N.Mul(2)
N.Add(1)
Pure pipelines vs the reader monad
Only use the reader monad when the computation genuinely needs an environment
(context, config, DB, logger, etc.). For pure transformations that don't
need external input, use Flow or Pipe directly — no reader wrapping needed.
func adultNames(users []User) R.Reader[context.Context, string] {
return F.Pipe2(
R.Of[context.Context](users),
R.Map[context.Context, []User, string](pureTransform),
)
}
func adultNames() func([]User) string {
return F.Flow2(
A.FilterMap(toAdultName()),
A.Intercalate(S.Monoid)(","),
)
}
Per-element filter+map: use A.FilterMap
When filtering and then extracting a field, combine both into a single pass
with A.FilterMap and O.FromPredicate:
import (
F "github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/array"
O "github.com/IBM/fp-go/v2/option"
N "github.com/IBM/fp-go/v2/number"
P "github.com/IBM/fp-go/v2/predicate"
S "github.com/IBM/fp-go/v2/string"
E "github.com/IBM/fp-go/v2/endomorphism"
)
func isAdult() P.Predicate[User] {
return F.Flow2(getAge, N.MoreThan(18))
}
func toAdultName() func(User) O.Option[string] {
return F.Flow2(
O.FromPredicate(isAdult()),
O.Map(getName),
)
}
func adultNames() func([]User) string {
return F.Flow2(
A.FilterMap(toAdultName()),
A.Intercalate(S.Monoid)(","),
)
}
Reader monad
The reader monad Reader[R, A] is func(R) A — a computation that reads
from an environment R and produces A. Only reach for it when the
computation needs to thread an environment (e.g. context.Context, a
config struct, a DB handle).
import (
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/reader"
"context"
)
func fetchUser(id string) R.Reader[context.Context, User] {
return R.Asks(func(ctx context.Context) User {
return User{ID: id}
})
}
When to use reader.Map vs full Pipe with reader operations
reader.Map inside Flow — when the step is pure and the environment
does not need to appear explicitly. This is the "abbreviation" pattern.
Pipe with reader.Chain, reader.Bind, reader.ApS — when the
sequence needs the context (e.g. calls another kleisli arrow) or when
do-notation makes the data flow clearer.
func renderUsers() func([]User) R.Reader[context.Context, string] {
return F.Flow2(
usersToNames,
R.Map[context.Context](strings.Join),
)
}
func enrichedUser(id string) R.Reader[context.Context, EnrichedUser] {
return F.Pipe3(
fetchUser(id),
R.Chain(fetchProfile),
R.Chain(fetchPermissions),
R.Map[context.Context](combineToEnriched),
)
}
Do-notation: Do / Bind / ApS / Let
Do-notation is the idiomatic way to assemble multiple reader (or IO/result)
computations into a named-field record. Always use it inside a Pipe.
import (
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/reader"
"context"
)
func setProfile(p Profile) func(RequestState) RequestState {
return func(s RequestState) RequestState { s.Profile = p; return s }
}
func fetchProfile(s RequestState) R.Reader[context.Context, Profile] {
return R.Asks(func(ctx context.Context) Profile { return Profile{} })
}
func buildRequestState(userID string) R.Reader[context.Context, RequestState] {
return F.Pipe3(
R.Do[context.Context](RequestState{UserID: userID}),
R.Bind(setProfile, fetchProfile),
R.Bind(setPerms, fetchPerms),
R.Map[context.Context](F.Identity[RequestState]),
)
}
Bind vs ApS vs Let
| Combinator | When to use |
|---|
Bind(setter, kleisli) | Result depends on accumulated state (sequential) |
ApS(setter, reader) | Result is independent of other fields |
Let(setter, pureFunc) | Pure transformation of accumulated state, no reader needed |
LetTo(setter, value) | Attach a constant value to state |
Use ApS when values can be computed independently; Bind when a later step
depends on an earlier one. Mixing them in the same pipeline is normal.
Lenses for struct field access
Never access struct fields with inline functions inside a Pipe. Create a
lens or a dedicated helper so the pipeline stays point-free.
import (
L "github.com/IBM/fp-go/v2/optics/lens"
)
var hostLens = L.MakeLens(
func(c Config) string { return c.Host },
func(c Config, v string) Config { c.Host = v; return c },
)
var getHost = hostLens.Get
var getPort = portLens.Get
Use R.ApSL(lens, reader) / R.BindL(lens, kleisli) as do-notation variants
that take a lens directly instead of a setter function.
Unit tests
Generate a _test.go for every non-trivial pipeline or flow.
func TestAdultNames(t *testing.T) {
users := []User{{Name: "Alice", Age: 25}, {Name: "Bob", Age: 16}}
assert.Equal(t, "Alice", adultNames()(users))
}
func TestBuildRequestState(t *testing.T) {
ctx := context.Background()
state := buildRequestState("user-42")(ctx)
assert.Equal(t, "user-42", state.UserID)
}
Testing guidelines
- For pure
Flow/Pipe functions: call the returned function with a concrete
value and assert with assert.Equal.
- For reader pipelines: call the reader with a concrete environment struct.
- For
IOResult/ReaderIOResult: call the innermost IO thunk and compare
with result.Of(expected).
- Prefer table-driven tests for pipelines with multiple input/output pairs.
- Do not mock the environment — pass a real (but lightweight) struct.
Common import aliases
import (
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/reader"
RR "github.com/IBM/fp-go/v2/readerresult"
IO "github.com/IBM/fp-go/v2/ioresult"
E "github.com/IBM/fp-go/v2/either"
O "github.com/IBM/fp-go/v2/option"
L "github.com/IBM/fp-go/v2/optics/lens"
A "github.com/IBM/fp-go/v2/array"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
P "github.com/IBM/fp-go/v2/predicate"
En "github.com/IBM/fp-go/v2/endomorphism"
)
Quick reference
| Goal | Pattern |
|---|
| Thread a value through N steps | F.PipeN(value, f1, f2, …) |
| Build a reusable function | F.FlowN(f1, f2, …) |
| Point-free numeric predicate | F.Flow2(getField, N.MoreThan(n)) returning P.Predicate[T] |
| Filter+map in one pass | A.FilterMap(F.Flow2(O.FromPredicate(pred), O.Map(f))) |
| Lift a pure function into Reader | R.Map[Env](pureFunc) |
| Chain kleisli arrows | R.Chain(kleisliFunc) |
| Start do-notation block | R.Do[Env](emptyStruct) |
| Add dependent field | R.Bind(setter, kleisliFunc) |
| Add independent field | R.ApS(setter, readerValue) |
| Add pure derived field | R.Let[Env](setter, pureFunc) |
| Lens getter in pipeline | var getX = xLens.Get |
| Do-notation with lens | R.ApSL(lens, readerValue) |
| Access full environment | R.Ask[Env]() |
| Access field of environment | R.Asks(getX) |