--- name: fp-go-pattern-matching description: >- Use this skill when writing, refactoring, or reviewing fp-go v2 code that replaces switch/case, if-else chains, or type switches with composable, point-free pattern matching: first-match-wins case lists, routers, classifiers, multi-format parsers, fallback chains, or matching over sum types and slices. Trigger on any mention of pattern matching in Go, match/case, switch to functional, AltAllArray, AltAllSeq, AltMonoid, readeroption.AltMonoid, readeroption.Alt, FindFirstMap, FindLastMap, FilterMap, option.Alt, first Some, fallback chain, InstanceOf, prism GetOption as matcher, GetOrElse default branch, option.Fold, either.Fold, or array.Match. --- # fp-go Pattern Matching All imports come from `github.com/IBM/fp-go/v2` and use the canonical aliases of the **fp-go** skill. The canonical long-form reference is `v2/docs/PATTERN_MATCHING.md`. Tested examples are in `v2/array/example_pattern_matching_test.go` and `v2/array/pattern_matching_test.go`. ```go import ( A "github.com/IBM/fp-go/v2/array" F "github.com/IBM/fp-go/v2/function" N "github.com/IBM/fp-go/v2/number" O "github.com/IBM/fp-go/v2/option" P "github.com/IBM/fp-go/v2/predicate" R "github.com/IBM/fp-go/v2/result" RD "github.com/IBM/fp-go/v2/reader" RO "github.com/IBM/fp-go/v2/readeroption" S "github.com/IBM/fp-go/v2/string" "github.com/IBM/fp-go/v2/optics/prism" "github.com/IBM/fp-go/v2/readerresult" // generic ReaderResult[T, B]; not context/readerresult ) ``` --- ## 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. ## The Model - A **case** is `func(T) Option[B]`: `Some(b)` if it matches, `None` otherwise. `RO.ReaderOption[T, B]`, `O.Kleisli[T, B]` and `prism.Prism[T, B].GetOption` are all aliases of this type, so values from these packages mix freely in one slice. - A **match** is an ordered slice of cases, and the first `Some` wins. - A **default** turns the partial match (`Option[B]`) into a total function. - **Everything is point-free.** Cases are composed from guards, getters and formatters with `Flow`/`Pipe`. Lambdas are allowed only for leaves (field accessors, multi-field formatters, deliberate side effects in tests). ## The Canonical Idiom (Use This by Default) ```go func getMethod(r Request) string { return r.Method } // leaf accessor (or lens.Get) func getPath(r Request) string { return r.Path } func on(guard P.Predicate[Request], branch func(Request) string) RO.ReaderOption[Request, string] { return F.Flow2(O.FromPredicate(guard), O.Map(branch)) } func isMethod(m string) P.Predicate[Request] { return F.Flow2(getMethod, P.IsStrictEqual[string]()(m)) } func HandleRequest() func(Request) string { return F.Pipe2( A.From( // ordered, specific -> general on(isMethod("GET"), F.Flow2(getPath, S.Format[string]("Fetching: %s"))), on(isMethod("DELETE"), F.Flow2(getPath, S.Format[string]("Deleting: %s"))), ), A.Fold(RO.AltMonoid[Request, string]()), // lazy first-match RO.GetOrElse(F.Flow2(getMethod, S.Format[string]("Unsupported: %s"))), // default ) } ``` - **Lazy:** stops at the first match. Later cases are never invoked. - Building the matcher runs no case. Wrap it in a constructor `func` (never a package-level `var`), call the constructor once during setup, and reuse the result. - The folded match is itself a case, so matches nest. - Zero cases give `Empty()`, which is `None` for all inputs. Generic helper for the most common case shape: ```go func when[T, B any](guard P.Predicate[T], b B) RO.ReaderOption[T, B] { return F.Flow2(O.FromPredicate(guard), O.Map(F.Constant1[T](b))) } ``` Alternatives, all lazy and point-free: ```go // append one or two fallback cases F.Pipe2(c1, RO.Alt(F.Constant(c2)), RO.Alt(F.Constant(c3))) // FindFirstMap over a slice of cases F.Flow3( RD.Read[O.Option[B], T], // x -> apply-a-case-to-x A.FindFirstMap[RO.ReaderOption[T, B], B], // -> first case that matches RD.Read[O.Option[B]](cases), // run over the cases ) ``` ## Point-Free Building Blocks | Instead of | Write | |---|---| | `func(r Req) bool { return r.Method == m }` | `F.Flow2(getMethod, P.IsStrictEqual[string]()(m))` | | `func(e Ev) bool { return e.Priority > 8 }` | `F.Flow2(getPriority, N.MoreThan(8))` | | `p(x) && q(x)` | `F.Pipe1(p, P.And(q))`, also `P.Or`, `P.Not` | | `strings.HasPrefix(s, p)` | `F.Bind2nd(strings.HasPrefix, p)` | | `strings.TrimPrefix(s, p)` | `F.Bind2nd(strings.TrimPrefix, p)` | | `fmt.Sprintf("x %v", v)` | `S.Format[T]("x %v")` | | `"INFO: " + s` | `S.Prepend("INFO: ")`, `S.Append` | | `func(T) B { return b }` | `F.Constant1[T](b)` | | `strconv.ParseInt(s, base, 64)` | `F.Bind23of3(R.Eitherize3(strconv.ParseInt))(base, 64)` | | `strconv.Atoi(s)` | `R.Eitherize1(strconv.Atoi)` | | `Result` -> `Option` | `R.ToOption[A]` | | `func() Option[B] { return c(x) }` | `F.Nullary2(F.Constant(x), c)` | | `func(s Shape) any { return s }` | `F.ToAny[Shape]` | | replace any error | `R.MapLeft[A](F.Constant1[error](err))` | (`number` has **no** `Equal`/`LessThanEqual`. Use `P.IsStrictEqual`, `P.IsZero[T]()`, `N.LessThan(n+1)`.) ## Critical Pitfall: AltAllArray over Applied Cases Is Eager ```go // BAD: all three cases run, because Go evaluates the slice literal first O.AltAllArray(O.None[B]())([]O.Option[B]{c1(x), c2(x), c3(x)}) // its point-free equivalent shows why: SequenceArray runs every case first F.Pipe2(cases, RD.SequenceArray[T, O.Option[B]], RD.Map[T](O.AltAllArray(O.None[B]()))) ``` `AltAllArray` stops *scanning* at the first `Some` but cannot prevent the calls that built the slice. Use it only when the `Option` values already exist. When reviewing code, rewrite this form to `A.Fold(RO.AltMonoid[T, B]())(A.From(c1, c2, c3))`. The same applies to `either.AltAllArray` and `result.AltAllArray`. ## Building Cases | Need | Build with | |---|---| | guard -> constant | `when(guard, b)` = `F.Flow2(O.FromPredicate(guard), O.Map(F.Constant1[T](b)))` | | guard -> computed | `F.Flow2(O.FromPredicate(guard), O.Map(f))` | | guard -> transform -> partial parse | `F.Flow3(O.FromPredicate(g), O.Map(f), O.Chain(k))` | | `func(A) (B, bool)` | `O.FromValidation(f)`, for example `O.FromValidation(os.LookupEnv)` | | type switch on `any` | `F.Flow2(O.InstanceOf[Variant], O.Map(handler))` | | type switch on named interface | `F.Flow3(F.ToAny[Shape], O.InstanceOf[Variant], O.Map(handler))` | | parse / extract | `prism.ParseInt().GetOption`, `ParseInt64`, `ParseFloat64`, `ParseBool`, `ParseDate(layout)`, `RegexMatcher(re)` | | pointer non-nil | `O.FromNillable2` | | catch-all | `RO.Of[T](b)` as the **last** case | | no combinator fits | small **named** `func(T) O.Option[B]` leaf | Prefix-parse case, fully point-free: ```go func withPrefix(prefix string, base int) RO.ReaderOption[string, int64] { return F.Flow3( O.FromPredicate(F.Bind2nd(strings.HasPrefix, prefix)), O.Map(F.Bind2nd(strings.TrimPrefix, prefix)), O.Chain(F.Flow2(F.Bind23of3(R.Eitherize3(strconv.ParseInt))(base, 64), R.ToOption[int64])), ) } ``` Sum-type cases via an adapter: ```go func caseOf[T Shape, B any](f func(T) B) RO.ReaderOption[Shape, B] { return F.Flow3(F.ToAny[Shape], O.InstanceOf[T], O.Map(f)) } describe := A.Fold(RO.AltMonoid[Shape, string]())(A.From( caseOf(S.Format[Circle]("circle %v")), caseOf(S.Format[Rect]("rect %v")), )) ``` Go has no exhaustiveness checking. A default guarantees a result, not coverage. Add one test per variant. ## Closing the Match ```go F.Pipe1(match, RO.GetOrElse(F.Flow2(getX, S.Format[X]("no match: %v")))) // default sees input F.Pipe1(match, RO.GetOrElse(F.Constant1[T](dflt))) // constant default A.From(c1, c2, RO.Of[T](dflt)) // catch-all case, still Option ``` ## Two-Way Matches: Use the Eliminator, Not a Case List | Input | Use | |---|---| | `Option[A]` | `O.Fold(onNone func() B, onSome func(A) B)`, e.g. `O.Fold(F.Constant("none"), S.Format[int]("got %d"))` | | `Either[E, A]` | `either.Fold(onLeft, onRight)` | | `Result[A]` | `R.Fold(onErr func(error) B, onOk func(A) B)` | | predicate | `P.Fold(onFalse, onTrue func(A) B)(pred)` | | slice empty/non-empty | `A.Match(onEmpty func() B, onNonEmpty func([]A) B)`, `A.MatchLeft(onEmpty, func(head A, tail []A) B)` | ## Matching over Slices | Goal | Use | |---|---| | first element matching any case | `A.FindFirstMap(match)` (lazy over elements) | | last element | `A.FindLastMap(match)` | | with index | `A.FindFirstMapWithIndex(func(int, A) O.Option[B])` | | all matches | `A.FilterMap(match)` -> `[]B` | ## Matching with Errors When a failing case should say why, use `Result` cases and the error-aware monoid: ```go func withPrefixR(prefix string, base int, err error) readerresult.ReaderResult[string, int64] { return F.Flow4( R.FromPredicate(F.Bind2nd(strings.HasPrefix, prefix), F.Constant1[string](err)), R.Map(F.Bind2nd(strings.TrimPrefix, prefix)), R.Chain(F.Bind23of3(R.Eitherize3(strconv.ParseInt))(base, 64)), R.MapLeft[int64](F.Constant1[error](err)), ) } parse := A.Fold(readerresult.AltMonoid(F.Constant(readerresult.Left[string, int64](errNoMatch))))(A.From( withPrefixR("0x", 16, errHex), withPrefixR("", 10, errDec), )) // two alternatives: F.Pipe1(c1, readerresult.Alt(F.Constant(c2))) ``` - Lazy. If all cases fail, the **last** case's error is returned. With no cases, `zero` is returned. - If the cases are declared as plain `func(string) R.Result[int64]`, `A.From` may need an explicit type argument: `A.From[readerresult.ReaderResult[string, int64]](...)`. - `readereither.AltMonoid(zero)` is the equivalent for custom error types. ## Review Checklist 1. Cases are ordered specific -> general (a general case first shadows later ones). 2. No `AltAllArray([]Option{c(x), ...})` over case applications. Rewrite to `A.Fold(RO.AltMonoid...)`. 3. Point-free: no lambdas that only compare a field, call one stdlib function, format a value or return a constant. Replace them using the building-blocks table. 4. The matcher is built by a constructor `func`, once, not inside a loop or per request. 5. Intended total functions end in `RO.GetOrElse`, so `Option` does not leak. 6. Cases are pure. If a branch needs IO, return the effect (`Option[IOResult[B]]`) instead of running it in the guard. 7. Each case has a unit test, plus a test of the combined match, including the default and priority. 8. Fixed, local, non-reused branching stays a plain `switch`. Do not force this pattern where it adds nothing.