1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-26 13:06:09 +02:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
61b948425b fix: cleaner use of Kleisli
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-11 16:24:11 +01:00
Dr. Carsten Leue
a276f3acff fix: add llms.txt
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 09:48:19 +01:00
Dr. Carsten Leue
8c656a4297 fix: more Alt tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 08:52:39 +01:00
10 changed files with 1383 additions and 22 deletions

99
v2/llms.txt Normal file
View File

@@ -0,0 +1,99 @@
# fp-go
> A comprehensive functional programming library for Go, bringing type-safe monads, functors, applicatives, optics, and composable abstractions inspired by fp-ts and Haskell to the Go ecosystem. Created by IBM, licensed under Apache-2.0.
fp-go v2 requires Go 1.24+ and leverages generic type aliases for a cleaner API.
Key concepts: `Option` for nullable values, `Either`/`Result` for error handling, `IO` for lazy side effects, `Reader` for dependency injection, `IOResult` for effectful error handling, `ReaderIOResult` for the full monad stack, and `Optics` (lens, prism, traversal, iso) for immutable data manipulation.
## Core Documentation
- [API Reference (pkg.go.dev)](https://pkg.go.dev/github.com/IBM/fp-go/v2): Complete API documentation for all packages
- [README](https://github.com/IBM/fp-go/blob/main/v2/README.md): Overview, quick start, installation, and migration guide from v1 to v2
- [Design Decisions](https://github.com/IBM/fp-go/blob/main/v2/DESIGN.md): Key design principles and patterns
- [Functional I/O Guide](https://github.com/IBM/fp-go/blob/main/v2/FUNCTIONAL_IO.md): Understanding Context, errors, and the Reader pattern for I/O operations
- [Idiomatic vs Standard Comparison](https://github.com/IBM/fp-go/blob/main/v2/IDIOMATIC_COMPARISON.md): Performance comparison and when to use each approach
- [Optics README](https://github.com/IBM/fp-go/blob/main/v2/optics/README.md): Guide to lens, prism, optional, and traversal optics
## Standard Packages (struct-based)
- [option](https://pkg.go.dev/github.com/IBM/fp-go/v2/option): Option monad — represent optional values without nil
- [either](https://pkg.go.dev/github.com/IBM/fp-go/v2/either): Either monad — type-safe error handling with Left/Right values
- [result](https://pkg.go.dev/github.com/IBM/fp-go/v2/result): Result monad — simplified Either with `error` as Left type (recommended for error handling)
- [io](https://pkg.go.dev/github.com/IBM/fp-go/v2/io): IO monad — lazy evaluation and side effect management
- [iooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/iooption): IOOption — IO combined with Option
- [ioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioeither): IOEither — IO combined with Either for effectful error handling
- [ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioresult): IOResult — IO combined with Result (recommended over IOEither)
- [reader](https://pkg.go.dev/github.com/IBM/fp-go/v2/reader): Reader monad — dependency injection pattern
- [readeroption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeroption): ReaderOption — Reader combined with Option
- [readeriooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeriooption): ReaderIOOption — Reader + IO + Option
- [readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioresult): ReaderIOResult — Reader + IO + Result for complex workflows
- [readerioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioeither): ReaderIOEither — Reader + IO + Either
- [statereaderioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/statereaderioeither): StateReaderIOEither — State + Reader + IO + Either
## Idiomatic Packages (tuple-based, high performance)
- [idiomatic/option](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/option): Option using native Go `(value, bool)` tuples
- [idiomatic/result](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/result): Result using native Go `(value, error)` tuples
- [idiomatic/ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/ioresult): IOResult using `func() (value, error)`
- [idiomatic/readerresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerresult): ReaderResult with tuple-based results
- [idiomatic/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerioresult): ReaderIOResult with tuple-based results
## Context Packages (context.Context specializations)
- [context/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult): ReaderIOResult specialized for context.Context
- [context/readerioresult/http](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http): Functional HTTP client utilities
- [context/readerioresult/http/builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http/builder): Functional HTTP request builder
- [context/statereaderioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/statereaderioresult): State + Reader + IO + Result for context.Context
## Optics
- [optics](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics): Core optics package
- [optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens): Lenses for focusing on fields in product types
- [optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism): Prisms for focusing on variants in sum types
- [optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso): Isomorphisms for bidirectional transformations
- [optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional): Optionals for values that may not exist
- [optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal): Traversals for focusing on multiple values
- [optics/codec](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/codec): Codecs for encoding/decoding with validation
## Utility Packages
- [array](https://pkg.go.dev/github.com/IBM/fp-go/v2/array): Functional array/slice operations (map, filter, fold, etc.)
- [record](https://pkg.go.dev/github.com/IBM/fp-go/v2/record): Functional operations for maps
- [function](https://pkg.go.dev/github.com/IBM/fp-go/v2/function): Function composition, pipe, flow, curry, identity
- [pair](https://pkg.go.dev/github.com/IBM/fp-go/v2/pair): Strongly-typed pair/tuple data structure
- [tuple](https://pkg.go.dev/github.com/IBM/fp-go/v2/tuple): Type-safe heterogeneous tuples
- [predicate](https://pkg.go.dev/github.com/IBM/fp-go/v2/predicate): Predicate combinators (and, or, not, etc.)
- [endomorphism](https://pkg.go.dev/github.com/IBM/fp-go/v2/endomorphism): Endomorphism operations (compose, chain)
- [eq](https://pkg.go.dev/github.com/IBM/fp-go/v2/eq): Type-safe equality comparisons
- [ord](https://pkg.go.dev/github.com/IBM/fp-go/v2/ord): Total ordering type class
- [semigroup](https://pkg.go.dev/github.com/IBM/fp-go/v2/semigroup): Semigroup algebraic structure
- [monoid](https://pkg.go.dev/github.com/IBM/fp-go/v2/monoid): Monoid algebraic structure
- [number](https://pkg.go.dev/github.com/IBM/fp-go/v2/number): Algebraic structures for numeric types
- [string](https://pkg.go.dev/github.com/IBM/fp-go/v2/string): Functional string utilities
- [boolean](https://pkg.go.dev/github.com/IBM/fp-go/v2/boolean): Functional boolean utilities
- [bytes](https://pkg.go.dev/github.com/IBM/fp-go/v2/bytes): Functional byte slice utilities
- [json](https://pkg.go.dev/github.com/IBM/fp-go/v2/json): Functional JSON encoding/decoding
- [lazy](https://pkg.go.dev/github.com/IBM/fp-go/v2/lazy): Lazy evaluation without side effects
- [identity](https://pkg.go.dev/github.com/IBM/fp-go/v2/identity): Identity monad
- [retry](https://pkg.go.dev/github.com/IBM/fp-go/v2/retry): Retry policies with configurable backoff
- [tailrec](https://pkg.go.dev/github.com/IBM/fp-go/v2/tailrec): Trampoline for tail-call optimization
- [di](https://pkg.go.dev/github.com/IBM/fp-go/v2/di): Dependency injection utilities
- [effect](https://pkg.go.dev/github.com/IBM/fp-go/v2/effect): Functional effect system
- [circuitbreaker](https://pkg.go.dev/github.com/IBM/fp-go/v2/circuitbreaker): Circuit breaker error types
- [builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/builder): Generic builder pattern with validation
## Code Samples
- [samples/builder](https://github.com/IBM/fp-go/tree/main/v2/samples/builder): Functional builder pattern example
- [samples/http](https://github.com/IBM/fp-go/tree/main/v2/samples/http): HTTP client examples
- [samples/lens](https://github.com/IBM/fp-go/tree/main/v2/samples/lens): Optics/lens examples
- [samples/mostly-adequate](https://github.com/IBM/fp-go/tree/main/v2/samples/mostly-adequate): Examples adapted from "Mostly Adequate Guide to Functional Programming"
- [samples/tuples](https://github.com/IBM/fp-go/tree/main/v2/samples/tuples): Tuple usage examples
## Optional
- [Source Code](https://github.com/IBM/fp-go): GitHub repository
- [Issues](https://github.com/IBM/fp-go/issues): Bug reports and feature requests
- [Go Report Card](https://goreportcard.com/report/github.com/IBM/fp-go/v2): Code quality report
- [Coverage](https://coveralls.io/github/IBM/fp-go?branch=main): Test coverage report

View File

@@ -20,6 +20,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/reader"
)
@@ -270,3 +271,210 @@ func MonadAlt[A, O, I any](first Type[A, O, I], second Lazy[Type[A, O, I]]) Type
func Alt[A, O, I any](second Lazy[Type[A, O, I]]) Operator[A, A, O, I] {
return F.Bind2nd(MonadAlt, second)
}
// AltMonoid creates a Monoid instance for Type[A, O, I] using alternative semantics
// with a provided zero/default codec.
//
// This function creates a monoid where:
// 1. The first successful codec wins (no result combination)
// 2. If the first fails during validation, the second is tried as a fallback
// 3. If both fail, errors are aggregated
// 4. The provided zero codec serves as the identity element
//
// Unlike other monoid patterns, AltMonoid does NOT combine successful results - it always
// returns the first success. This makes it ideal for building fallback chains with default
// codecs, configuration loading from multiple sources, and parser combinators with alternatives.
//
// # Type Parameters
//
// - A: The target type that all codecs decode to
// - O: The output type that all codecs encode to
// - I: The input type that all codecs decode from
//
// # Parameters
//
// - zero: A lazy Type[A, O, I] that serves as the identity element. This is typically
// a codec that always succeeds with a default value, but can also be a failing
// codec if no default is appropriate.
//
// # Returns
//
// A Monoid[Type[A, O, I]] that combines codecs using alternative semantics where
// the first success wins.
//
// # Behavior Details
//
// The AltMonoid implements a "first success wins" strategy:
//
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
// - **Concat with Empty**: The zero codec is used as fallback
// - **Encoding**: Always uses the first codec's encoder
//
// # Example: Configuration Loading with Fallbacks
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/array"
// )
//
// // Create a monoid with a default configuration
// m := codec.AltMonoid(func() codec.Type[Config, string, string] {
// return codec.MakeType(
// "DefaultConfig",
// codec.Is[Config](),
// func(s string) codec.Decode[codec.Context, Config] {
// return func(c codec.Context) codec.Validation[Config] {
// return validation.Success(defaultConfig)
// }
// },
// encodeConfig,
// )
// })
//
// // Define codecs for different sources
// fileCodec := loadFromFile("config.json")
// envCodec := loadFromEnv()
// defaultCodec := m.Empty()
//
// // Try file, then env, then default
// configCodec := array.MonadFold(
// []codec.Type[Config, string, string]{fileCodec, envCodec, defaultCodec},
// m.Empty(),
// m.Concat,
// )
//
// // Load configuration - tries each source in order
// result := configCodec.Decode(input)
//
// # Example: Parser with Multiple Formats
//
// // Create a monoid for parsing dates in multiple formats
// m := codec.AltMonoid(func() codec.Type[time.Time, string, string] {
// return codec.Date(time.RFC3339) // default format
// })
//
// // Define parsers for different date formats
// iso8601 := codec.Date("2006-01-02")
// usFormat := codec.Date("01/02/2006")
// euroFormat := codec.Date("02/01/2006")
//
// // Combine: try ISO 8601, then US, then European, then RFC3339
// flexibleDate := m.Concat(
// m.Concat(
// m.Concat(iso8601, usFormat),
// euroFormat,
// ),
// m.Empty(),
// )
//
// // Can parse any of these formats
// result1 := flexibleDate.Decode("2024-03-15") // ISO 8601
// result2 := flexibleDate.Decode("03/15/2024") // US format
// result3 := flexibleDate.Decode("15/03/2024") // European format
//
// # Example: Integer Parsing with Default
//
// // Create a monoid with default value of 0
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
// return codec.MakeType(
// "DefaultZero",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.Success(0)
// }
// },
// strconv.Itoa,
// )
// })
//
// // Try parsing as int, fall back to 0
// intOrZero := m.Concat(codec.IntFromString(), m.Empty())
//
// result1 := intOrZero.Decode("42") // Success(42)
// result2 := intOrZero.Decode("invalid") // Success(0) - uses default
//
// # Example: Error Aggregation
//
// // Both codecs fail - errors are aggregated
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
// return codec.MakeType(
// "NoDefault",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "no default available")(c)
// }
// },
// strconv.Itoa,
// )
// })
//
// failing1 := codec.MakeType(
// "Failing1",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "error 1")(c)
// }
// },
// strconv.Itoa,
// )
//
// failing2 := codec.MakeType(
// "Failing2",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "error 2")(c)
// }
// },
// strconv.Itoa,
// )
//
// combined := m.Concat(failing1, failing2)
// result := combined.Decode("input")
// // result contains errors: "error 1", "error 2", and "no default available"
//
// # Monoid Laws
//
// AltMonoid satisfies the monoid laws:
//
// 1. **Left Identity**: m.Concat(m.Empty(), codec) ≡ codec
// 2. **Right Identity**: m.Concat(codec, m.Empty()) ≡ codec (tries codec first, falls back to zero)
// 3. **Associativity**: m.Concat(m.Concat(a, b), c) ≡ m.Concat(a, m.Concat(b, c))
//
// Note: Due to the "first success wins" behavior, right identity means the zero is only
// used if the codec fails.
//
// # Use Cases
//
// - Configuration loading with multiple sources (file, env, default)
// - Parsing data in multiple formats with fallbacks
// - API versioning (try v2, fall back to v1, then default)
// - Content negotiation (try JSON, then XML, then plain text)
// - Validation with default values
// - Parser combinators with alternative branches
//
// # Notes
//
// - The zero codec is lazily evaluated, only when needed
// - First success short-circuits evaluation (subsequent codecs not tried)
// - Error aggregation ensures all validation failures are reported
// - Encoding always uses the first codec's encoder
// - This follows the alternative functor laws
//
// # See Also
//
// - MonadAlt: The underlying alternative operation for two codecs
// - Alt: The curried version for pipeline composition
// - validate.AltMonoid: The validation-level alternative monoid
// - decode.AltMonoid: The decode-level alternative monoid
func AltMonoid[A, O, I any](zero Lazy[Type[A, O, I]]) Monoid[Type[A, O, I]] {
return monoid.AltMonoid(
zero,
MonadAlt[A, O, I],
)
}

View File

@@ -561,3 +561,361 @@ func TestAltErrorMessages(t *testing.T) {
assert.True(t, hasCodec2Error, "should have error from second codec")
})
}
// TestAltMonoid tests the AltMonoid function
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
// Create a monoid with a default value of 0
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"DefaultZero",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
strconv.Itoa,
)
})
// Create codecs
intFromString := IntFromString()
failing := MakeType(
"Failing",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "always fails")(c)
}
},
strconv.Itoa,
)
t.Run("first success wins", func(t *testing.T) {
// Combine two successful codecs - first should win
codec1 := MakeType(
"Returns10",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(10)
}
},
strconv.Itoa,
)
codec2 := MakeType(
"Returns20",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(20)
}
},
strconv.Itoa,
)
combined := m.Concat(codec1, codec2)
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
assert.Equal(t, 10, value, "first success should win")
})
t.Run("falls back to second when first fails", func(t *testing.T) {
combined := m.Concat(failing, intFromString)
result := combined.Decode("42")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
assert.Equal(t, 42, value)
})
t.Run("uses zero when both fail", func(t *testing.T) {
combined := m.Concat(failing, m.Empty())
result := combined.Decode("invalid")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
assert.Equal(t, 0, value, "should use default zero value")
})
})
t.Run("with failing zero", func(t *testing.T) {
// Create a monoid with a failing zero
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"NoDefault",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "no default available")(c)
}
},
strconv.Itoa,
)
})
failing1 := MakeType(
"Failing1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "error 1")(c)
}
},
strconv.Itoa,
)
failing2 := MakeType(
"Failing2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "error 2")(c)
}
},
strconv.Itoa,
)
t.Run("aggregates all errors when all fail", func(t *testing.T) {
combined := m.Concat(m.Concat(failing1, failing2), m.Empty())
result := combined.Decode("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
// Should have errors from all three: failing1, failing2, and zero
assert.GreaterOrEqual(t, len(errors), 3)
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
hasError1 := false
hasError2 := false
hasNoDefault := false
for _, msg := range messages {
if msg == "error 1" {
hasError1 = true
}
if msg == "error 2" {
hasError2 = true
}
if msg == "no default available" {
hasNoDefault = true
}
}
assert.True(t, hasError1, "should have error from failing1")
assert.True(t, hasError2, "should have error from failing2")
assert.True(t, hasNoDefault, "should have error from zero")
})
})
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Type[string, string, string] {
return MakeType(
"Default",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
return validation.Success("default")
}
},
F.Identity[string],
)
})
primary := MakeType(
"Primary",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s == "primary" {
return validation.Success("from primary")
}
return validation.FailureWithMessage[string](s, "not primary")(c)
}
},
F.Identity[string],
)
secondary := MakeType(
"Secondary",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s == "secondary" {
return validation.Success("from secondary")
}
return validation.FailureWithMessage[string](s, "not secondary")(c)
}
},
F.Identity[string],
)
// Chain: try primary, then secondary, then default
combined := m.Concat(m.Concat(primary, secondary), m.Empty())
t.Run("uses primary when it succeeds", func(t *testing.T) {
result := combined.Decode("primary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "from primary", value)
})
t.Run("uses secondary when primary fails", func(t *testing.T) {
result := combined.Decode("secondary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "from secondary", value)
})
t.Run("uses default when both fail", func(t *testing.T) {
result := combined.Decode("other")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "default", value)
})
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"DefaultZero",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
strconv.Itoa,
)
})
codec1 := MakeType(
"Codec1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(10)
}
},
strconv.Itoa,
)
codec2 := MakeType(
"Codec2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(20)
}
},
strconv.Itoa,
)
codec3 := MakeType(
"Codec3",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(30)
}
},
strconv.Itoa,
)
t.Run("left identity", func(t *testing.T) {
// m.Concat(m.Empty(), codec) should behave like codec
// But with AltMonoid, if codec fails, it falls back to empty
combined := m.Concat(m.Empty(), codec1)
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
// Empty (0) comes first, so it wins
assert.Equal(t, 0, value)
})
t.Run("right identity", func(t *testing.T) {
// m.Concat(codec, m.Empty()) tries codec first, falls back to empty
combined := m.Concat(codec1, m.Empty())
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
assert.Equal(t, 10, value, "codec1 should win")
})
t.Run("associativity", func(t *testing.T) {
// For AltMonoid, first success wins
left := m.Concat(m.Concat(codec1, codec2), codec3)
right := m.Concat(codec1, m.Concat(codec2, codec3))
resultLeft := left.Decode("input")
resultRight := right.Decode("input")
assert.True(t, either.IsRight(resultLeft))
assert.True(t, either.IsRight(resultRight))
valueLeft := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultRight)
// Both should return 10 (first success)
assert.Equal(t, valueLeft, valueRight)
assert.Equal(t, 10, valueLeft)
})
})
t.Run("encoding uses first codec", func(t *testing.T) {
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"Default",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
func(n int) string { return "DEFAULT" },
)
})
codec1 := MakeType(
"Codec1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(42)
}
},
func(n int) string { return fmt.Sprintf("FIRST:%d", n) },
)
codec2 := MakeType(
"Codec2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(100)
}
},
func(n int) string { return fmt.Sprintf("SECOND:%d", n) },
)
combined := m.Concat(codec1, codec2)
// Encoding should use first codec's encoder
encoded := combined.Encode(42)
assert.Equal(t, "FIRST:42", encoded)
})
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/internal/formatting"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/decode"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/optics/codec/validation"
@@ -40,6 +41,27 @@ type (
// Codec combines a Decoder and an Encoder for bidirectional transformations.
// It can decode input I to type A and encode type A to output O.
//
// This is a simple struct that pairs a decoder with an encoder, providing
// the basic building blocks for bidirectional data transformation. Unlike
// the Type interface, Codec is a concrete struct without validation context
// or type checking capabilities.
//
// Type Parameters:
// - I: The input type to decode from
// - O: The output type to encode to
// - A: The intermediate type (decoded to, encoded from)
//
// Fields:
// - Decode: A decoder that transforms I to A
// - Encode: An encoder that transforms A to O
//
// Example:
// A Codec[string, string, int] can decode strings to integers and
// encode integers back to strings.
//
// Note: For most use cases, prefer using the Type interface which provides
// additional validation and type checking capabilities.
Codec[I, O, A any] struct {
Decode decoder.Decoder[I, A]
Encode encoder.Encoder[O, A]
@@ -55,16 +77,82 @@ type (
// Validate is a function that validates input I to produce type A.
// It takes an input and returns a Reader that depends on the validation Context.
//
// The Validate type is the core validation abstraction, defined as:
// Reader[I, Decode[Context, A]]
//
// This means:
// 1. It takes an input of type I
// 2. Returns a Reader that depends on validation Context
// 3. That Reader produces a Validation[A] (Either[Errors, A])
//
// This layered structure allows validators to:
// - Access the input value
// - Track validation context (path in nested structures)
// - Accumulate multiple validation errors
// - Compose with other validators
//
// Example:
// A Validate[string, int] takes a string and returns a context-aware
// function that validates and converts it to an integer.
Validate[I, A any] = validate.Validate[I, A]
// Decode is a function that decodes input I to type A with validation.
// It returns a Validation result directly.
//
// The Decode type is defined as:
// Reader[I, Validation[A]]
//
// This is simpler than Validate as it doesn't require explicit context passing.
// The context is typically created automatically when the decoder is invoked.
//
// Decode is used when:
// - You don't need to manually manage validation context
// - You want a simpler API for basic validation
// - You're working at the top level of validation
//
// Example:
// A Decode[string, int] takes a string and returns a Validation[int]
// which is Either[Errors, int].
Decode[I, A any] = decode.Decode[I, A]
// Encode is a function that encodes type A to output O.
//
// Encode is simply a Reader[A, O], which is a function from A to O.
// Encoders are pure functions with no error handling - they assume
// the input is valid.
//
// Encoding is the inverse of decoding:
// - Decoding: I -> Validation[A] (may fail)
// - Encoding: A -> O (always succeeds)
//
// Example:
// An Encode[int, string] takes an integer and returns its string
// representation.
Encode[A, O any] = Reader[A, O]
// Decoder is an interface for types that can decode and validate input.
//
// A Decoder transforms input of type I into a validated value of type A,
// providing detailed error information when validation fails. It supports
// both context-aware validation (via Validate) and direct decoding (via Decode).
//
// Type Parameters:
// - I: The input type to decode from
// - A: The target type to decode to
//
// Methods:
// - Name(): Returns a descriptive name for this decoder (used in error messages)
// - Validate(I): Returns a context-aware validation function that can track
// the path through nested structures
// - Decode(I): Directly decodes input to a Validation result with a fresh context
//
// The Validate method is more flexible as it returns a Reader that can be called
// with different contexts, while Decode is a convenience method that creates a
// new context automatically.
//
// Example:
// A Decoder[string, int] can decode strings to integers with validation.
Decoder[I, A any] interface {
Name() string
Validate(I) Decode[Context, A]
@@ -72,13 +160,76 @@ type (
}
// Encoder is an interface for types that can encode values.
//
// An Encoder transforms values of type A into output format O. This is the
// inverse operation of decoding, allowing bidirectional transformations.
//
// Type Parameters:
// - A: The source type to encode from
// - O: The output type to encode to
//
// Methods:
// - Encode(A): Transforms a value of type A into output format O
//
// Encoders are pure functions with no validation or error handling - they
// assume the input is valid. Validation should be performed during decoding.
//
// Example:
// An Encoder[int, string] can encode integers to their string representation.
Encoder[A, O any] interface {
// Encode transforms a value of type A into output format O.
Encode(A) O
}
// Type is a bidirectional codec that combines encoding, decoding, validation,
// and type checking capabilities. It represents a complete specification of
// how to work with a particular type.
//
// Type is the central abstraction in the codec package, providing:
// - Decoding: Transform input I to validated type A
// - Encoding: Transform type A to output O
// - Validation: Context-aware validation with detailed error reporting
// - Type Checking: Runtime type verification via Is()
// - Formatting: Human-readable type descriptions via Name()
//
// Type Parameters:
// - A: The target type (what we decode to and encode from)
// - O: The output type (what we encode to)
// - I: The input type (what we decode from)
//
// Common patterns:
// - Type[A, A, A]: Identity codec (no transformation)
// - Type[A, string, string]: String-based serialization
// - Type[A, any, any]: Generic codec accepting any input/output
// - Type[A, JSON, JSON]: JSON codec
//
// Methods:
// - Name(): Returns the codec's descriptive name
// - Validate(I): Returns context-aware validation function
// - Decode(I): Decodes input with automatic context creation
// - Encode(A): Encodes value to output format
// - AsDecoder(): Returns this Type as a Decoder interface
// - AsEncoder(): Returns this Type as an Encoder interface
// - Is(any): Checks if a value can be converted to type A
//
// Example usage:
// intCodec := codec.Int() // Type[int, int, any]
// stringCodec := codec.String() // Type[string, string, any]
// intFromString := codec.IntFromString() // Type[int, string, string]
//
// // Decode
// result := intFromString.Decode("42") // Validation[int]
//
// // Encode
// str := intFromString.Encode(42) // "42"
//
// // Type check
// isInt := intCodec.Is(42) // Right(42)
// notInt := intCodec.Is("42") // Left(error)
//
// Composition:
// Types can be composed using operators like Alt, Map, Chain, and Pipe
// to build complex codecs from simpler ones.
Type[A, O, I any] interface {
Formattable
Decoder[I, A]
@@ -99,9 +250,92 @@ type (
// contain a value of type A. It provides a way to preview and review values.
Prism[S, A any] = prism.Prism[S, A]
// Refinement represents the concept that B is a specialized type of A
// Refinement represents the concept that B is a specialized type of A.
// It's an alias for Prism[A, B], providing a semantic name for type refinement operations.
//
// A refinement allows you to:
// - Preview: Try to extract a B from an A (may fail if A is not a B)
// - Review: Inject a B back into an A
//
// This is useful for working with subtypes, validated types, or constrained types.
//
// Example:
// - Refinement[int, PositiveInt] - refines int to positive integers only
// - Refinement[string, NonEmptyString] - refines string to non-empty strings
// - Refinement[any, User] - refines any to User type
Refinement[A, B any] = Prism[A, B]
Kleisli[A, B, O, I any] = Reader[A, Type[B, O, I]]
// Kleisli represents a Kleisli arrow in the codec context.
// It's a function that takes a value of type A and returns a codec Type[B, O, I].
//
// This is the fundamental building block for codec transformations and compositions.
// Kleisli arrows allow you to:
// - Chain codec operations
// - Build dependent codecs (where the next codec depends on the previous result)
// - Create codec pipelines
//
// Type Parameters:
// - A: The input type to the function
// - B: The target type that the resulting codec decodes to
// - O: The output type that the resulting codec encodes to
// - I: The input type that the resulting codec decodes from
//
// Example:
// A Kleisli[string, int, string, string] takes a string and returns a codec
// that can decode strings to ints and encode ints to strings.
Kleisli[A, B, O, I any] = Reader[A, Type[B, O, I]]
// Operator is a specialized Kleisli arrow that transforms codecs.
// It takes a codec Type[A, O, I] and returns a new codec Type[B, O, I].
//
// Operators are the primary way to build codec transformation pipelines.
// They enable functional composition of codec transformations using F.Pipe.
//
// Type Parameters:
// - A: The source type that the input codec decodes to
// - B: The target type that the output codec decodes to
// - O: The output type (same for both input and output codecs)
// - I: The input type (same for both input and output codecs)
//
// Common operators include:
// - Map: Transforms the decoded value
// - Chain: Sequences dependent codec operations
// - Alt: Provides alternative fallback codecs
// - Refine: Adds validation constraints
//
// Example:
// An Operator[int, PositiveInt, int, any] transforms a codec that decodes
// to int into a codec that decodes to PositiveInt (with validation).
//
// Usage with F.Pipe:
// codec := F.Pipe2(
// baseCodec,
// operator1, // Operator[A, B, O, I]
// operator2, // Operator[B, C, O, I]
// )
Operator[A, B, O, I any] = Kleisli[Type[A, O, I], B, O, I]
// Monoid represents an algebraic structure with an associative binary operation
// and an identity element.
//
// A Monoid[A] provides:
// - Empty(): Returns the identity element
// - Concat(A, A): Combines two values associatively
//
// Monoid laws:
// 1. Left Identity: Concat(Empty(), a) = a
// 2. Right Identity: Concat(a, Empty()) = a
// 3. Associativity: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
//
// In the codec context, monoids are used to:
// - Combine multiple codecs with specific semantics
// - Build codec chains with fallback behavior (AltMonoid)
// - Aggregate validation results (ApplicativeMonoid)
// - Compose codec transformations
//
// Example monoids for codecs:
// - AltMonoid: First success wins (alternative semantics)
// - ApplicativeMonoid: Combines successful results using inner monoid
// - AlternativeMonoid: Combines applicative and alternative behaviors
Monoid[A any] = monoid.Monoid[A]
)

View File

@@ -3,7 +3,7 @@ package readerio
import "github.com/IBM/fp-go/v2/io"
//go:inline
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, struct{}] {
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, Void] {
return ChainIOK[R](io.FromConsumer(c))
}

View File

@@ -18,6 +18,7 @@ package readerio
import (
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
@@ -66,4 +67,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
)

View File

@@ -1,14 +1,107 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"github.com/IBM/fp-go/v2/readerioeither"
)
// ChainConsumer chains a consumer (side-effect function) into a ReaderIOResult computation,
// replacing the success value with Void (empty struct).
//
// This is useful for performing side effects (like logging, printing, or writing to a file)
// where you don't need to preserve the original value. The consumer is only executed if the
// computation succeeds; if it fails with an error, the consumer is skipped.
//
// Type parameters:
// - R: The context/environment type
// - A: The value type to consume
//
// Parameters:
// - c: A consumer function that performs a side effect on the value
//
// Returns:
//
// An Operator that executes the consumer and returns Void on success
//
// Example:
//
// import (
// "context"
// "fmt"
// RIO "github.com/IBM/fp-go/v2/readerioresult"
// )
//
// // Log a value and discard it
// logValue := RIO.ChainConsumer[context.Context](func(x int) {
// fmt.Printf("Value: %d\n", x)
// })
//
// computation := F.Pipe1(
// RIO.Of[context.Context](42),
// logValue,
// )
// // Prints "Value: 42" and returns result.Of(struct{}{})
//
//go:inline
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, struct{}] {
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, Void] {
return readerioeither.ChainConsumer[R, error](c)
}
// ChainFirstConsumer chains a consumer into a ReaderIOResult computation while preserving
// the original value.
//
// This is useful for performing side effects (like logging, printing, or metrics collection)
// where you want to keep the original value for further processing. The consumer is only
// executed if the computation succeeds; if it fails with an error, the consumer is skipped
// and the error is propagated.
//
// Type parameters:
// - R: The context/environment type
// - A: The value type to consume and preserve
//
// Parameters:
// - c: A consumer function that performs a side effect on the value
//
// Returns:
//
// An Operator that executes the consumer and returns the original value on success
//
// Example:
//
// import (
// "context"
// "fmt"
// F "github.com/IBM/fp-go/v2/function"
// N "github.com/IBM/fp-go/v2/number"
// RIO "github.com/IBM/fp-go/v2/readerioresult"
// )
//
// // Log a value but keep it for further processing
// logValue := RIO.ChainFirstConsumer[context.Context](func(x int) {
// fmt.Printf("Processing: %d\n", x)
// })
//
// computation := F.Pipe2(
// RIO.Of[context.Context](10),
// logValue,
// RIO.Map[context.Context](N.Mul(2)),
// )
// // Prints "Processing: 10" and returns result.Of(20)
//
//go:inline
func ChainFirstConsumer[R, A any](c Consumer[A]) Operator[R, A, A] {
return readerioeither.ChainFirstConsumer[R, error](c)

View File

@@ -0,0 +1,362 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"context"
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestChainConsumer_Success tests that ChainConsumer executes the consumer
// and returns Void when the computation succeeds
func TestChainConsumer_Success(t *testing.T) {
// Track if consumer was called
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a successful computation and chain the consumer
computation := F.Pipe1(
Of[context.Context](42),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with correct value
assert.Equal(t, 42, consumed)
// Verify result is successful with Void
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) Void { return Void{} })(res)
assert.Equal(t, Void{}, val)
}
}
// TestChainConsumer_Failure tests that ChainConsumer does not execute
// the consumer when the computation fails
func TestChainConsumer_Failure(t *testing.T) {
// Track if consumer was called
consumerCalled := false
consumer := func(x int) {
consumerCalled = true
}
// Create a failing computation
expectedErr := errors.New("test error")
computation := F.Pipe1(
Left[context.Context, int](expectedErr),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was NOT called
assert.False(t, consumerCalled)
// Verify result is an error
assert.True(t, result.IsLeft(res))
}
// TestChainConsumer_MultipleOperations tests chaining multiple operations
// with ChainConsumer in a pipeline
func TestChainConsumer_MultipleOperations(t *testing.T) {
// Track consumer calls
var values []int
consumer := func(x int) {
values = append(values, x)
}
// Create a pipeline with multiple operations
computation := F.Pipe2(
Of[context.Context](10),
Map[context.Context](N.Mul(2)),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with transformed value
assert.Equal(t, []int{20}, values)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_Success tests that ChainFirstConsumer executes
// the consumer and preserves the original value
func TestChainFirstConsumer_Success(t *testing.T) {
// Track if consumer was called
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a successful computation and chain the consumer
computation := F.Pipe1(
Of[context.Context](42),
ChainFirstConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with correct value
assert.Equal(t, 42, consumed)
// Verify result is successful and preserves original value
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 42, val)
}
}
// TestChainFirstConsumer_Failure tests that ChainFirstConsumer does not
// execute the consumer when the computation fails
func TestChainFirstConsumer_Failure(t *testing.T) {
// Track if consumer was called
consumerCalled := false
consumer := func(x int) {
consumerCalled = true
}
// Create a failing computation
expectedErr := errors.New("test error")
computation := F.Pipe1(
Left[context.Context, int](expectedErr),
ChainFirstConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was NOT called
assert.False(t, consumerCalled)
// Verify result is an error
assert.True(t, result.IsLeft(res))
}
// TestChainFirstConsumer_PreservesValue tests that ChainFirstConsumer
// preserves the value for further processing
func TestChainFirstConsumer_PreservesValue(t *testing.T) {
// Track consumer calls
var logged []int
logger := func(x int) {
logged = append(logged, x)
}
// Create a pipeline that logs intermediate values
computation := F.Pipe3(
Of[context.Context](10),
ChainFirstConsumer[context.Context](logger),
Map[context.Context](N.Mul(2)),
ChainFirstConsumer[context.Context](logger),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called at each step
assert.Equal(t, []int{10, 20}, logged)
// Verify final result
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 20, val)
}
}
// TestChainFirstConsumer_WithMap tests combining ChainFirstConsumer with Map
func TestChainFirstConsumer_WithMap(t *testing.T) {
// Track intermediate values
var intermediate int
consumer := func(x int) {
intermediate = x
}
// Create a pipeline with logging and transformation
computation := F.Pipe2(
Of[context.Context](5),
ChainFirstConsumer[context.Context](consumer),
Map[context.Context](N.Mul(3)),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer saw original value
assert.Equal(t, 5, intermediate)
// Verify final result is transformed
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 15, val)
}
}
// TestChainConsumer_WithContext tests that consumers work with context
func TestChainConsumer_WithContext(t *testing.T) {
type Config struct {
Multiplier int
}
// Track consumer calls
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a computation that uses context
computation := F.Pipe2(
Of[Config](10),
Map[Config](N.Mul(2)),
ChainConsumer[Config](consumer),
)
// Execute with context
cfg := Config{Multiplier: 3}
res := computation(cfg)()
// Verify consumer was called
assert.Equal(t, 20, consumed)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_SideEffects tests that ChainFirstConsumer
// can be used for side effects like logging
func TestChainFirstConsumer_SideEffects(t *testing.T) {
// Simulate a logging side effect
var logs []string
logValue := func(x string) {
logs = append(logs, "Processing: "+x)
}
// Create a pipeline with logging
computation := F.Pipe3(
Of[context.Context]("hello"),
ChainFirstConsumer[context.Context](logValue),
Map[context.Context](S.Append(" world")),
ChainFirstConsumer[context.Context](logValue),
)
// Execute the computation
res := computation(context.Background())()
// Verify logs were created
assert.Equal(t, []string{
"Processing: hello",
"Processing: hello world",
}, logs)
// Verify final result
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) string { return "" })(res)
assert.Equal(t, "hello world", val)
}
}
// TestChainConsumer_ComplexType tests consumers with complex types
func TestChainConsumer_ComplexType(t *testing.T) {
type User struct {
Name string
Age int
}
// Track consumed user
var consumedUser *User
consumer := func(u User) {
consumedUser = &u
}
// Create a computation with a complex type
user := User{Name: "Alice", Age: 30}
computation := F.Pipe1(
Of[context.Context](user),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer received the user
assert.NotNil(t, consumedUser)
assert.Equal(t, "Alice", consumedUser.Name)
assert.Equal(t, 30, consumedUser.Age)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_ComplexType tests ChainFirstConsumer with complex types
func TestChainFirstConsumer_ComplexType(t *testing.T) {
type Product struct {
ID int
Name string
Price float64
}
// Track consumed products
var consumedProducts []Product
consumer := func(p Product) {
consumedProducts = append(consumedProducts, p)
}
// Create a pipeline with complex type
product := Product{ID: 1, Name: "Widget", Price: 9.99}
computation := F.Pipe2(
Of[context.Context](product),
ChainFirstConsumer[context.Context](consumer),
Map[context.Context](func(p Product) Product {
p.Price = p.Price * 1.1 // Apply 10% markup
return p
}),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer saw original product
assert.Len(t, consumedProducts, 1)
assert.Equal(t, 9.99, consumedProducts[0].Price)
// Verify final result has updated price
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
finalProduct := result.GetOrElse(func(error) Product { return Product{} })(res)
assert.InDelta(t, 10.989, finalProduct.Price, 0.001)
}
}
// Made with Bob

View File

@@ -25,10 +25,11 @@ import (
"github.com/IBM/fp-go/v2/readerio"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
//go:inline
func FromReaderOption[R, A any](onNone func() error) Kleisli[R, ReaderOption[R, A], A] {
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
return RIOE.FromReaderOption[R, A](onNone)
}
@@ -113,7 +114,7 @@ func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIO
// The Either is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, B] {
func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainEitherK(ma, f)
}
@@ -121,7 +122,7 @@ func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]
// The Either is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, B] {
func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainEitherK(ma, f)
}
@@ -129,7 +130,7 @@ func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
func ChainEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return RIOE.ChainEitherK[R](f)
}
@@ -137,7 +138,7 @@ func ChainEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
func ChainResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return RIOE.ChainEitherK[R](f)
}
@@ -145,12 +146,12 @@ func ChainResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
// Useful for validation or side effects that return Either.
//
//go:inline
func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
}
@@ -158,12 +159,12 @@ func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B])
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func ChainFirstEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
//go:inline
func TapEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func TapEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
}
@@ -171,12 +172,12 @@ func TapEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
// Useful for validation or side effects that return Either.
//
//go:inline
func MonadChainFirstResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadChainFirstResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
}
@@ -184,12 +185,12 @@ func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B])
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func ChainFirstResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
//go:inline
func TapResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func TapResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
}
@@ -230,17 +231,17 @@ func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
}
//go:inline
func ChainReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
func ChainReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderOptionK[R, A, B](onNone)
}
//go:inline
func ChainFirstReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
func ChainFirstReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderOptionK[R, A, B](onNone)
}
//go:inline
func TapReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
func TapReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderOptionK[R, A, B](onNone)
}
@@ -421,7 +422,7 @@ func TapIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, A] {
// If the Option is None, the provided error function is called to produce the error value.
//
//go:inline
func ChainOptionK[R, A, B any](onNone func() error) func(func(A) Option[B]) Operator[R, A, B] {
func ChainOptionK[R, A, B any](onNone Lazy[error]) func(func(A) Option[B]) Operator[R, A, B] {
return RIOE.ChainOptionK[R, A, B](onNone)
}
@@ -619,7 +620,7 @@ func Asks[R, A any](r Reader[R, A]) ReaderIOResult[R, A] {
// If the Option is None, the provided function is called to produce the error.
//
//go:inline
func FromOption[R, A any](onNone func() error) Kleisli[R, Option[A], A] {
func FromOption[R, A any](onNone Lazy[error]) Kleisli[R, Option[A], A] {
return RIOE.FromOption[R, A](onNone)
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
@@ -122,4 +123,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
)