--- name: fp-go-context description: >- Use this skill when working with Go's context.Context in fp-go code (github.com/IBM/fp-go/v2/context/...). Trigger on mentions of context.Context in fp-go pipelines, request-scoped values, ctx.Value, context.WithValue, context keys, AskValue, WithValue, timeouts or deadlines (WithTimeout, WithDeadline, context.WithTimeout, defer cancel), cancellation, Local / LocalIOK / LocalIOResultK, WithContext / WithContextK, request-scoped loggers, converting func(ctx, ...) (T, error) functions to ReaderIOResult, or reviewing code that threads ctx by hand. --- # fp-go Context Handling ## Core Principle In imperative Go the context is threaded by hand: every function takes `ctx` first, values are read with `ctx.Value(k).(T)`, scopes are opened with `ctx, cancel := context.WithTimeout(...); defer cancel()`. In fp-go the context is the **Reader environment**. A computation is a *description* `func(context.Context) …` that runs only when a context is supplied. Therefore: 1. **Pipelines never mention `ctx`.** Build them from operators; the context flows implicitly. 2. **Supply the context exactly once, at the edge** — HTTP handler (`r.Context()`), `main`, or a test (`t.Context()`). 3. **Read the context with operators** (`Ask`, `FromReader`, `AskValue`), never with `ctx.Value(k).(T)`. 4. **Scope the context with operators** (`WithValue`, `WithTimeout`, `WithDeadline`, `Local`), never with `context.With*` + `defer cancel()`. 5. **Only request-scoped data goes into the context.** Dependencies (DB, clients, config) belong in `Effect[Deps, A]`. ## 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. ## Packages and Availability | Package | Shape | Use when | |---------|-------|----------| | `context/readerioresult` (`RIO`) | `func(ctx) func() Result[A]` | default for services: IO + errors + context | | `context/readerresult` (`RR`) | `func(ctx) Result[A]` | synchronous, can fail | | `context/readerio` (`RIOC`) | `func(ctx) func() A` | IO that cannot fail | | `context/statereaderioresult` (`SRIO`) | `func(S) func(ctx) func() Result[Pair[S, A]]` | explicit state + context | | `idiomatic/context/readerresult` (`IRR`) | `func(ctx) (A, error)` | high-performance, native `(A, error)` | | `context/reader` (`CR`) | `func(ctx) A` | plain building blocks for use *outside* pipelines | | `effect` (`EF`) | `func(C) RIO.ReaderIOResult[A]` | typed deps `C` **plus** the runtime context | | Operator | RIO | RR | RIOC | SRIO | IRR | CR | |----------|:---:|:--:|:----:|:----:|:---:|:--:| | `Ask()` | ✓ | ✓ | ✓ | | ✓ | | | `FromReader(f)` / `Asks(f)` | ✓ | ✓ | ✓ | `Asks` | ✓ | | | `AskValue[V](key)` | ✓ | ✓ | ✓ | ✓ (`[S, V]`) | ✓ | ✓ | | `WithValue[A](key, v)` | ✓ | ✓ | ✓ | ✓ (`[S, A]`) | ✓ | Kleisli form | | `WithTimeout[A](d)` / `WithDeadline[A](t)` | ✓ | ✓ | ✓ | ✓ (`[S, A]`) | ✓ | | | `Local[A](f)` | ✓ | ✓ | ✓ | ✓ | ✓ | | | `LocalIOK` / `LocalIOResultK` | ✓ / ✓ | | ✓ / | | | | | `WithContext` / `WithContextK` | ✓ | ✓ | | | ✓ | | | `NopCancel(ctx)` | | | | | | ✓ | `Local`'s argument is `func(ctx) Pair[context.CancelFunc, context.Context]` in the standard packages and `func(ctx) (context.Context, context.CancelFunc)` in `IRR`. ## 1. Running at the Edge ```go // HTTP handler: the request context is supplied once func handler(w http.ResponseWriter, r *http.Request) { res := handleRequest(r)(r.Context())() // Result[Response] — ONE value resp, err := R.Unwrap(res) // bridge back to (A, error) // ... } // Tests: use t.Context(), cancelled automatically when the test ends res := pipeline(t.Context())() ``` Never capture a `ctx` in a closure and never store it in a struct; pass it only when running. ## 2. Bridging `func(ctx, …) (T, error)` Existing context-first Go functions are lifted, not wrapped by hand: ```go // func(context.Context, string) ([]byte, error) -> func(string) RIO.ReaderIOResult[[]byte] fetch := RIO.Eitherize1(fetchBytes) // idiomatic package: the shape is already native fetchI := IRR.From1(fetchBytes) // func(string) IRR.ReaderResult[[]byte] // and back, for APIs that expect the Go shape fetchGo := RIO.Uneitherize1(fetch) // func(context.Context, string) ([]byte, error) ``` The lifted function receives the pipeline's (possibly scoped) context — timeouts and values applied with the operators below reach it automatically. ## 3. Reading the Context | Need | Operator | Result | |------|----------|--------| | whole context | `RIO.Ask()` | `ReaderIOResult[context.Context]` | | pure projection | `RIO.FromReader(f)` | `ReaderIOResult[A]` | | one typed value | `RIO.AskValue[V](key)` | `ReaderIOResult[Option[V]]` | `AskValue` **never panics and never fails**: `Some(v)` if the key holds a `V`, `None` if it is absent *or holds another type*. The caller decides what "missing" means. ### Keys and Accessors Use an unexported key type — never plain strings or exported types — and keep one read and one write accessor next to each key, so the rest of the code never touches the key: ```go type ctxKey int const ( userKey ctxKey = iota requestIDKey ) var errNoUser = errors.New("no authenticated user in context") // optional value: None -> default func requestID() RIO.ReaderIOResult[string] { return F.Pipe1( RIO.AskValue[string](requestIDKey), RIO.Map(O.GetOrElse(LZ.Of("-"))), ) } // required value: None -> error func requireUser() RIO.ReaderIOResult[User] { return F.Pipe1( RIO.AskValue[User](userKey), RIO.Chain(RIO.FromOption[User](F.Constant(errNoUser))), ) } // writer: scopes the value to the wrapped computation func withUser[A any](u User) RIO.Operator[A, A] { return RIO.WithValue[A](userKey, u) } ``` `RR` and `IRR` have no `FromOption`, so the required-value accessor looks slightly different there: ```go // RR: lift result.FromOption (Option[User] -> Result[User]) into the reader func requireUserRR() RR.ReaderResult[User] { return F.Pipe1( RR.AskValue[User](userKey), RR.ChainEitherK(R.FromOption[User](LZ.Of(errNoUser))), ) } // IRR: ChainOptionK expects an idiomatic (B, bool) Kleisli; O.Unwrap turns the Option into that tuple func requireUserIRR() IRR.ReaderResult[User] { return F.Pipe1( IRR.AskValue[User](userKey), IRR.ChainOptionK[O.Option[User], User](LZ.Of(errNoUser))(O.Unwrap[User]), ) } ``` The `IRR` form needs the explicit `[O.Option[User], User]` because `ChainOptionK` receives only the `onNone` error first, so Go cannot infer `A` and `B` from it. ## 4. Scoping the Context Scoping operators run the wrapped computation with a **derived** context. The caller's context is never modified, and the derived context's cancel function is **always** released when the computation completes — no `defer cancel()`, no leaks. | Operator | Derives with | Notes | |----------|--------------|-------| | `WithValue[A](key, v)` | `context.WithValue` | inner values shadow outer ones for the same key | | `WithTimeout[A](d)` | `context.WithTimeout` | relative to when the computation *runs*, not when the operator is built | | `WithDeadline[A](t)` | `context.WithDeadline` | an earlier parent deadline still wins | | `Local[A](f)` | anything | general form; the others are built on it | They are ordinary operators and compose in `Pipe`. The context flows **from the last operator inwards**, so the last one is applied first: ```go func handleRequest(r *http.Request) RIO.ReaderIOResult[Response] { return F.Pipe4( requireUser(), RIO.Chain(loadProfile), RIO.WithTimeout[Response](5*time.Second), // bounds the work above withUser[Response](userFromRequest(r)), // visible to everything above RIO.WithValue[Response](requestIDKey, r.Header.Get(HD.XRequestID)), ) } ``` Scope as narrowly as the requirement: put `WithTimeout` directly after the step it should bound, not around the whole handler, if only one call needs it. ### Deriving the Context from an Effect When the new context value itself needs IO (generating an ID, loading a token), use `LocalIOK` / `LocalIOResultK` with the `context/reader` building blocks: ```go // uuid.NewString is a func() string, i.e. already an IO[string] func addRequestID(ctx context.Context) IO.IO[RIO.ContextCancel] { return F.Pipe1( uuid.NewString, IO.Map(F.Flow3( CR.WithValue[string](requestIDKey), // string -> Endomorphism[context.Context] RD.Read[context.Context](ctx), // apply it to ctx CR.NopCancel, // -> Pair[CancelFunc, context.Context] )), ) } scoped := RIO.LocalIOK[Response](addRequestID)(handler) ``` `LocalIOResultK` does the same with a fallible derivation; on failure the wrapped computation does not run. ## 5. Cancellation - In `RIO`, `RR` and `IRR`, **`Chain` checks the context before each step**: once the context is cancelled, the next step does not run and the result is `Left(context.Cause(ctx))`. `Map` does not check (pure functions are cheap). - `RIOC` (`context/readerio`) has no error channel, so it cannot short-circuit; use it only for IO that is fine to complete. - `WithContext(ma)` / `WithContextK(f)` add the same check explicitly, e.g. before an expensive first step. - `Delay`, retries, `Bracket` / `WithResource` and the HTTP client observe cancellation. - **Leaf computations that block** (loops, `select`, custom IO) must watch `ctx.Done()` themselves and return `ctx.Err()` / `context.Cause(ctx)`: ```go func waitForJob(id string) RIO.ReaderIOResult[Job] { return func(ctx context.Context) RIO.IOResult[Job] { return func() R.Result[Job] { select { case job := <-jobs(id): return R.Of(job) case <-ctx.Done(): return R.Left[Job](context.Cause(ctx)) } } } } ``` ## 6. Request-Scoped Logger `logging.WithLogger(l)` already has `Local`'s argument shape, so a request logger is one operator. All context-aware logging (`TapSLog`, `SLog`, `LogEntryExit`) inside picks it up: ```go F.Pipe1( pipeline, RIO.Local[Response](logging.WithLogger(slog.Default().With("requestID", id))), ) ``` Read it with `logging.GetLoggerFromContext` (falls back to the global logger). For a logger under your own key: `F.Flow2(CR.AskValue[*slog.Logger](myKey), O.GetOrElse(slog.Default))`. ## 7. Context in `Effect` `C` holds the dependencies and `context.Context` holds the request scope; see the `fp-go-effect` skill for designing `C`. `Effect[C, A]` is `func(C) RIO.ReaderIOResult[A]`: `C` carries typed dependencies, `context.Context` is still the runtime context supplied by `RunSync(…)(ctx)`. - `EF.Local`, `EF.Ask`, `EF.Asks` operate on **`C`**, not on `context.Context`. - `EF.Eitherize(func(C, context.Context) (T, error))` receives both. - To scope the runtime context of an `Effect`, lift the `RIO` operator over `C` with `RD.Map` (plain `reader.Map`): ```go bounded := RD.Map[Deps](RIO.WithTimeout[User](2*time.Second))(fetchUserEffect) // Effect[Deps, User] tagged := RD.Map[Deps](RIO.WithValue[User](requestIDKey, id))(fetchUserEffect) ``` (`RD` = `github.com/IBM/fp-go/v2/reader`.) ## 8. Outside Pipelines When a plain `context.Context` is genuinely needed (handing it to a non-fp-go API, building a fixture), use the `context/reader` building blocks instead of the standard library calls, so reads and writes stay symmetric: ```go ctx2 := CR.WithValue[User](userKey)(u)(ctx) // context.WithValue, curried user := CR.AskValue[User](userKey)(ctx2) // Option[User] cc := CR.NopCancel(ctx2) // Pair[CancelFunc, ctx] for Local-shaped APIs ``` For optics composition over the context: `lenses.AtContext[V](key)` (`optics/lenses`) is a lens `context.Context → Option[V]`. ## 9. What Belongs in the Context | Data | Where | |------|-------| | request / correlation / trace IDs | context (`WithValue`) | | authenticated principal | context (`WithValue`) | | request-scoped logger | context (`Local(logging.WithLogger(l))`) | | deadlines, cancellation | context (`WithTimeout`, `WithDeadline`) | | DB handles, HTTP clients, repositories | `Effect[Deps, A]` | | configuration, feature flags | `Effect[Deps, A]` or function parameters | | per-call inputs | function parameters (Kleisli arrows) | A value that is required for the program to be correct is a dependency, not a context value — the compiler cannot check that it was provided. ## 10. Testing ```go func TestRequireUser(t *testing.T) { // present res := F.Pipe1(requireUser(), withUser[User](alice))(t.Context())() assert.Equal(t, R.Of(alice), res) // missing -> error, no panic assert.Equal(t, R.Left[User](errNoUser), requireUser()(t.Context())()) } func TestTimeout(t *testing.T) { res := F.Pipe1(waitForJob("slow"), RIO.WithTimeout[Job](10*time.Millisecond))(t.Context())() assert.Equal(t, R.Left[Job](context.DeadlineExceeded), res) } ``` - Always run with `t.Context()`, not `context.Background()`. - Inject context values with the same `with…` accessors production code uses. - Test the missing-value path of every required value. - Keep timeouts in tests short (milliseconds) and assert on `context.DeadlineExceeded`. ## Common Mistakes | ❌ Avoid | ✅ Prefer | Why | |---------|----------|-----| | `ctx.Value(k).(T)` | `RIO.AskValue[T](k)` + `GetOrElse` / `FromOption` | assertion panics when missing or mistyped | | `v, _ := ctx.Value(k).(T)` then `if v == ""` | `AskValue` + `Option` | zero value is indistinguishable from "missing" | | `context.WithValue(ctx, "user", u)` | unexported `ctxKey` + `WithValue[A](userKey, u)` | string keys collide across packages | | `ctx, cancel := context.WithTimeout(ctx, d); defer cancel()` around `pipeline(ctx)()` | `F.Pipe1(pipeline, RIO.WithTimeout[A](d))` | scoping belongs to the description, not the call site | | `Local` with `ctx2, _ := context.WithTimeout(…)` | `WithTimeout[A](d)` | discarded cancel leaks a timer until the deadline | | `func(ctx) … { return pipeline(ctx) }` wrappers | return the pipeline value itself | the Reader already *is* that function | | capturing `ctx` in a closure or struct field | supply it when running | the pipeline silently uses a stale context | | DB / client stored in the context | `Effect[Deps, A]` | untyped, unchecked, hard to test | | `pair.Unpack(logging.WithLogger(l)(ctx))` + `defer` | `RIO.Local[A](logging.WithLogger(l))` | already `Local`-shaped | | `context.Background()` inside library code or tests | the caller's context / `t.Context()` | breaks cancellation and deadlines | | blocking leaf that ignores `ctx.Done()` | `select` on `ctx.Done()` | timeouts and cancellation cannot take effect | ## Review Checklist - [ ] No `ctx.Value(...)` type assertions; values are read with `AskValue`. - [ ] Keys are unexported types; each key has a read and a write accessor. - [ ] Required values are turned into errors explicitly (`FromOption` / `ChainOptionK`), optional ones defaulted. - [ ] No `context.WithValue` / `WithTimeout` / `WithDeadline` / `WithCancel` inside or around pipelines — operators are used instead. - [ ] No discarded cancel functions. - [ ] The context is supplied once, at the edge; not captured or stored. - [ ] Blocking leaf computations observe `ctx.Done()`. - [ ] Dependencies live in `Effect[Deps, A]`, not in the context. - [ ] Tests run with `t.Context()` and cover the missing-value and timeout paths. ## Import Reference ```go import ( RIO "github.com/IBM/fp-go/v2/context/readerioresult" RR "github.com/IBM/fp-go/v2/context/readerresult" RIOC "github.com/IBM/fp-go/v2/context/readerio" SRIO "github.com/IBM/fp-go/v2/context/statereaderioresult" IRR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult" CR "github.com/IBM/fp-go/v2/context/reader" EF "github.com/IBM/fp-go/v2/effect" F "github.com/IBM/fp-go/v2/function" O "github.com/IBM/fp-go/v2/option" R "github.com/IBM/fp-go/v2/result" IO "github.com/IBM/fp-go/v2/io" LZ "github.com/IBM/fp-go/v2/lazy" RD "github.com/IBM/fp-go/v2/reader" HD "github.com/IBM/fp-go/v2/http/headers" LS "github.com/IBM/fp-go/v2/optics/lenses" "github.com/IBM/fp-go/v2/logging" ) ``` See also the `context` package documentation (`go doc github.com/IBM/fp-go/v2/context`) and the `fp-go`, `fp-go-logging` and `fp-go-http` skills.