mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-08 13:29:18 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
475d09e987 | ||
|
|
fd21bdeabf | ||
|
|
6834f72856 |
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIO.
|
||||
@@ -35,21 +36,24 @@ import (
|
||||
// The function f returns both a new context and a CancelFunc that should be called to release resources.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
// - A: The original result type produced by the ReaderIO
|
||||
// - B: The new output result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - f: Function to transform the input environment R into context.Context (contravariant)
|
||||
// - g: Function to transform the output value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[B]
|
||||
// - A Kleisli arrow that takes a ReaderIO[A] and returns a function from R to B
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context], g func(A) B) Operator[A, B] {
|
||||
func Promap[R, A, B any](f pair.Kleisli[context.CancelFunc, R, context.Context], g func(A) B) RIO.Kleisli[R, ReaderIO[A], B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
RIO.Map[R](g),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,15 +67,18 @@ func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, contex
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type (unchanged)
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[A]
|
||||
// - A Kleisli arrow that takes a ReaderIO[A] and returns a function from R to A
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
func Contramap[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIO.Kleisli[R, ReaderIO[A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -634,12 +634,15 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type of the ReaderIO
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms the context and returns a cancel function
|
||||
// - f: A function that transforms the input environment R into context.Context and returns a cancel function
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that runs the computation with the transformed context
|
||||
// - A Kleisli arrow that runs the computation with the transformed context
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -649,9 +652,9 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
// type key int
|
||||
// const userKey key = 0
|
||||
//
|
||||
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// addUser := readerio.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx := context.WithValue(ctx, userKey, "Alice")
|
||||
// return newCtx, func() {} // No-op cancel
|
||||
// return pair.MakePair(func() {}, newCtx) // No-op cancel
|
||||
// })
|
||||
//
|
||||
// getUser := readerio.FromReader(func(ctx context.Context) string {
|
||||
@@ -670,19 +673,20 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
// Timeout Example:
|
||||
//
|
||||
// // Add a 5-second timeout to a specific operation
|
||||
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// return context.WithTimeout(ctx, 5*time.Second)
|
||||
// withTimeout := readerio.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// fetchData,
|
||||
// withTimeout,
|
||||
// )
|
||||
func Local[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
return func(rr ReaderIO[A]) ReaderIO[A] {
|
||||
return func(ctx context.Context) IO[A] {
|
||||
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIO.Kleisli[R, ReaderIO[A], A] {
|
||||
return func(rr ReaderIO[A]) RIO.ReaderIO[R, A] {
|
||||
return func(r R) IO[A] {
|
||||
return func() A {
|
||||
otherCancel, otherCtx := pair.Unpack(f(ctx))
|
||||
otherCancel, otherCtx := pair.Unpack(f(r))
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
@@ -38,21 +39,24 @@ import (
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
// - A: The original success type produced by the ReaderIOResult
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - f: Function to transform the input environment R into context.Context (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[A] and returns a function from R to B
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context], g func(A) B) Operator[A, B] {
|
||||
func Promap[R, A, B any](f pair.Kleisli[context.CancelFunc, R, context.Context], g func(A) B) RIOR.Kleisli[R, ReaderIOResult[A], B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
RIOR.Map[R](g),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,15 +70,18 @@ func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, contex
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[A] and returns a function from R to A
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
func Contramap[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIOR.Kleisli[R, ReaderIOResult[A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1011,12 +1010,15 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type of the ReaderIOResult
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms the context and returns a cancel function
|
||||
// - f: A function that transforms the input environment R into context.Context and returns a cancel function
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that runs the computation with the transformed context
|
||||
// - A Kleisli arrow that runs the computation with the transformed context
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -1026,9 +1028,9 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// type key int
|
||||
// const userKey key = 0
|
||||
//
|
||||
// addUser := readerioresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// addUser := readerioresult.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx := context.WithValue(ctx, userKey, "Alice")
|
||||
// return newCtx, func() {} // No-op cancel
|
||||
// return pair.MakePair(func() {}, newCtx) // No-op cancel
|
||||
// })
|
||||
//
|
||||
// getUser := readerioresult.FromReader(func(ctx context.Context) string {
|
||||
@@ -1047,27 +1049,19 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// Timeout Example:
|
||||
//
|
||||
// // Add a 5-second timeout to a specific operation
|
||||
// withTimeout := readerioresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// return context.WithTimeout(ctx, 5*time.Second)
|
||||
// withTimeout := readerioresult.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// fetchData,
|
||||
// withTimeout,
|
||||
// )
|
||||
func Local[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOResult[A] {
|
||||
return func() Result[A] {
|
||||
if ctx.Err() != nil {
|
||||
return result.Left[A](context.Cause(ctx))
|
||||
}
|
||||
otherCancel, otherCtx := pair.Unpack(f(ctx))
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)()
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIOR.Kleisli[R, ReaderIOResult[A], A] {
|
||||
return readerio.Local[Result[A]](f)
|
||||
}
|
||||
|
||||
// WithTimeout adds a timeout to the context for a ReaderIOResult computation.
|
||||
|
||||
@@ -13,6 +13,17 @@ import (
|
||||
// Local modifies the outer environment before passing it to a computation.
|
||||
// Useful for providing different configurations to sub-computations.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.Local[context.Context, error, A](f)
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderResult.
|
||||
@@ -34,21 +36,24 @@ import (
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
// - A: The original success type produced by the ReaderResult
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - f: Function to transform the input environment R into context.Context (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[B]
|
||||
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to B
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
func Promap[R, A, B any](f pair.Kleisli[context.CancelFunc, R, context.Context], g func(A) B) RR.Kleisli[R, ReaderResult[A], B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
RR.Map[R](g),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,15 +67,18 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
|
||||
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to A
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
func Contramap[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RR.Kleisli[R, ReaderResult[A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
@@ -89,16 +97,19 @@ func Contramap[A any](f func(context.Context) (context.Context, context.CancelFu
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type (unchanged)
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
|
||||
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return func(rr ReaderResult[A]) ReaderResult[A] {
|
||||
return func(ctx context.Context) Result[A] {
|
||||
otherCtx, otherCancel := f(ctx)
|
||||
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to A
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RR.Kleisli[R, ReaderResult[A], A] {
|
||||
return func(rr ReaderResult[A]) RR.ReaderResult[R, A] {
|
||||
return func(r R) Result[A] {
|
||||
otherCancel, otherCtx := pair.Unpack(f(r))
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -34,9 +35,9 @@ func TestPromapBasic(t *testing.T) {
|
||||
return R.Of(0)
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
@@ -57,9 +58,9 @@ func TestContramapBasic(t *testing.T) {
|
||||
return R.Of(0)
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
@@ -79,9 +80,9 @@ func TestLocalBasic(t *testing.T) {
|
||||
return R.Of("unknown")
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addUser := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "user", "Alice")
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
|
||||
adapted := Local[string](addUser)(getValue)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/statet"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
SRIOE "github.com/IBM/fp-go/v2/statereaderioeither"
|
||||
)
|
||||
|
||||
// Left creates a StateReaderIOResult that represents a failed computation with the given error.
|
||||
@@ -215,24 +216,28 @@ func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] {
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - A: The result type
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a StateReaderIOResult[S, A] and returns a StateReaderIOResult[S, A]
|
||||
// - A Kleisli arrow that takes a StateReaderIOResult[S, A] and returns a StateReaderIOEither[S, R, error, A]
|
||||
//
|
||||
// Note: When R is context.Context, the return type simplifies to func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Add a timeout to a specific operation
|
||||
// withTimeout := statereaderioresult.Local[AppState, Data](
|
||||
// func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// return context.WithTimeout(ctx, 60*time.Second)
|
||||
// withTimeout := statereaderioresult.Local[AppState, Data, context.Context](
|
||||
// func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// },
|
||||
// )
|
||||
// result := withTimeout(computation)
|
||||
func Local[S, A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[S, A, A] {
|
||||
return func(ma StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
|
||||
func Local[S, A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) SRIOE.Kleisli[S, R, error, StateReaderIOResult[S, A], A] {
|
||||
return func(ma StateReaderIOResult[S, A]) SRIOE.StateReaderIOEither[S, R, error, A] {
|
||||
return function.Flow2(ma, RIORES.Local[Pair[S, A]](f))
|
||||
}
|
||||
}
|
||||
|
||||
318
v2/skills/fp-go-http/SKILL.md
Normal file
318
v2/skills/fp-go-http/SKILL.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# fp-go HTTP Requests
|
||||
|
||||
## Overview
|
||||
|
||||
fp-go wraps `net/http` in the `ReaderIOResult` monad, giving you composable, context-aware HTTP operations with automatic error propagation. The core package is:
|
||||
|
||||
```
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http
|
||||
```
|
||||
|
||||
All HTTP operations are lazy — they describe what to do but do not execute until you call the resulting function with a `context.Context`.
|
||||
|
||||
## Core Types
|
||||
|
||||
```go
|
||||
// Requester builds an *http.Request given a context.
|
||||
type Requester = ReaderIOResult[*http.Request] // func(context.Context) func() result.Result[*http.Request]
|
||||
|
||||
// Client executes a Requester and returns the response wrapped in ReaderIOResult.
|
||||
type Client interface {
|
||||
Do(Requester) ReaderIOResult[*http.Response]
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Create a Client
|
||||
|
||||
```go
|
||||
import (
|
||||
HTTP "net/http"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
)
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
|
||||
// Or with a custom client:
|
||||
custom := &HTTP.Client{Timeout: 10 * time.Second}
|
||||
client := H.MakeClient(custom)
|
||||
```
|
||||
|
||||
### 2. Build a Request
|
||||
|
||||
```go
|
||||
// GET request (most common)
|
||||
req := H.MakeGetRequest("https://api.example.com/users/1")
|
||||
|
||||
// Arbitrary method + body
|
||||
req := H.MakeRequest("POST", "https://api.example.com/users", bodyReader)
|
||||
```
|
||||
|
||||
### 3. Execute and Parse
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
|
||||
// ReadJSON validates status, Content-Type, then unmarshals JSON
|
||||
result := H.ReadJSON[User](client)(H.MakeGetRequest("https://api.example.com/users/1"))
|
||||
|
||||
// Execute — provide context once
|
||||
user, err := result(context.Background())()
|
||||
```
|
||||
|
||||
## Response Readers
|
||||
|
||||
All accept a `Client` and return a function `Requester → ReaderIOResult[A]`:
|
||||
|
||||
| Function | Returns | Notes |
|
||||
|----------|---------|-------|
|
||||
| `ReadJSON[A](client)` | `ReaderIOResult[A]` | Validates status + Content-Type, unmarshals JSON |
|
||||
| `ReadText(client)` | `ReaderIOResult[string]` | Validates status, reads body as UTF-8 string |
|
||||
| `ReadAll(client)` | `ReaderIOResult[[]byte]` | Validates status, returns raw body bytes |
|
||||
| `ReadFullResponse(client)` | `ReaderIOResult[FullResponse]` | Returns `Pair[*http.Response, []byte]` |
|
||||
|
||||
`FullResponse = Pair[*http.Response, []byte]` — use `pair.First` / `pair.Second` to access components.
|
||||
|
||||
## Composing Requests in Pipelines
|
||||
|
||||
```go
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
readPost := H.ReadJSON[Post](client)
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
H.MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1"),
|
||||
readPost,
|
||||
RIO.ChainFirstIOK(IO.Logf[Post]("Got post: %v")),
|
||||
)
|
||||
|
||||
post, err := pipeline(context.Background())()
|
||||
```
|
||||
|
||||
## Parallel Requests — Homogeneous Types
|
||||
|
||||
Use `RIO.TraverseArray` when all requests return the same type:
|
||||
|
||||
```go
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
type PostItem struct {
|
||||
UserID uint `json:"userId"`
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
readPost := H.ReadJSON[PostItem](client)
|
||||
|
||||
// Fetch 10 posts in parallel
|
||||
data := F.Pipe3(
|
||||
A.MakeBy(10, func(i int) string {
|
||||
return fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", i+1)
|
||||
}),
|
||||
RIO.TraverseArray(F.Flow3(
|
||||
H.MakeGetRequest,
|
||||
readPost,
|
||||
RIO.ChainFirstIOK(IO.Logf[PostItem]("Post: %v")),
|
||||
)),
|
||||
RIO.ChainFirstIOK(IO.Logf[[]PostItem]("All posts: %v")),
|
||||
RIO.Map(A.Size[PostItem]),
|
||||
)
|
||||
|
||||
count, err := data(context.Background())()
|
||||
```
|
||||
|
||||
## Parallel Requests — Heterogeneous Types
|
||||
|
||||
Use `RIO.TraverseTuple2` (or `Tuple3`, etc.) when requests return different types:
|
||||
|
||||
```go
|
||||
import (
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
type CatFact struct {
|
||||
Fact string `json:"fact"`
|
||||
}
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
readPost := H.ReadJSON[PostItem](client)
|
||||
readCatFact := H.ReadJSON[CatFact](client)
|
||||
|
||||
// Execute both requests in parallel with different response types
|
||||
data := F.Pipe3(
|
||||
T.MakeTuple2(
|
||||
"https://jsonplaceholder.typicode.com/posts/1",
|
||||
"https://catfact.ninja/fact",
|
||||
),
|
||||
T.Map2(H.MakeGetRequest, H.MakeGetRequest), // build both requesters
|
||||
RIO.TraverseTuple2(readPost, readCatFact), // run in parallel, typed
|
||||
RIO.ChainFirstIOK(IO.Logf[T.Tuple2[PostItem, CatFact]]("Result: %v")),
|
||||
)
|
||||
|
||||
both, err := data(context.Background())()
|
||||
// both.F1 is PostItem, both.F2 is CatFact
|
||||
```
|
||||
|
||||
## Building Requests with the Builder API
|
||||
|
||||
For complex requests (custom headers, query params, JSON body), use the builder:
|
||||
|
||||
```go
|
||||
import (
|
||||
B "github.com/IBM/fp-go/v2/http/builder"
|
||||
RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// GET with query parameters
|
||||
req := F.Pipe2(
|
||||
B.Default,
|
||||
B.WithURL("https://api.example.com/items?page=1"),
|
||||
B.WithQueryArg("limit")("50"),
|
||||
)
|
||||
requester := RB.Requester(req)
|
||||
|
||||
// POST with JSON body
|
||||
req := F.Pipe3(
|
||||
B.Default,
|
||||
B.WithURL("https://api.example.com/users"),
|
||||
B.WithMethod("POST"),
|
||||
B.WithJSON(map[string]string{"name": "Alice"}),
|
||||
// sets Content-Type: application/json automatically
|
||||
)
|
||||
requester := RB.Requester(req)
|
||||
|
||||
// With authentication and custom headers
|
||||
req := F.Pipe3(
|
||||
B.Default,
|
||||
B.WithURL("https://api.example.com/protected"),
|
||||
B.WithBearer("my-token"), // sets Authorization: Bearer my-token
|
||||
B.WithHeader("X-Request-ID")("123"),
|
||||
)
|
||||
requester := RB.Requester(req)
|
||||
|
||||
// Execute
|
||||
result := H.ReadJSON[Response](client)(requester)
|
||||
data, err := result(ctx)()
|
||||
```
|
||||
|
||||
### Builder Functions
|
||||
|
||||
| Function | Effect |
|
||||
|----------|--------|
|
||||
| `B.WithURL(url)` | Set the target URL |
|
||||
| `B.WithMethod(method)` | Set HTTP method (GET, POST, PUT, DELETE, …) |
|
||||
| `B.WithJSON(v)` | Marshal `v` as JSON body, set `Content-Type: application/json` |
|
||||
| `B.WithBytes(data)` | Set raw bytes body, set `Content-Length` automatically |
|
||||
| `B.WithHeader(key)(value)` | Add a request header |
|
||||
| `B.WithBearer(token)` | Set `Authorization: Bearer <token>` |
|
||||
| `B.WithQueryArg(key)(value)` | Append a query parameter |
|
||||
|
||||
## Error Handling
|
||||
|
||||
Errors from request creation, HTTP status codes, Content-Type validation, and JSON parsing all propagate automatically through the `Result` monad. You only handle errors at the call site:
|
||||
|
||||
```go
|
||||
// Pattern 1: direct extraction
|
||||
value, err := pipeline(ctx)()
|
||||
if err != nil { /* handle */ }
|
||||
|
||||
// Pattern 2: Fold for clean HTTP handler
|
||||
RIO.Fold(
|
||||
func(err error) { http.Error(w, err.Error(), http.StatusInternalServerError) },
|
||||
func(data MyType) { json.NewEncoder(w).Encode(data) },
|
||||
)(pipeline)(ctx)()
|
||||
```
|
||||
|
||||
## Full HTTP Handler Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
HTTP "net/http"
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
var client = H.MakeClient(HTTP.DefaultClient)
|
||||
|
||||
func fetchPost(id int) RIO.ReaderIOResult[Post] {
|
||||
url := fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", id)
|
||||
return F.Pipe2(
|
||||
H.MakeGetRequest(url),
|
||||
H.ReadJSON[Post](client),
|
||||
RIO.ChainFirstIOK(IO.Logf[Post]("fetched: %v")),
|
||||
)
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
RIO.Fold(
|
||||
func(err error) {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
},
|
||||
func(post Post) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(post)
|
||||
},
|
||||
)(fetchPost(1))(r.Context())()
|
||||
}
|
||||
```
|
||||
|
||||
## Import Reference
|
||||
|
||||
```go
|
||||
import (
|
||||
HTTP "net/http"
|
||||
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
|
||||
B "github.com/IBM/fp-go/v2/http/builder"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
```
|
||||
|
||||
Requires Go 1.24+.
|
||||
410
v2/skills/fp-go-logging/SKILL.md
Normal file
410
v2/skills/fp-go-logging/SKILL.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# fp-go Logging
|
||||
|
||||
## Overview
|
||||
|
||||
fp-go provides logging utilities that integrate naturally with functional pipelines. Logging is always a **side effect** — it should not change the value being processed. The library achieves this through `ChainFirst`-style combinators that thread the original value through unchanged while performing the log.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `github.com/IBM/fp-go/v2/logging` | Global logger, context-embedded logger, `LoggingCallbacks` |
|
||||
| `github.com/IBM/fp-go/v2/io` | `Logf`, `Logger`, `LogGo`, `Printf`, `PrintGo` — IO-level logging helpers |
|
||||
| `github.com/IBM/fp-go/v2/readerio` | `SLog`, `SLogWithCallback` — structured logging for ReaderIO |
|
||||
| `github.com/IBM/fp-go/v2/context/readerio` | `SLog`, `SLogWithCallback` — structured logging for context ReaderIO |
|
||||
| `github.com/IBM/fp-go/v2/context/readerresult` | `SLog`, `TapSLog`, `SLogWithCallback` — structured logging for ReaderResult |
|
||||
| `github.com/IBM/fp-go/v2/context/readerioresult` | `SLog`, `TapSLog`, `SLogWithCallback`, `LogEntryExit`, `LogEntryExitWithCallback` — full suite for ReaderIOResult |
|
||||
|
||||
## Logging Inside Pipelines
|
||||
|
||||
The idiomatic way to log inside a monadic pipeline is `ChainFirstIOK` (or `ChainFirst` where the monad is already IO). These combinators execute a side-effecting function and pass the **original value** downstream unchanged.
|
||||
|
||||
### With `IOResult` / `ReaderIOResult` — printf-style
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
fetchUser(42),
|
||||
RIO.ChainEitherK(validateUser),
|
||||
// Log after validation — value flows through unchanged
|
||||
RIO.ChainFirstIOK(IO.Logf[User]("Validated user: %v")),
|
||||
RIO.Map(enrichUser),
|
||||
)
|
||||
```
|
||||
|
||||
`IO.Logf[A](format string) func(A) IO[A]` logs using `log.Printf` and returns the value unchanged. It's a Kleisli arrow suitable for `ChainFirst` and `ChainFirstIOK`.
|
||||
|
||||
### With `IOEither` / plain `IO`
|
||||
|
||||
```go
|
||||
import (
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
file.ReadFile("config.json"),
|
||||
IOE.ChainEitherK(J.Unmarshal[Config]),
|
||||
IOE.ChainFirstIOK(IO.Logf[Config]("Loaded config: %v")),
|
||||
IOE.Map[error](processConfig),
|
||||
)
|
||||
```
|
||||
|
||||
### Logging Arrays in TraverseArray
|
||||
|
||||
```go
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Log each item individually, then log the final slice
|
||||
pipeline := F.Pipe2(
|
||||
A.MakeBy(3, idxToFilename),
|
||||
RIO.TraverseArray(F.Flow3(
|
||||
file.ReadFile,
|
||||
RIO.ChainEitherK(J.Unmarshal[Record]),
|
||||
RIO.ChainFirstIOK(IO.Logf[Record]("Parsed record: %v")),
|
||||
)),
|
||||
RIO.ChainFirstIOK(IO.Logf[[]Record]("All records: %v")),
|
||||
)
|
||||
```
|
||||
|
||||
## IO Logging Functions
|
||||
|
||||
All live in `github.com/IBM/fp-go/v2/io`:
|
||||
|
||||
### `Logf` — printf-style
|
||||
|
||||
```go
|
||||
IO.Logf[A any](format string) func(A) IO[A]
|
||||
```
|
||||
|
||||
Uses `log.Printf`. The format string works like `fmt.Sprintf`.
|
||||
|
||||
```go
|
||||
IO.Logf[User]("Processing user: %+v")
|
||||
IO.Logf[int]("Count: %d")
|
||||
```
|
||||
|
||||
### `Logger` — with custom `*log.Logger`
|
||||
|
||||
```go
|
||||
IO.Logger[A any](loggers ...*log.Logger) func(prefix string) func(A) IO[A]
|
||||
```
|
||||
|
||||
Uses `logger.Printf(prefix+": %v", value)`. Pass your own `*log.Logger` instance.
|
||||
|
||||
```go
|
||||
customLog := log.New(os.Stderr, "APP ", log.LstdFlags)
|
||||
logUser := IO.Logger[User](customLog)("user")
|
||||
// logs: "APP user: {ID:42 Name:Alice}"
|
||||
```
|
||||
|
||||
### `LogGo` — Go template syntax
|
||||
|
||||
```go
|
||||
IO.LogGo[A any](tmpl string) func(A) IO[A]
|
||||
```
|
||||
|
||||
Uses Go's `text/template`. The template receives the value as `.`.
|
||||
|
||||
```go
|
||||
type User struct{ Name string; Age int }
|
||||
IO.LogGo[User]("User {{.Name}} is {{.Age}} years old")
|
||||
```
|
||||
|
||||
### `Printf` / `PrintGo` — stdout instead of log
|
||||
|
||||
Same signatures as `Logf` / `LogGo` but use `fmt.Printf`/`fmt.Println` (no log prefix, no timestamp).
|
||||
|
||||
```go
|
||||
IO.Printf[Result]("Result: %v\n")
|
||||
IO.PrintGo[User]("Name: {{.Name}}")
|
||||
```
|
||||
|
||||
## Structured Logging in the `context` Package
|
||||
|
||||
The `context/readerioresult`, `context/readerresult`, and `context/readerio` packages provide structured `slog`-based logging functions that are context-aware: they retrieve the logger from the context (via `logging.GetLoggerFromContext`) rather than using a fixed logger instance.
|
||||
|
||||
### `TapSLog` — inline structured logging in a ReaderIOResult pipeline
|
||||
|
||||
`TapSLog` is an **Operator** (`func(ReaderIOResult[A]) ReaderIOResult[A]`). It sits directly in a `F.Pipe` call on a `ReaderIOResult`, logs the current value or error using `slog`, and passes the result through unchanged.
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe4(
|
||||
fetchOrder(orderID),
|
||||
RIO.TapSLog[Order]("Order fetched"), // logs value=<Order> or error=<err>
|
||||
RIO.Chain(validateOrder),
|
||||
RIO.TapSLog[Order]("Order validated"),
|
||||
RIO.Chain(processPayment),
|
||||
)
|
||||
|
||||
result, err := pipeline(ctx)()
|
||||
```
|
||||
|
||||
- Logs **both** success values (`value=<A>`) and errors (`error=<err>`) using `slog` structured attributes.
|
||||
- Respects the logger level — if the logger is configured to discard Info-level logs, nothing is written.
|
||||
- Available in both `context/readerioresult` and `context/readerresult`.
|
||||
|
||||
### `SLog` — Kleisli-style structured logging
|
||||
|
||||
`SLog` is a **Kleisli arrow** (`func(Result[A]) ReaderResult[A]` / `func(Result[A]) ReaderIOResult[A]`). It is used with `Chain` when you want to intercept the raw `Result` directly.
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
fetchData(id),
|
||||
RIO.Chain(RIO.SLog[Data]("Data fetched")), // log raw Result, pass it through
|
||||
RIO.Chain(validateData),
|
||||
RIO.Chain(RIO.SLog[Data]("Data validated")),
|
||||
RIO.Chain(processData),
|
||||
)
|
||||
```
|
||||
|
||||
**Difference from `TapSLog`:**
|
||||
- `TapSLog[A](msg)` is an `Operator[A, A]` — used directly in `F.Pipe` on a `ReaderIOResult[A]`.
|
||||
- `SLog[A](msg)` is a `Kleisli[Result[A], A]` — used with `Chain`, giving access to the raw `Result[A]`.
|
||||
|
||||
Both log in the same format. `TapSLog` is more ergonomic in most pipelines.
|
||||
|
||||
### `SLogWithCallback` — custom log level and logger source
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Log at DEBUG level with a custom logger extracted from context
|
||||
debugLog := RIO.SLogWithCallback[User](
|
||||
slog.LevelDebug,
|
||||
logging.GetLoggerFromContext, // or any func(context.Context) *slog.Logger
|
||||
"Fetched user",
|
||||
)
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
fetchUser(123),
|
||||
RIO.Chain(debugLog),
|
||||
RIO.Map(func(u User) string { return u.Name }),
|
||||
)
|
||||
```
|
||||
|
||||
### `LogEntryExit` — automatic entry/exit timing with correlation IDs
|
||||
|
||||
`LogEntryExit` wraps a `ReaderIOResult` computation with structured entry and exit log messages. It assigns a unique **correlation ID** (`ID=<n>`) to each invocation so concurrent or nested operations can be correlated in logs.
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
fetchUser(123),
|
||||
RIO.LogEntryExit[User]("fetchUser"), // wraps the operation
|
||||
RIO.Chain(func(user User) RIO.ReaderIOResult[[]Order] {
|
||||
return F.Pipe1(
|
||||
fetchOrders(user.ID),
|
||||
RIO.LogEntryExit[[]Order]("fetchOrders"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := pipeline(ctx)()
|
||||
// Logs:
|
||||
// level=INFO msg="[entering]" name=fetchUser ID=1
|
||||
// level=INFO msg="[exiting ]" name=fetchUser ID=1 duration=42ms
|
||||
// level=INFO msg="[entering]" name=fetchOrders ID=2
|
||||
// level=INFO msg="[exiting ]" name=fetchOrders ID=2 duration=18ms
|
||||
```
|
||||
|
||||
On error, the exit log changes to `[throwing]` and includes the error:
|
||||
|
||||
```
|
||||
level=INFO msg="[throwing]" name=fetchUser ID=3 duration=5ms error="user not found"
|
||||
```
|
||||
|
||||
Key properties:
|
||||
- **Correlation ID** (`ID=`) is unique per operation, monotonically increasing, and stored in the context so nested operations can access the parent's ID.
|
||||
- **Duration** (`duration=`) is measured from entry to exit.
|
||||
- **Logger is taken from the context** — embed a request-scoped logger with `logging.WithLogger` before executing the pipeline and `LogEntryExit` picks it up automatically.
|
||||
- **Level-aware** — if the logger does not have the log level enabled, the entire entry/exit instrumentation is skipped (zero overhead).
|
||||
- The original `ReaderIOResult[A]` value flows through **unchanged**.
|
||||
|
||||
```go
|
||||
// Use a context logger so all log messages carry request metadata
|
||||
cancelFn, ctxWithLogger := pair.Unpack(
|
||||
logging.WithLogger(
|
||||
slog.Default().With("requestID", r.Header.Get("X-Request-ID")),
|
||||
)(r.Context()),
|
||||
)
|
||||
defer cancelFn()
|
||||
|
||||
result, err := pipeline(ctxWithLogger)()
|
||||
```
|
||||
|
||||
### `LogEntryExitWithCallback` — custom log level
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Log at DEBUG level instead of INFO
|
||||
debugPipeline := F.Pipe1(
|
||||
expensiveComputation(),
|
||||
RIO.LogEntryExitWithCallback[Result](
|
||||
slog.LevelDebug,
|
||||
logging.GetLoggerFromContext,
|
||||
"expensiveComputation",
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### `SLog` / `SLogWithCallback` in `context/readerresult`
|
||||
|
||||
The same `SLog` and `TapSLog` functions are also available in `context/readerresult` for use with the synchronous `ReaderResult[A] = func(context.Context) (A, error)` monad:
|
||||
|
||||
```go
|
||||
import RR "github.com/IBM/fp-go/v2/context/readerresult"
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
queryDB(id),
|
||||
RR.TapSLog[Row]("Row fetched"),
|
||||
RR.Chain(parseRow),
|
||||
RR.TapSLog[Record]("Record parsed"),
|
||||
)
|
||||
```
|
||||
|
||||
## Global Logger (`logging` package)
|
||||
|
||||
The `logging` package manages a global `*slog.Logger` (structured logging, Go 1.21+).
|
||||
|
||||
```go
|
||||
import "github.com/IBM/fp-go/v2/logging"
|
||||
|
||||
// Get the current global logger (defaults to slog.Default())
|
||||
logger := logging.GetLogger()
|
||||
logger.Info("application started", "version", "1.0")
|
||||
|
||||
// Replace the global logger; returns the old one for deferred restore
|
||||
old := logging.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
defer logging.SetLogger(old)
|
||||
```
|
||||
|
||||
## Context-Embedded Logger
|
||||
|
||||
Embed a `*slog.Logger` in a `context.Context` to carry request-scoped loggers across the call stack. All context-package logging functions (`TapSLog`, `SLog`, `LogEntryExit`) pick up this logger automatically.
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Create a request-scoped logger
|
||||
reqLogger := slog.Default().With("requestID", "abc-123")
|
||||
|
||||
// Embed it into a context using the Kleisli arrow WithLogger
|
||||
cancelFn, ctxWithLogger := pair.Unpack(logging.WithLogger(reqLogger)(ctx))
|
||||
defer cancelFn()
|
||||
|
||||
// All downstream logging (TapSLog, LogEntryExit, etc.) uses reqLogger
|
||||
result, err := pipeline(ctxWithLogger)()
|
||||
```
|
||||
|
||||
`WithLogger` returns a `ContextCancel = Pair[context.CancelFunc, context.Context]`. The cancel function is a no-op — the context is only enriched, not made cancellable.
|
||||
|
||||
`GetLoggerFromContext` falls back to the global logger if no logger is found in the context.
|
||||
|
||||
## `LoggingCallbacks` — Dual-Logger Pattern
|
||||
|
||||
```go
|
||||
import "github.com/IBM/fp-go/v2/logging"
|
||||
|
||||
// Returns (infoCallback, errorCallback) — both are func(string, ...any)
|
||||
infoLog, errLog := logging.LoggingCallbacks() // use log.Default() for both
|
||||
infoLog, errLog := logging.LoggingCallbacks(myLogger) // same logger for both
|
||||
infoLog, errLog := logging.LoggingCallbacks(infoLog, errorLog) // separate loggers
|
||||
```
|
||||
|
||||
Used internally by `io.Logger` and by packages that need separate info/error sinks.
|
||||
|
||||
## Choosing the Right Logging Function
|
||||
|
||||
| Situation | Use |
|
||||
|-----------|-----|
|
||||
| Quick printf logging mid-pipeline | `IO.Logf[A]("fmt")` with `ChainFirstIOK` |
|
||||
| Go template formatting mid-pipeline | `IO.LogGo[A]("tmpl")` with `ChainFirstIOK` |
|
||||
| Print to stdout (no log prefix) | `IO.Printf[A]("fmt")` with `ChainFirstIOK` |
|
||||
| Structured slog — log value or error inline | `RIO.TapSLog[A]("msg")` (Operator, used in Pipe) |
|
||||
| Structured slog — intercept raw Result | `RIO.Chain(RIO.SLog[A]("msg"))` (Kleisli) |
|
||||
| Structured slog — custom log level | `RIO.SLogWithCallback[A](level, cb, "msg")` |
|
||||
| Entry/exit timing + correlation IDs | `RIO.LogEntryExit[A]("name")` |
|
||||
| Entry/exit at custom log level | `RIO.LogEntryExitWithCallback[A](level, cb, "name")` |
|
||||
| Structured logging globally | `logging.GetLogger()` / `logging.SetLogger()` |
|
||||
| Request-scoped logger in context | `logging.WithLogger(logger)` + `logging.GetLoggerFromContext(ctx)` |
|
||||
| Custom `*log.Logger` in pipeline | `IO.Logger[A](logger)("prefix")` with `ChainFirstIOK` |
|
||||
|
||||
## Complete Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure JSON structured logging globally
|
||||
L.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
|
||||
// Embed a request-scoped logger into the context
|
||||
_, ctx := P.Unpack(L.WithLogger(
|
||||
L.GetLogger().With("requestID", "req-001"),
|
||||
)(context.Background()))
|
||||
|
||||
pipeline := F.Pipe5(
|
||||
fetchData(42),
|
||||
RIO.LogEntryExit[Data]("fetchData"), // entry/exit with timing + ID
|
||||
RIO.TapSLog[Data]("raw data"), // inline structured value log
|
||||
RIO.ChainEitherK(transformData),
|
||||
RIO.LogEntryExit[Result]("transformData"),
|
||||
RIO.ChainFirstIOK(IO.LogGo[Result]("result: {{.Value}}")), // template log
|
||||
)
|
||||
|
||||
value, err := pipeline(ctx)()
|
||||
if err != nil {
|
||||
L.GetLogger().Error("pipeline failed", "error", err)
|
||||
}
|
||||
_ = value
|
||||
}
|
||||
```
|
||||
520
v2/skills/fp-go-monadic-operations/SKILL.md
Normal file
520
v2/skills/fp-go-monadic-operations/SKILL.md
Normal file
@@ -0,0 +1,520 @@
|
||||
# fp-go Monadic Operations
|
||||
|
||||
## 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
|
||||
|
||||
| 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]` — shorthand for the common case |
|
||||
| `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 |
|
||||
|
||||
Idiomatic (high-performance, tuple-based) equivalents live in `idiomatic/`:
|
||||
- `idiomatic/option` — `(A, bool)` tuples
|
||||
- `idiomatic/result` — `(A, error)` tuples
|
||||
- `idiomatic/ioresult` — `func() (A, error)`
|
||||
- `idiomatic/context/readerresult` — `func(context.Context) (A, error)`
|
||||
|
||||
## Standard Operations
|
||||
|
||||
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 |
|
||||
| `Alt` | `alt` / `<\|>` | Provide an alternative when the first computation fails |
|
||||
| `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:
|
||||
|
||||
```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.
|
||||
|
||||
## Examples
|
||||
|
||||
### Option — nullable values without nil
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
parseAndDouble := F.Flow2(
|
||||
O.FromPredicate(func(s string) bool { return s != "" }),
|
||||
O.Chain(func(s string) O.Option[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(n * 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"
|
||||
"strconv"
|
||||
"errors"
|
||||
)
|
||||
|
||||
parse := R.Eitherize1(strconv.Atoi) // lifts (int, error) → Result[int]
|
||||
|
||||
validate := func(n int) R.Result[int] {
|
||||
if n < 0 {
|
||||
return R.Error[int](errors.New("must be non-negative"))
|
||||
}
|
||||
return R.Of(n)
|
||||
}
|
||||
|
||||
pipeline := F.Flow2(parse, R.Chain(validate))
|
||||
|
||||
pipeline("42") // Ok(42)
|
||||
pipeline("-1") // Error("must be non-negative")
|
||||
pipeline("abc") // Error(strconv parse error)
|
||||
```
|
||||
|
||||
### IOResult — lazy IO with error handling
|
||||
|
||||
```go
|
||||
import (
|
||||
IOE "github.com/IBM/fp-go/v2/ioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
J "github.com/IBM/fp-go/v2/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
readConfig := F.Flow2(
|
||||
IOE.Eitherize1(os.ReadFile), // func(string) IOResult[[]byte]
|
||||
IOE.ChainEitherK(J.Unmarshal[Config]), // parse JSON, propagate errors
|
||||
)
|
||||
|
||||
result := readConfig("config.json")() // execute lazily
|
||||
```
|
||||
|
||||
### ReaderIOResult — context-aware pipelines (recommended for services)
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"context"
|
||||
)
|
||||
|
||||
// type ReaderIOResult[A any] = func(context.Context) func() result.Result[A]
|
||||
|
||||
fetchUser := func(id int) RIO.ReaderIOResult[User] {
|
||||
return func(ctx context.Context) func() result.Result[User] {
|
||||
return func() result.Result[User] {
|
||||
// perform IO here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
fetchUser(42),
|
||||
RIO.ChainEitherK(validateUser), // lift pure (User, error) function
|
||||
RIO.Map(enrichUser), // lift pure User → User function
|
||||
RIO.ChainFirstIOK(IO.Logf[User]("Fetched: %v")), // side-effect logging
|
||||
)
|
||||
|
||||
user, err := pipeline(ctx)() // provide context once, execute
|
||||
```
|
||||
|
||||
### 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]
|
||||
)
|
||||
```
|
||||
|
||||
## Function Composition with Flow and Pipe
|
||||
|
||||
```go
|
||||
import F "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Flow: compose functions left-to-right, returns a new function
|
||||
transform := F.Flow3(
|
||||
option.Map(strings.TrimSpace),
|
||||
option.Filter(func(s string) bool { return s != "" }),
|
||||
option.GetOrElse(func() string { return "default" }),
|
||||
)
|
||||
result := transform(option.Some(" hello ")) // "hello"
|
||||
|
||||
// Pipe: apply a value through a pipeline immediately
|
||||
result := F.Pipe3(
|
||||
option.Some(" hello "),
|
||||
option.Map(strings.TrimSpace),
|
||||
option.Filter(func(s string) bool { return s != "" }),
|
||||
option.GetOrElse(func() string { return "default" }),
|
||||
)
|
||||
```
|
||||
|
||||
## Lifting Pure Functions into Monadic Context
|
||||
|
||||
fp-go provides helpers to promote non-monadic functions:
|
||||
|
||||
| Helper | Lifts |
|
||||
|--------|-------|
|
||||
| `ChainEitherK` | `func(A) (B, error)` → works inside the monad |
|
||||
| `ChainOptionK` | `func(A) Option[B]` → works inside the monad |
|
||||
| `ChainFirstIOK` | `func(A) IO[B]` for side effects, keeps original value |
|
||||
| `Eitherize1..N` | `func(A) (B, error)` → `func(A) Result[B]` |
|
||||
| `FromPredicate` | `func(A) bool` + error builder → `func(A) Result[A]` |
|
||||
|
||||
## Type Parameter Ordering Rule (V2)
|
||||
|
||||
Non-inferrable type parameters come **first**, so the compiler can infer the rest:
|
||||
|
||||
```go
|
||||
// B cannot be inferred from the argument — it comes first
|
||||
result := either.Ap[string](value)(funcInEither)
|
||||
|
||||
// All types inferrable — no explicit params needed
|
||||
result := either.Map(transform)(value)
|
||||
result := either.Chain(validator)(value)
|
||||
```
|
||||
|
||||
## When to Use Which Monad
|
||||
|
||||
| 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 | `Effect[C, A]` |
|
||||
| High-performance services | Idiomatic packages in `idiomatic/` |
|
||||
|
||||
## Do-Notation: Accumulating State with `Bind` and `ApS`
|
||||
|
||||
When a pipeline needs to carry **multiple intermediate results** forward — not just a single value — the `Chain`/`Map` style becomes unwieldy because each step only threads one value and prior results are lost. Do-notation solves this by accumulating results into a growing struct (the "state") at each step.
|
||||
|
||||
Every monad that supports do-notation exports the same family of functions. The examples below use `context/readerioresult` (`RIO`), but the identical API is available in `result`, `option`, `ioresult`, `readerioresult`, and others.
|
||||
|
||||
### The Function Family
|
||||
|
||||
| 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, integrating naturally with the optics system.
|
||||
|
||||
### `Bind` — Sequential, Dependent Steps
|
||||
|
||||
`Bind` sequences two monadic computations. The function `f` receives the **full accumulated state** so it can read anything gathered so far. Errors short-circuit automatically.
|
||||
|
||||
```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"
|
||||
"context"
|
||||
)
|
||||
|
||||
type Pipeline struct {
|
||||
User User
|
||||
Config Config
|
||||
Posts []Post
|
||||
}
|
||||
|
||||
// Lenses — focus on individual fields; .Set is already func(T) func(S) S
|
||||
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 })
|
||||
)
|
||||
|
||||
result := F.Pipe3(
|
||||
RIO.Do(Pipeline{}), // lift empty struct
|
||||
RIO.Bind(userLens.Set, func(_ Pipeline) RIO.ReaderIOResult[User] { return fetchUser(42) }),
|
||||
RIO.Bind(configLens.Set, F.Flow2(userLens.Get, fetchConfigForUser)), // read s.User, pass to fetcher
|
||||
RIO.Bind(postsLens.Set, F.Flow2(userLens.Get, fetchPostsForUser)), // read s.User, pass to fetcher
|
||||
)
|
||||
|
||||
pipeline, err := result(context.Background())()
|
||||
// pipeline.User, pipeline.Config, pipeline.Posts are all populated
|
||||
```
|
||||
|
||||
The setter signature is `func(T) func(S1) S2` — it takes the new value and returns a state transformer. `lens.Set` already has this shape, so no manual setter functions are needed. `F.Flow2(lens.Get, f)` composes the field getter with any Kleisli arrow `f` point-free.
|
||||
|
||||
### `ApS` — Independent, Applicative Steps
|
||||
|
||||
`ApS` uses **applicative** semantics: `fa` is evaluated without any access to the current state. Use it when steps have no dependency on each other — the library can choose to execute them concurrently.
|
||||
|
||||
```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"
|
||||
)
|
||||
|
||||
type Summary struct {
|
||||
User User
|
||||
Weather Weather
|
||||
}
|
||||
|
||||
var (
|
||||
userLens = L.MakeLens(func(s Summary) User { return s.User }, func(s Summary, u User) Summary { s.User = u; return s })
|
||||
weatherLens = L.MakeLens(func(s Summary) Weather { return s.Weather }, func(s Summary, w Weather) Summary { s.Weather = w; return s })
|
||||
)
|
||||
|
||||
// Both are independent — neither needs the other's result
|
||||
result := F.Pipe2(
|
||||
RIO.Do(Summary{}),
|
||||
RIO.ApS(userLens.Set, fetchUser(42)),
|
||||
RIO.ApS(weatherLens.Set, fetchWeather("NYC")),
|
||||
)
|
||||
```
|
||||
|
||||
**Key difference from `Bind`:**
|
||||
|
||||
| | `Bind(setter, f)` | `ApS(setter, fa)` |
|
||||
|-|---|---|
|
||||
| Second argument | `func(S1) M[T]` — a **function** of state | `M[T]` — a **fixed** monadic value |
|
||||
| Can read prior state? | Yes — receives `S1` | No — no access to state |
|
||||
| Semantics | Monadic (sequential) | Applicative (independent) |
|
||||
|
||||
### `Let` and `LetTo` — Pure Additions
|
||||
|
||||
`Let` adds a value computed by a **pure function** of the current state (no monad, cannot fail):
|
||||
|
||||
```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"
|
||||
)
|
||||
|
||||
type Enriched struct {
|
||||
User User
|
||||
FullName string
|
||||
}
|
||||
|
||||
var (
|
||||
userLens = L.MakeLens(func(s Enriched) User { return s.User }, func(s Enriched, u User) Enriched { s.User = u; return s })
|
||||
fullNameLens = L.MakeLens(func(s Enriched) string { return s.FullName }, func(s Enriched, n string) Enriched { s.FullName = n; return s })
|
||||
)
|
||||
|
||||
fullName := func(u User) string { return u.FirstName + " " + u.LastName }
|
||||
|
||||
result := F.Pipe2(
|
||||
RIO.Do(Enriched{}),
|
||||
RIO.Bind(userLens.Set, func(_ Enriched) RIO.ReaderIOResult[User] { return fetchUser(42) }),
|
||||
RIO.Let(fullNameLens.Set, F.Flow2(userLens.Get, fullName)), // read s.User, compute pure string
|
||||
)
|
||||
```
|
||||
|
||||
`LetTo` adds a **constant** with no computation:
|
||||
|
||||
```go
|
||||
RIO.LetTo(setVersion, "v1.2.3")
|
||||
```
|
||||
|
||||
### `BindTo` — Starting from an Existing Value
|
||||
|
||||
When you have an existing `M[T]` and want to project it into a state struct rather than starting from `Do(empty)`:
|
||||
|
||||
```go
|
||||
type State struct{ User User }
|
||||
|
||||
result := F.Pipe1(
|
||||
fetchUser(42), // ReaderIOResult[User]
|
||||
RIO.BindTo(func(u User) State { return State{User: u} }),// ReaderIOResult[State]
|
||||
)
|
||||
```
|
||||
|
||||
### Lens Variants (`ApSL`, `BindL`, `LetL`, `LetToL`)
|
||||
|
||||
If you have a `Lens[S, T]` (from the optics system or code generation), you can skip writing the setter function entirely:
|
||||
|
||||
```go
|
||||
import (
|
||||
RO "github.com/IBM/fp-go/v2/readeroption"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Lenses generated by go:generate (see optics/README.md)
|
||||
// personLenses.Name : Lens[*Person, Name]
|
||||
// personLenses.Age : Lens[*Person, Age]
|
||||
|
||||
makePerson := F.Pipe2(
|
||||
RO.Do[*PartialPerson](emptyPerson),
|
||||
RO.ApSL(personLenses.Name, maybeName), // replaces: ApS(personLenses.Name.Set, maybeName)
|
||||
RO.ApSL(personLenses.Age, maybeAge),
|
||||
)
|
||||
```
|
||||
|
||||
This exact pattern is used in [`samples/builder`](samples/builder/builder.go) to validate and construct a `Person` from an unvalidated `PartialPerson`.
|
||||
|
||||
### Lifted Variants for Mixed Monads
|
||||
|
||||
`context/readerioresult` provides `Bind*K` helpers that lift simpler computations directly into the do-chain:
|
||||
|
||||
| Helper | Lifts |
|
||||
|--------|-------|
|
||||
| `BindResultK` / `BindEitherK` | `func(S1) (T, error)` — pure result |
|
||||
| `BindIOResultK` / `BindIOEitherK` | `func(S1) func() (T, error)` — lazy IO result |
|
||||
| `BindIOK` | `func(S1) func() T` — infallible IO |
|
||||
| `BindReaderK` | `func(S1) func(ctx) T` — context reader |
|
||||
|
||||
```go
|
||||
RIO.BindResultK(setUser, func(s Pipeline) (User, error) {
|
||||
return validateAndBuild(s) // plain (value, error) function, no wrapping needed
|
||||
})
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
The same pattern works with simpler monads. Here with `result.Result[A]`:
|
||||
|
||||
`Eitherize1` converts any standard `func(A) (B, error)` into `func(A) Result[B]`. Define these lifted functions once as variables. Then use lenses to focus on individual struct fields and compose with `F.Flow2(lens.Get, f)` — no inline lambdas, no manual error handling.
|
||||
|
||||
```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
|
||||
}
|
||||
|
||||
// Lenses — focus on individual fields of Parsed.
|
||||
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 },
|
||||
)
|
||||
)
|
||||
|
||||
// Lifted functions — convert standard (value, error) functions into Result-returning ones.
|
||||
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), // set Raw to constant input
|
||||
R.Bind(numberLens.Set, F.Flow2(rawLens.Get, atoi)), // get Raw, parse → Result[int]
|
||||
R.Let(doubleLens.Set, F.Flow2(numberLens.Get, N.Mul(2))), // get Number, multiply → int
|
||||
)
|
||||
}
|
||||
|
||||
parse("21") // Ok(Parsed{Raw:"21", Number:21, Double:42})
|
||||
parse("abc") // Error(strconv parse error)
|
||||
```
|
||||
|
||||
`rawLens.Set` is already `func(string) func(Parsed) Parsed`, matching the setter signature `Bind` and `LetTo` expect — no manual setter functions to write. `F.Flow2(rawLens.Get, atoi)` composes the field getter with the eitherized parse function into a `Kleisli[Parsed, int]` without any intermediate lambda.
|
||||
|
||||
## Import Paths
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/effect"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
)
|
||||
```
|
||||
|
||||
Requires Go 1.24+ (generic type aliases).
|
||||
Reference in New Issue
Block a user