1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-24 12:57:26 +02:00

Compare commits

..

6 Commits

Author SHA1 Message Date
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
Dr. Carsten Leue
bd9a642e93 fix: implement Alt for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 18:31:00 +01:00
Dr. Carsten Leue
3b55cae265 fix: implement alternative monoid for codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 09:59:17 +01:00
Dr. Carsten Leue
1472fa5a50 fix: add some more validation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-04 17:58:08 +01:00
Dr. Carsten Leue
49deb57d24 fix: OrElse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 17:54:43 +01:00
33 changed files with 8822 additions and 894 deletions

View File

@@ -1 +0,0 @@
{"mcpServers":{}}

View File

@@ -196,6 +196,8 @@ func MonadChain[E, A, B any](fa Either[E, A], f Kleisli[E, A, B]) Either[E, B] {
// MonadChainLeft operates on Left values (errors). It's useful for error recovery, error transformation,
// or chaining alternative computations when an error occurs.
//
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// The error type can be transformed from EA to EB, allowing flexible error type conversions.
//
// Example:
@@ -235,6 +237,8 @@ func MonadChainLeft[EA, EB, A any](fa Either[EA, A], f Kleisli[EB, EA, A]) Eithe
// ChainLeft is the curried version of [MonadChainLeft].
// Returns a function that sequences a computation on the Left (error) value.
//
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is useful for creating reusable error handlers or transformers that can be
// composed with other Either operations using pipes or function composition.
//
@@ -542,6 +546,8 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
// If the Either is Left, it applies the provided function to the error value,
// which returns a new Either that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative computations.
// The error type can be widened from E1 to E2, allowing transformation of error types.
//

View File

@@ -489,6 +489,8 @@ func After[E, A any](timestamp time.Time) Operator[E, A, A] {
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
//
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is useful for error recovery or error transformation scenarios where you want to handle
// errors by performing another computation that may also fail.
//
@@ -523,6 +525,8 @@ func MonadChainLeft[EA, EB, A any](fa IOEither[EA, A], f Kleisli[EB, EA, A]) IOE
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of an [IOEither].
//
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is particularly useful in functional composition pipelines where you want to handle
// errors by performing another computation that may also fail.
//
@@ -644,6 +648,8 @@ func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
// If the IOEither is Left, it applies the provided function to the error value,
// which returns a new IOEither that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative IO computations.
// The error type can be widened from E1 to E2, allowing transformation of error types.
//

View File

@@ -490,3 +490,148 @@ func TestOrElseW(t *testing.T) {
preserved := preserveRecover(preservedRight)()
assert.Equal(t, E.Right[AppError](42), preserved)
}
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
// This test verifies that both functions produce the same results for all scenarios:
// - Left values with error recovery
// - Left values with error transformation
// - Right values passing through unchanged
func TestChainLeftIdenticalToOrElse(t *testing.T) {
// Test 1: Left value with error recovery - both should recover to Right
t.Run("Left value recovery - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) IOEither[string, int] {
if e == "recoverable" {
return Right[string](42)
}
return Left[int](e)
}
input := Left[int]("recoverable")
// Using ChainLeft
resultChainLeft := ChainLeft(recoveryFn)(input)()
// Using OrElse
resultOrElse := OrElse(recoveryFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](42), resultChainLeft)
})
// Test 2: Left value with error transformation - both should transform error
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
transformFn := func(e string) IOEither[string, int] {
return Left[int]("transformed: " + e)
}
input := Left[int]("original error")
// Using ChainLeft
resultChainLeft := ChainLeft(transformFn)(input)()
// Using OrElse
resultOrElse := OrElse(transformFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
})
// Test 3: Right value - both should pass through unchanged
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
handlerFn := func(e string) IOEither[string, int] {
return Left[int]("should not be called")
}
input := Right[string](100)
// Using ChainLeft
resultChainLeft := ChainLeft(handlerFn)(input)()
// Using OrElse
resultOrElse := OrElse(handlerFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](100), resultChainLeft)
})
// Test 4: Error type widening - both should handle type transformation
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
widenFn := func(e string) IOEither[int, int] {
return Left[int](404)
}
input := Left[int]("not found")
// Using ChainLeft
resultChainLeft := ChainLeft(widenFn)(input)()
// Using OrElse
resultOrElse := OrElse(widenFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int](404), resultChainLeft)
})
// Test 5: Composition in pipeline - both should work identically in F.Pipe
t.Run("Pipeline composition - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) IOEither[string, int] {
if e == "network error" {
return Right[string](0)
}
return Left[int](e)
}
input := Left[int]("network error")
// Using ChainLeft in pipeline
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))()
// Using OrElse in pipeline
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](0), resultChainLeft)
})
// Test 6: Multiple chained operations - both should behave identically
t.Run("Multiple operations - ChainLeft equals OrElse", func(t *testing.T) {
handler1 := func(e string) IOEither[string, int] {
if e == "error1" {
return Right[string](1)
}
return Left[int](e)
}
handler2 := func(e string) IOEither[string, int] {
if e == "error2" {
return Right[string](2)
}
return Left[int](e)
}
input := Left[int]("error2")
// Using ChainLeft
resultChainLeft := F.Pipe2(
input,
ChainLeft(handler1),
ChainLeft(handler2),
)()
// Using OrElse
resultOrElse := F.Pipe2(
input,
OrElse(handler1),
OrElse(handler2),
)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](2), resultChainLeft)
})
}

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

480
v2/optics/codec/alt.go Normal file
View File

@@ -0,0 +1,480 @@
// 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 codec
import (
"fmt"
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"
)
// validateAlt creates a validation function that tries the first codec's validation,
// and if it fails, tries the second codec's validation as a fallback.
//
// This is an internal helper function that implements the Alternative pattern for
// codec validation. It combines two codec validators using the validate.Alt operation,
// which provides error recovery and fallback logic.
//
// # Type Parameters
//
// - A: The target type that both codecs decode to
// - O: The output type that both codecs encode to
// - I: The input type that both codecs decode from
//
// # Parameters
//
// - first: The primary codec whose validation is tried first
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
// first validation fails.
//
// # Returns
//
// A Validate[I, A] function that tries the first codec's validation, falling back
// to the second if needed. If both fail, errors from both are aggregated.
//
// # Behavior
//
// - **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
//
// # Notes
//
// - The second codec is lazily evaluated for efficiency
// - This function is used internally by MonadAlt and Alt
// - The validation context is threaded through both validators
// - Errors are accumulated using the validation error monoid
func validateAlt[A, O, I any](
first Type[A, O, I],
second Lazy[Type[A, O, I]],
) Validate[I, A] {
return F.Pipe1(
first.Validate,
validate.Alt(F.Pipe1(
second,
lazy.Map(F.Flip(reader.Curry(Type[A, O, I].Validate))),
)),
)
}
// MonadAlt creates a new codec that tries the first codec, and if it fails during
// validation, tries the second codec as a fallback.
//
// This function implements the Alternative typeclass pattern for codecs, enabling
// "try this codec, or else try that codec" logic. It's particularly useful for:
// - Handling multiple valid input formats
// - Providing backward compatibility with legacy formats
// - Implementing graceful degradation in parsing
// - Supporting union types or polymorphic data
//
// The resulting codec uses the first codec's encoder and combines both validators
// using the Alternative pattern. If both validations fail, errors from both are
// aggregated for comprehensive error reporting.
//
// # Type Parameters
//
// - A: The target type that both codecs decode to
// - O: The output type that both codecs encode to
// - I: The input type that both codecs decode from
//
// # Parameters
//
// - first: The primary codec to try first. Its encoder is used for the result.
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
// first validation fails.
//
// # Returns
//
// A new Type[A, O, I] that combines both codecs with Alternative semantics.
//
// # Behavior
//
// **Validation**:
// - **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
//
// **Encoding**:
// - Always uses the first codec's encoder
// - This assumes both codecs encode to the same output format
//
// **Type Checking**:
// - Uses the generic Is[A]() type checker
// - Validates that values are of type A
//
// # Example: Multiple Input Formats
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// )
//
// // Accept integers as either strings or numbers
// intFromString := codec.IntFromString()
// intFromNumber := codec.Int()
//
// // Try parsing as string first, fall back to number
// flexibleInt := codec.MonadAlt(
// intFromString,
// func() codec.Type[int, any, any] { return intFromNumber },
// )
//
// // Can now decode both "42" and 42
// result1 := flexibleInt.Decode("42") // Success(42)
// result2 := flexibleInt.Decode(42) // Success(42)
//
// # Example: Backward Compatibility
//
// // Support both old and new configuration formats
// newConfigCodec := codec.Struct(/* new format */)
// oldConfigCodec := codec.Struct(/* old format */)
//
// // Try new format first, fall back to old format
// configCodec := codec.MonadAlt(
// newConfigCodec,
// func() codec.Type[Config, any, any] { return oldConfigCodec },
// )
//
// // Automatically handles both formats
// config := configCodec.Decode(input)
//
// # Example: Error Aggregation
//
// // Both validations will fail for invalid input
// result := flexibleInt.Decode("not a number")
// // Result contains errors from both string and number parsing attempts
//
// # Notes
//
// - The second codec is lazily evaluated for efficiency
// - First success short-circuits evaluation (second not called)
// - Errors are aggregated when both fail
// - The resulting codec's name is "Alt[<first codec name>]"
// - Both codecs must have compatible input and output types
// - The first codec's encoder is always used
//
// # See Also
//
// - Alt: The curried, point-free version
// - validate.MonadAlt: The underlying validation operation
// - Either: For codecs that decode to Either[L, R] types
func MonadAlt[A, O, I any](first Type[A, O, I], second Lazy[Type[A, O, I]]) Type[A, O, I] {
return MakeType(
fmt.Sprintf("Alt[%s]", first.Name()),
Is[A](),
validateAlt(first, second),
first.Encode,
)
}
// Alt creates an operator that adds alternative fallback logic to a codec.
//
// This is the curried, point-free version of MonadAlt. It returns a function that
// can be applied to codecs to add fallback behavior. This style is particularly
// useful for building codec transformation pipelines using function composition.
//
// Alt implements the Alternative typeclass pattern, enabling "try this codec, or
// else try that codec" logic in a composable way.
//
// # Type Parameters
//
// - A: The target type that both codecs decode to
// - O: The output type that both codecs encode to
// - I: The input type that both codecs decode from
//
// # Parameters
//
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
// first codec's validation fails.
//
// # Returns
//
// An Operator[A, A, O, I] that transforms codecs by adding alternative fallback logic.
// This operator can be applied to any Type[A, O, I] to create a new codec with
// fallback behavior.
//
// # Behavior
//
// When the returned operator is applied to a codec:
// - **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
//
// # Example: Point-Free Style
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/optics/codec"
// )
//
// // Create a reusable fallback operator
// withNumberFallback := codec.Alt(func() codec.Type[int, any, any] {
// return codec.Int()
// })
//
// // Apply it to different codecs
// flexibleInt1 := withNumberFallback(codec.IntFromString())
// flexibleInt2 := withNumberFallback(codec.IntFromHex())
//
// # Example: Pipeline Composition
//
// // Build a codec pipeline with multiple fallbacks
// flexibleCodec := F.Pipe2(
// primaryCodec,
// codec.Alt(func() codec.Type[T, O, I] { return fallback1 }),
// codec.Alt(func() codec.Type[T, O, I] { return fallback2 }),
// )
// // Tries primary, then fallback1, then fallback2
//
// # Example: Reusable Transformations
//
// // Create a transformation that adds JSON fallback
// withJSONFallback := codec.Alt(func() codec.Type[Config, string, string] {
// return codec.JSONCodec[Config]()
// })
//
// // Apply to multiple codecs
// yamlWithFallback := withJSONFallback(yamlCodec)
// tomlWithFallback := withJSONFallback(tomlCodec)
//
// # Notes
//
// - This is the point-free style version of MonadAlt
// - Useful for building transformation pipelines with F.Pipe
// - The second codec is lazily evaluated for efficiency
// - First success short-circuits evaluation
// - Errors are aggregated when both fail
// - Can be composed with other codec operators
//
// # See Also
//
// - MonadAlt: The direct application version
// - validate.Alt: The underlying validation operation
// - F.Pipe: For composing multiple operators
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],
)
}

921
v2/optics/codec/alt_test.go Normal file
View File

@@ -0,0 +1,921 @@
// 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 codec
import (
"fmt"
"strconv"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestMonadAltBasicFunctionality tests the basic behavior of MonadAlt
func TestMonadAltBasicFunctionality(t *testing.T) {
t.Run("uses first codec when it succeeds", func(t *testing.T) {
// Create two codecs that both work with strings
stringCodec := Id[string]()
// Create another string codec that only accepts uppercase
uppercaseOnly := MakeType(
"UppercaseOnly",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
for _, r := range s {
if r >= 'a' && r <= 'z' {
return validation.FailureWithMessage[string](s, "must be uppercase")(c)
}
}
return validation.Success(s)
}
},
F.Identity[string],
)
// Create alt codec that tries uppercase first, then any string
altCodec := MonadAlt(
uppercaseOnly,
func() Type[string, string, string] { return stringCodec },
)
// Test with uppercase string - should succeed with first codec
result := altCodec.Decode("HELLO")
assert.True(t, either.IsRight(result), "should successfully decode with first codec")
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "HELLO", value)
})
t.Run("falls back to second codec when first fails", func(t *testing.T) {
// Create a codec that only accepts positive integers
positiveInt := MakeType(
"PositiveInt",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i <= 0 {
return validation.FailureWithMessage[int](i, "must be positive")(c)
}
return validation.Success(i)
}
},
F.Identity[int],
)
// Create a codec that accepts any integer (with same input type)
anyInt := MakeType(
"AnyInt",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(i)
}
},
F.Identity[int],
)
// Create alt codec
altCodec := MonadAlt(
positiveInt,
func() Type[int, int, int] { return anyInt },
)
// Test with negative number - first fails, second succeeds
result := altCodec.Decode(-5)
assert.True(t, either.IsRight(result), "should successfully decode with second codec")
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
assert.Equal(t, -5, value)
})
t.Run("aggregates errors when both codecs fail", func(t *testing.T) {
// Create two codecs that will both fail
positiveInt := MakeType(
"PositiveInt",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i <= 0 {
return validation.FailureWithMessage[int](i, "must be positive")(c)
}
return validation.Success(i)
}
},
F.Identity[int],
)
evenInt := MakeType(
"EvenInt",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i%2 != 0 {
return validation.FailureWithMessage[int](i, "must be even")(c)
}
return validation.Success(i)
}
},
F.Identity[int],
)
// Create alt codec
altCodec := MonadAlt(
positiveInt,
func() Type[int, int, int] { return evenInt },
)
// Test with -3 (negative and odd) - both should fail
result := altCodec.Decode(-3)
assert.True(t, either.IsLeft(result), "should fail when both codecs fail")
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
// Should have errors from both validation attempts
assert.GreaterOrEqual(t, len(errors), 2, "should have errors from both codecs")
})
}
// TestMonadAltNaming tests that the codec name is correctly generated
func TestMonadAltNaming(t *testing.T) {
t.Run("generates correct name", func(t *testing.T) {
stringCodec := Id[string]()
anotherStringCodec := Id[string]()
altCodec := MonadAlt(
stringCodec,
func() Type[string, string, string] { return anotherStringCodec },
)
assert.Equal(t, "Alt[string]", altCodec.Name())
})
}
// TestMonadAltEncoding tests that encoding uses the first codec's encoder
func TestMonadAltEncoding(t *testing.T) {
t.Run("uses first codec's encoder", func(t *testing.T) {
// Create a codec that encodes ints as strings with prefix
prefixedInt := MakeType(
"PrefixedInt",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
var n int
_, err := fmt.Sscanf(s, "NUM:%d", &n)
if err != nil {
return validation.FailureWithError[int](s, "expected NUM:n format")(err)(c)
}
return validation.Success(n)
}
},
func(n int) string {
return fmt.Sprintf("NUM:%d", n)
},
)
// Create a standard int from string codec
standardInt := IntFromString()
// Create alt codec
altCodec := MonadAlt(
prefixedInt,
func() Type[int, string, string] { return standardInt },
)
// Encode should use first codec's encoder
encoded := altCodec.Encode(42)
assert.Equal(t, "NUM:42", encoded)
})
}
// TestAltOperator tests the curried Alt function
func TestAltOperator(t *testing.T) {
t.Run("creates reusable operator", func(t *testing.T) {
// Create a fallback operator that accepts any string
withStringFallback := Alt(func() Type[string, string, string] {
return Id[string]()
})
// Create a codec that only accepts "hello"
helloOnly := MakeType(
"HelloOnly",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s != "hello" {
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
// Apply fallback to the codec
altCodec := withStringFallback(helloOnly)
// Test that it works
result1 := altCodec.Decode("hello")
assert.True(t, either.IsRight(result1))
result2 := altCodec.Decode("world")
assert.True(t, either.IsRight(result2))
})
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
// Create a codec pipeline with multiple fallbacks
baseCodec := MakeType(
"StrictInt",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
if s != "42" {
return validation.FailureWithMessage[int](s, "must be exactly '42'")(c)
}
return validation.Success(42)
}
},
strconv.Itoa,
)
fallback1 := MakeType(
"Fallback1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
if s != "100" {
return validation.FailureWithMessage[int](s, "must be exactly '100'")(c)
}
return validation.Success(100)
}
},
strconv.Itoa,
)
fallback2 := MakeType(
"AnyInt",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
n, err := strconv.Atoi(s)
if err != nil {
return validation.FailureWithError[int](s, "not an integer")(err)(c)
}
return validation.Success(n)
}
},
strconv.Itoa,
)
// Build pipeline with multiple alternatives
pipeline := F.Pipe2(
baseCodec,
Alt(func() Type[int, string, string] { return fallback1 }),
Alt(func() Type[int, string, string] { return fallback2 }),
)
// Test with "42" - should use base codec
result1 := pipeline.Decode("42")
assert.True(t, either.IsRight(result1))
value1 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result1)
assert.Equal(t, 42, value1)
// Test with "100" - should use fallback1
result2 := pipeline.Decode("100")
assert.True(t, either.IsRight(result2))
value2 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result2)
assert.Equal(t, 100, value2)
// Test with "999" - should use fallback2
result3 := pipeline.Decode("999")
assert.True(t, either.IsRight(result3))
value3 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result3)
assert.Equal(t, 999, value3)
})
}
// TestAltLazyEvaluation tests that the second codec is only evaluated when needed
func TestAltLazyEvaluation(t *testing.T) {
t.Run("does not evaluate second codec when first succeeds", func(t *testing.T) {
evaluated := false
stringCodec := Id[string]()
altCodec := MonadAlt(
stringCodec,
func() Type[string, string, string] {
evaluated = true
return Id[string]()
},
)
// Decode with first codec succeeding
result := altCodec.Decode("hello")
assert.True(t, either.IsRight(result))
// Second codec should not have been evaluated
assert.False(t, evaluated, "second codec should not be evaluated when first succeeds")
})
t.Run("evaluates second codec when first fails", func(t *testing.T) {
evaluated := false
// Create a codec that always fails
failingCodec := MakeType(
"Failing",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
return validation.FailureWithMessage[string](s, "always fails")(c)
}
},
F.Identity[string],
)
altCodec := MonadAlt(
failingCodec,
func() Type[string, string, string] {
evaluated = true
return Id[string]()
},
)
// Decode with first codec failing
result := altCodec.Decode("hello")
assert.True(t, either.IsRight(result))
// Second codec should have been evaluated
assert.True(t, evaluated, "second codec should be evaluated when first fails")
})
}
// TestAltWithComplexTypes tests Alt with more complex codec scenarios
func TestAltWithComplexTypes(t *testing.T) {
t.Run("works with string length validation", func(t *testing.T) {
// Create codec that accepts strings of length 5
length5 := MakeType(
"Length5",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if len(s) != 5 {
return validation.FailureWithMessage[string](s, "must be length 5")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
// Create codec that accepts any string
anyString := Id[string]()
// Create alt codec
altCodec := MonadAlt(
length5,
func() Type[string, string, string] { return anyString },
)
// Test with length 5 - should use first codec
result1 := altCodec.Decode("hello")
assert.True(t, either.IsRight(result1))
// Test with different length - should fall back to second codec
result2 := altCodec.Decode("hi")
assert.True(t, either.IsRight(result2))
})
}
// TestAltTypeChecking tests that type checking works correctly
func TestAltTypeChecking(t *testing.T) {
t.Run("type checking uses generic Is", func(t *testing.T) {
stringCodec := Id[string]()
anotherStringCodec := Id[string]()
altCodec := MonadAlt(
stringCodec,
func() Type[string, string, string] { return anotherStringCodec },
)
// Test Is with valid type
result1 := altCodec.Is("hello")
assert.True(t, either.IsRight(result1))
// Test Is with invalid type
result2 := altCodec.Is(42)
assert.True(t, either.IsLeft(result2))
})
}
// TestAltRoundTrip tests encoding and decoding round trips
func TestAltRoundTrip(t *testing.T) {
t.Run("round-trip with first codec", func(t *testing.T) {
stringCodec := Id[string]()
anotherStringCodec := Id[string]()
altCodec := MonadAlt(
stringCodec,
func() Type[string, string, string] { return anotherStringCodec },
)
original := "hello"
// Decode
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
// Encode
encoded := altCodec.Encode(decoded)
// Verify
assert.Equal(t, original, encoded)
})
t.Run("round-trip with second codec", func(t *testing.T) {
// Create a codec that only accepts "hello"
helloOnly := MakeType(
"HelloOnly",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s != "hello" {
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
anyString := Id[string]()
altCodec := MonadAlt(
helloOnly,
func() Type[string, string, string] { return anyString },
)
original := "world"
// Decode (will use second codec)
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
// Encode (uses first codec's encoder, which is identity)
encoded := altCodec.Encode(decoded)
// Verify
assert.Equal(t, original, encoded)
})
}
// TestAltErrorMessages tests that error messages are informative
func TestAltErrorMessages(t *testing.T) {
t.Run("provides detailed error context", func(t *testing.T) {
// Create two codecs with specific error messages
codec1 := MakeType(
"Codec1",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](i, "codec1 error")(c)
}
},
F.Identity[int],
)
codec2 := MakeType(
"Codec2",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](i, "codec2 error")(c)
}
},
F.Identity[int],
)
altCodec := MonadAlt(
codec1,
func() Type[int, int, int] { return codec2 },
)
result := altCodec.Decode(42)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
require.GreaterOrEqual(t, len(errors), 2)
// Check that both error messages are present
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
hasCodec1Error := false
hasCodec2Error := false
for _, msg := range messages {
if msg == "codec1 error" {
hasCodec1Error = true
}
if msg == "codec2 error" {
hasCodec2Error = true
}
}
assert.True(t, hasCodec1Error, "should have error from first codec")
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

@@ -0,0 +1,321 @@
# ChainLeft and OrElse in the Decode Package
## Overview
In [`optics/codec/decode/monad.go`](monad.go:53-62), the [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) functions work with decoders that may fail during decoding operations.
```go
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return readert.Chain[Decode[I, A]](
validation.ChainLeft,
f,
)
}
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return ChainLeft(f)
}
```
## Key Insight: OrElse is ChainLeft
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**.
## Understanding the Types
### Decode[I, A]
A decoder that takes input of type `I` and produces a `Validation[A]`:
```go
type Decode[I, A any] = func(I) Validation[A]
```
### Kleisli[I, Errors, A]
A function that takes `Errors` and produces a `Decode[I, A]`:
```go
type Kleisli[I, Errors, A] = func(Errors) Decode[I, A]
```
This allows error handlers to:
1. Access the validation errors that occurred
2. Access the original input (via the returned Decode function)
3. Either recover with a success value or produce new errors
### Operator[I, A, A]
A function that transforms one decoder into another:
```go
type Operator[I, A, A] = func(Decode[I, A]) Decode[I, A]
```
## Core Behavior
Both [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) delegate to [`validation.ChainLeft`](../validation/monad.go:304), which provides:
### 1. Error Aggregation
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid:
```go
failingDecoder := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "original error"},
})
}
handler := ChainLeft(func(errs Errors) Decode[string, int] {
return func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "additional error"},
})
}
})
decoder := handler(failingDecoder)
result := decoder("input")
// Result contains BOTH errors: ["original error", "additional error"]
```
### 2. Success Pass-Through
Success values pass through unchanged - the handler is never called:
```go
successDecoder := Of[string](42)
handler := ChainLeft(func(errs Errors) Decode[string, int] {
return func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "never called"},
})
}
})
decoder := handler(successDecoder)
result := decoder("input")
// Result: Success(42) - unchanged
```
### 3. Error Recovery
The handler can recover from failures by returning a successful decoder:
```go
failingDecoder := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "not found"},
})
}
recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
for _, err := range errs {
if err.Messsage == "not found" {
return Of[string](0) // recover with default
}
}
return func(input string) Validation[int] {
return either.Left[int](errs)
}
})
decoder := recoverFromNotFound(failingDecoder)
result := decoder("input")
// Result: Success(0) - recovered from failure
```
### 4. Access to Original Input
The handler returns a `Decode[I, A]` function, giving it access to the original input:
```go
handler := ChainLeft(func(errs Errors) Decode[string, int] {
return func(input string) Validation[int] {
// Can access both errs and input here
if input == "special" {
return validation.Of(999)
}
return either.Left[int](errs)
}
})
```
## Use Cases
### 1. Fallback Decoding (OrElse reads better)
```go
// Primary decoder that may fail
primaryDecoder := func(input string) Validation[int] {
n, err := strconv.Atoi(input)
if err != nil {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "not a valid integer"},
})
}
return validation.Of(n)
}
// Use OrElse for semantic clarity - "try primary, or else use default"
withDefault := OrElse(func(errs Errors) Decode[string, int] {
return Of[string](0) // default to 0 if decoding fails
})
decoder := withDefault(primaryDecoder)
result1 := decoder("42") // Success(42)
result2 := decoder("abc") // Success(0) - fallback
```
### 2. Error Context Addition (ChainLeft reads better)
```go
decodeUserAge := func(data map[string]any) Validation[int] {
age, ok := data["age"].(int)
if !ok {
return either.Left[int](validation.Errors{
{Value: data["age"], Messsage: "invalid type"},
})
}
return validation.Of(age)
}
// Use ChainLeft when emphasizing error transformation
addContext := ChainLeft(func(errs Errors) Decode[map[string]any, int] {
return func(data map[string]any) Validation[int] {
return either.Left[int](validation.Errors{
{
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
Messsage: "failed to decode user age",
},
})
}
})
decoder := addContext(decodeUserAge)
// Errors will include both original error and context
```
### 3. Conditional Recovery Based on Input
```go
decodePort := func(input string) Validation[int] {
port, err := strconv.Atoi(input)
if err != nil {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "invalid port"},
})
}
return validation.Of(port)
}
// Recover with different defaults based on input
smartDefault := OrElse(func(errs Errors) Decode[string, int] {
return func(input string) Validation[int] {
// Check input to determine appropriate default
if strings.Contains(input, "http") {
return validation.Of(80)
}
if strings.Contains(input, "https") {
return validation.Of(443)
}
return validation.Of(8080)
}
})
decoder := smartDefault(decodePort)
result1 := decoder("http-server") // Success(80)
result2 := decoder("https-server") // Success(443)
result3 := decoder("other") // Success(8080)
```
### 4. Pipeline Composition
```go
type Config struct {
DatabaseURL string
}
decodeConfig := func(data map[string]any) Validation[Config] {
url, ok := data["db_url"].(string)
if !ok {
return either.Left[Config](validation.Errors{
{Messsage: "missing db_url"},
})
}
return validation.Of(Config{DatabaseURL: url})
}
// Build a pipeline with multiple error handlers
decoder := F.Pipe2(
decodeConfig,
OrElse(func(errs Errors) Decode[map[string]any, Config] {
// Try environment variable as fallback
return func(data map[string]any) Validation[Config] {
if url := os.Getenv("DATABASE_URL"); url != "" {
return validation.Of(Config{DatabaseURL: url})
}
return either.Left[Config](errs)
}
}),
OrElse(func(errs Errors) Decode[map[string]any, Config] {
// Final fallback to default
return Of[map[string]any](Config{
DatabaseURL: "localhost:5432",
})
}),
)
```
## Comparison with validation.ChainLeft
The decode package's [`ChainLeft`](monad.go:53) wraps [`validation.ChainLeft`](../validation/monad.go:304) using the Reader transformer pattern:
| Aspect | validation.ChainLeft | decode.ChainLeft |
|--------|---------------------|------------------|
| **Input** | `Validation[A]` | `Decode[I, A]` (function) |
| **Handler** | `func(Errors) Validation[A]` | `func(Errors) Decode[I, A]` |
| **Output** | `Validation[A]` | `Decode[I, A]` (function) |
| **Context** | No input access | Access to original input `I` |
| **Use Case** | Pure validation logic | Decoding with input-dependent recovery |
The key difference is that decode's version gives handlers access to the original input through the returned `Decode[I, A]` function.
## When to Use Which Name
### Use **OrElse** when:
- Emphasizing fallback/alternative decoding logic
- Providing default values on decode failure
- The intent is "try this, or else try that"
- Code reads more naturally with "or else"
### Use **ChainLeft** when:
- Emphasizing technical error channel transformation
- Adding context or enriching error information
- The focus is on error handling mechanics
- Working with other functional programming concepts
## Verification
The test suite in [`monad_test.go`](monad_test.go:385) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
- ✅ Identical behavior for Success values
- ✅ Identical behavior for error recovery
- ✅ Identical behavior for error aggregation
- ✅ Identical behavior in pipeline composition
- ✅ Identical behavior for multiple error scenarios
- ✅ Both provide access to original input
Run the tests:
```bash
go test -v -run "TestChainLeft|TestOrElse" ./optics/codec/decode
```
## Conclusion
**`OrElse` is exactly the same as `ChainLeft`** in the decode package - they are aliases with identical implementations and behavior. Both:
1. **Delegate to validation.ChainLeft** for error handling logic
2. **Aggregate errors** when transformations fail
3. **Preserve successes** unchanged
4. **Enable recovery** from decode failures
5. **Provide access** to the original input
The choice between them is purely about **code readability and semantic intent**:
- Use **`OrElse`** when emphasizing fallback/alternative decoding
- Use **`ChainLeft`** when emphasizing error transformation
Both maintain the critical property of **error aggregation**, ensuring all validation failures are preserved and reported together.

View File

@@ -1,9 +1,10 @@
package decode
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
)
// Of creates a Decode that always succeeds with the given value.
@@ -14,7 +15,82 @@ import (
// decoder := decode.Of[string](42)
// result := decoder("any input") // Always returns validation.Success(42)
func Of[I, A any](a A) Decode[I, A] {
return reader.Of[I](validation.Of(a))
return readereither.Of[I, Errors](a)
}
// Left creates a Decode that always fails with the given validation errors.
// This is the dual of Of - while Of lifts a success value, Left lifts failure errors
// into the Decode context.
//
// Left is useful for:
// - Creating decoders that represent known failure states
// - Short-circuiting decode pipelines with specific errors
// - Building custom validation error responses
// - Testing error handling paths
//
// The returned decoder ignores its input and always returns a validation failure
// containing the provided errors. This makes it the identity element for the
// Alt/OrElse operations when used as a fallback.
//
// Type signature: func(Errors) Decode[I, A]
// - Takes validation errors
// - Returns a decoder that always fails with those errors
// - The decoder ignores its input of type I
// - The failure type A can be any type (phantom type)
//
// Example - Creating a failing decoder:
//
// failDecoder := decode.Left[string, int](validation.Errors{
// &validation.ValidationError{
// Value: nil,
// Messsage: "operation not supported",
// },
// })
// result := failDecoder("any input") // Always fails with the error
//
// Example - Short-circuiting with specific errors:
//
// validateAge := func(age int) Decode[map[string]any, int] {
// if age < 0 {
// return decode.Left[map[string]any, int](validation.Errors{
// &validation.ValidationError{
// Value: age,
// Context: validation.Context{{Key: "age", Type: "int"}},
// Messsage: "age cannot be negative",
// },
// })
// }
// return decode.Of[map[string]any](age)
// }
//
// Example - Building error responses:
//
// notFoundError := decode.Left[string, User](validation.Errors{
// &validation.ValidationError{
// Messsage: "user not found",
// },
// })
//
// decoder := decode.MonadAlt(
// tryFindUser,
// func() Decode[string, User] { return notFoundError },
// )
//
// Example - Testing error paths:
//
// // Create a decoder that always fails for testing
// alwaysFails := decode.Left[string, int](validation.Errors{
// &validation.ValidationError{Messsage: "test error"},
// })
//
// // Test error recovery logic
// recovered := decode.OrElse(func(errs Errors) Decode[string, int] {
// return decode.Of[string](0) // recover with default
// })(alwaysFails)
//
// result := recovered("input") // Success(0)
func Left[I, A any](err Errors) Decode[I, A] {
return readereither.Left[I, A](err)
}
// MonadChain sequences two decode operations, passing the result of the first to the second.
@@ -50,6 +126,60 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
)
}
// ChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
// This is the left-biased monadic chain operation that operates on validation failures.
//
// **Key behaviors**:
// - Success values pass through unchanged - the handler is never called
// - On failure, the handler receives the errors and can recover or add context
// - When the handler also fails, **both original and new errors are aggregated**
// - The handler returns a Decode[I, A], giving it access to the original input
//
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
// returns a failure, both the original errors AND the new errors are combined using the
// Errors monoid. This ensures no validation errors are lost.
//
// Use cases:
// - Adding contextual information to validation errors
// - Recovering from specific error conditions
// - Transforming error messages while preserving original errors
// - Implementing conditional recovery based on error types
//
// Example - Error recovery:
//
// failingDecoder := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not found"},
// })
// }
//
// recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
// for _, err := range errs {
// if err.Messsage == "not found" {
// return Of[string](0) // recover with default
// }
// }
// return func(input string) Validation[int] {
// return either.Left[int](errs)
// }
// })
//
// decoder := recoverFromNotFound(failingDecoder)
// result := decoder("input") // Success(0) - recovered from failure
//
// Example - Adding context:
//
// addContext := ChainLeft(func(errs Errors) Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to decode user age",
// },
// })
// }
// })
// // Result will contain BOTH original error and context error
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return readert.Chain[Decode[I, A]](
validation.ChainLeft,
@@ -57,6 +187,151 @@ func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
)
}
// MonadChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
// This is the uncurried version of ChainLeft, taking both the decoder and the transformation function directly.
//
// **Key behaviors**:
// - Success values pass through unchanged - the handler is never called
// - On failure, the handler receives the errors and can recover or add context
// - When the handler also fails, **both original and new errors are aggregated**
// - The handler returns a Decode[I, A], giving it access to the original input
//
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
// returns a failure, both the original errors AND the new errors are combined using the
// Errors monoid. This ensures no validation errors are lost.
//
// This function is the direct, uncurried form of ChainLeft. Use ChainLeft when you need
// a curried operator for composition pipelines, and use MonadChainLeft when you have both
// the decoder and transformation function available at once.
//
// Use cases:
// - Adding contextual information to validation errors
// - Recovering from specific error conditions
// - Transforming error messages while preserving original errors
// - Implementing conditional recovery based on error types
//
// Example - Error recovery:
//
// failingDecoder := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not found"},
// })
// }
//
// recoverFromNotFound := func(errs Errors) Decode[string, int] {
// for _, err := range errs {
// if err.Messsage == "not found" {
// return Of[string](0) // recover with default
// }
// }
// return func(input string) Validation[int] {
// return either.Left[int](errs)
// }
// }
//
// decoder := MonadChainLeft(failingDecoder, recoverFromNotFound)
// result := decoder("input") // Success(0) - recovered from failure
//
// Example - Adding context:
//
// addContext := func(errs Errors) Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to decode user age",
// },
// })
// }
// }
//
// decoder := MonadChainLeft(failingDecoder, addContext)
// result := decoder("abc")
// // Result will contain BOTH original error and context error
//
// Example - Comparison with ChainLeft:
//
// // MonadChainLeft - direct application
// result1 := MonadChainLeft(decoder, handler)("input")
//
// // ChainLeft - curried for pipelines
// result2 := ChainLeft(handler)(decoder)("input")
//
// // Both produce identical results
func MonadChainLeft[I, A any](fa Decode[I, A], f Kleisli[I, Errors, A]) Decode[I, A] {
return readert.MonadChain(
validation.MonadChainLeft,
fa,
f,
)
}
// OrElse provides fallback decoding logic when the primary decoder fails.
// This is an alias for ChainLeft with a more semantic name for fallback scenarios.
//
// **OrElse is exactly the same as ChainLeft** - they are aliases with identical implementations
// and behavior. The choice between them is purely about code readability and semantic intent:
// - Use **OrElse** when emphasizing fallback/alternative decoding logic
// - Use **ChainLeft** when emphasizing technical error channel transformation
//
// **Key behaviors** (identical to ChainLeft):
// - Success values pass through unchanged - the handler is never called
// - On failure, the handler receives the errors and can provide an alternative
// - When the handler also fails, **both original and new errors are aggregated**
// - The handler returns a Decode[I, A], giving it access to the original input
//
// The name "OrElse" reads naturally in code: "try this decoder, or else try this alternative."
// This makes it ideal for expressing fallback logic and default values.
//
// Use cases:
// - Providing default values when decoding fails
// - Trying alternative decoding strategies
// - Implementing fallback chains with multiple alternatives
// - Input-dependent recovery (using access to original input)
//
// Example - Simple fallback:
//
// primaryDecoder := func(input string) Validation[int] {
// n, err := strconv.Atoi(input)
// if err != nil {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not a valid integer"},
// })
// }
// return validation.Of(n)
// }
//
// withDefault := OrElse(func(errs Errors) Decode[string, int] {
// return Of[string](0) // default to 0 if decoding fails
// })
//
// decoder := withDefault(primaryDecoder)
// result1 := decoder("42") // Success(42)
// result2 := decoder("abc") // Success(0) - fallback
//
// Example - Input-dependent fallback:
//
// smartDefault := OrElse(func(errs Errors) Decode[string, int] {
// return func(input string) Validation[int] {
// // Access original input to determine appropriate default
// if strings.Contains(input, "http") {
// return validation.Of(80)
// }
// if strings.Contains(input, "https") {
// return validation.Of(443)
// }
// return validation.Of(8080)
// }
// })
//
// decoder := smartDefault(decodePort)
// result1 := decoder("http-server") // Success(80)
// result2 := decoder("https-server") // Success(443)
// result3 := decoder("other") // Success(8080)
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return ChainLeft(f)
}
// MonadMap transforms the decoded value using the provided function.
// This is the functor map operation that applies a transformation to successful decode results.
//
@@ -134,3 +409,155 @@ func Ap[B, I, A any](fa Decode[I, A]) Operator[I, func(A) B, B] {
fa,
)
}
// MonadAlt provides alternative/fallback decoding with error aggregation.
// This is the Alternative pattern's core operation that tries the first decoder,
// and if it fails, tries the second decoder as a fallback.
//
// **Key behaviors**:
// - If first succeeds: returns the first result (second is never evaluated)
// - If first fails and second succeeds: returns the second result
// - If both fail: **aggregates errors from both decoders**
//
// **Error Aggregation**: Unlike simple fallback patterns, when both decoders fail,
// MonadAlt combines ALL errors from both attempts using the Errors monoid. This ensures
// complete visibility into why all alternatives failed, which is crucial for debugging
// and providing comprehensive error messages to users.
//
// The name "Alt" comes from the Alternative type class in functional programming,
// which represents computations with a notion of choice and failure.
//
// Use cases:
// - Trying multiple decoding strategies for the same input
// - Providing fallback decoders when primary decoder fails
// - Building validation pipelines with multiple alternatives
// - Implementing "try this, or else try that" logic
//
// Example - Simple fallback:
//
// primaryDecoder := func(input string) Validation[int] {
// n, err := strconv.Atoi(input)
// if err != nil {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not a valid integer"},
// })
// }
// return validation.Of(n)
// }
//
// fallbackDecoder := func() Decode[string, int] {
// return func(input string) Validation[int] {
// // Try parsing as float and converting to int
// f, err := strconv.ParseFloat(input, 64)
// if err != nil {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not a valid number"},
// })
// }
// return validation.Of(int(f))
// }
// }
//
// decoder := MonadAlt(primaryDecoder, fallbackDecoder)
// result1 := decoder("42") // Success(42) - primary succeeds
// result2 := decoder("42.5") // Success(42) - fallback succeeds
// result3 := decoder("abc") // Failures with both errors aggregated
//
// Example - Multiple alternatives:
//
// decoder1 := parseAsJSON
// decoder2 := func() Decode[string, Config] { return parseAsYAML }
// decoder3 := func() Decode[string, Config] { return parseAsINI }
//
// // Try JSON, then YAML, then INI
// decoder := MonadAlt(MonadAlt(decoder1, decoder2), decoder3)
// // If all fail, errors from all three attempts are aggregated
//
// Example - Error aggregation:
//
// failing1 := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Messsage: "primary decoder failed"},
// })
// }
// failing2 := func() Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Messsage: "fallback decoder failed"},
// })
// }
// }
//
// decoder := MonadAlt(failing1, failing2)
// result := decoder("input")
// // Result contains BOTH errors: ["primary decoder failed", "fallback decoder failed"]
func MonadAlt[I, A any](first Decode[I, A], second Lazy[Decode[I, A]]) Decode[I, A] {
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
}
// Alt creates an operator that provides alternative/fallback decoding with error aggregation.
// This is the curried version of MonadAlt, useful for composition pipelines.
//
// **Key behaviors** (identical to MonadAlt):
// - If first succeeds: returns the first result (second is never evaluated)
// - If first fails and second succeeds: returns the second result
// - If both fail: **aggregates errors from both decoders**
//
// The Alt operator enables building reusable fallback chains that can be applied
// to different decoders. It reads naturally in pipelines: "apply this decoder,
// with this alternative if it fails."
//
// Use cases:
// - Creating reusable fallback strategies
// - Building decoder combinators with alternatives
// - Composing multiple fallback layers
// - Implementing retry logic with different strategies
//
// Example - Creating a reusable fallback:
//
// // Create an operator that falls back to a default value
// withDefault := Alt(func() Decode[string, int] {
// return Of[string](0)
// })
//
// // Apply to any decoder
// decoder1 := withDefault(parseInteger)
// decoder2 := withDefault(parseFromJSON)
//
// result1 := decoder1("42") // Success(42)
// result2 := decoder1("abc") // Success(0) - fallback
//
// Example - Composing multiple alternatives:
//
// tryYAML := Alt(func() Decode[string, Config] { return parseAsYAML })
// tryINI := Alt(func() Decode[string, Config] { return parseAsINI })
// useDefault := Alt(func() Decode[string, Config] {
// return Of[string](defaultConfig)
// })
//
// // Build a pipeline: try JSON, then YAML, then INI, then default
// decoder := useDefault(tryINI(tryYAML(parseAsJSON)))
//
// Example - Error aggregation in pipeline:
//
// failing1 := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{{Messsage: "error 1"}})
// }
// failing2 := func() Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{{Messsage: "error 2"}})
// }
// }
// failing3 := func() Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{{Messsage: "error 3"}})
// }
// }
//
// // Chain multiple alternatives
// decoder := Alt(failing3)(Alt(failing2)(failing1))
// result := decoder("input")
// // Result contains ALL errors: ["error 1", "error 2", "error 3"]
func Alt[I, A any](second Lazy[Decode[I, A]]) Operator[I, A, A] {
return ChainLeft(function.Ignore1of1[Errors](second))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,368 @@
package decode
import "github.com/IBM/fp-go/v2/monoid"
// ApplicativeMonoid creates a Monoid instance for Decode[I, A] given a Monoid for A.
// This allows combining decoders where both the decoded values and validation errors
// are combined according to their respective monoid operations.
//
// The resulting monoid enables:
// - Combining multiple decoders that produce monoidal values
// - Accumulating validation errors when any decoder fails
// - Building complex decoders from simpler ones through composition
//
// **Behavior**:
// - Empty: Returns a decoder that always succeeds with the empty value from the inner monoid
// - Concat: Combines two decoders:
// - Both succeed: Combines decoded values using the inner monoid
// - Any fails: Accumulates all validation errors using the Errors monoid
//
// This is particularly useful for:
// - Aggregating results from multiple independent decoders
// - Building decoders that combine partial results
// - Validating and combining configuration from multiple sources
// - Parallel validation with result accumulation
//
// Example - Combining string decoders:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// // Create a monoid for decoders that produce strings
// m := ApplicativeMonoid[map[string]any](S.Monoid)
//
// decoder1 := func(data map[string]any) Validation[string] {
// if name, ok := data["firstName"].(string); ok {
// return validation.Of(name)
// }
// return either.Left[string](validation.Errors{
// {Messsage: "missing firstName"},
// })
// }
//
// decoder2 := func(data map[string]any) Validation[string] {
// if name, ok := data["lastName"].(string); ok {
// return validation.Of(" " + name)
// }
// return either.Left[string](validation.Errors{
// {Messsage: "missing lastName"},
// })
// }
//
// // Combine decoders - will concatenate strings if both succeed
// combined := m.Concat(decoder1, decoder2)
// result := combined(map[string]any{
// "firstName": "John",
// "lastName": "Doe",
// }) // Success("John Doe")
//
// Example - Error accumulation:
//
// // If any decoder fails, errors are accumulated
// result := combined(map[string]any{}) // Failures with both error messages
//
// Example - Numeric aggregation:
//
// import N "github.com/IBM/fp-go/v2/number"
//
// intMonoid := monoid.MakeMonoid(N.Add[int], 0)
// m := ApplicativeMonoid[string](intMonoid)
//
// decoder1 := func(input string) Validation[int] {
// return validation.Of(10)
// }
// decoder2 := func(input string) Validation[int] {
// return validation.Of(32)
// }
//
// combined := m.Concat(decoder1, decoder2)
// result := combined("input") // Success(42) - values are added
func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Decode[I, A]] {
return monoid.ApplicativeMonoid(
Of[I, A],
MonadMap[I, A, Endomorphism[A]],
MonadAp[A, I, A],
m,
)
}
// AlternativeMonoid creates a Monoid instance for Decode[I, A] using the Alternative pattern.
// This combines applicative error-accumulation behavior with alternative fallback behavior,
// allowing you to both accumulate errors and provide fallback alternatives when combining decoders.
//
// The Alternative pattern provides two key operations:
// - Applicative operations (Of, Map, Ap): accumulate errors when combining decoders
// - Alternative operation (Alt): provide fallback when a decoder fails
//
// This monoid is particularly useful when you want to:
// - Try multiple decoding strategies and fall back to alternatives
// - Combine successful values using the provided monoid
// - Accumulate all errors from failed attempts
// - Build decoding pipelines with fallback logic
//
// **Behavior**:
// - Empty: Returns a decoder that always succeeds with the empty value from the inner monoid
// - Concat: Combines two decoders using both applicative and alternative semantics:
// - If first succeeds and second succeeds: combines decoded values using inner monoid
// - If first fails: tries second as fallback (alternative behavior)
// - If both fail: **accumulates all errors from both decoders**
//
// **Error Aggregation**: When both decoders fail, all validation errors from both attempts
// are combined using the Errors monoid. This provides complete visibility into why all
// alternatives failed, which is essential for debugging and user feedback.
//
// Type Parameters:
// - I: The input type being decoded
// - A: The output type after successful decoding
//
// Parameters:
// - m: The monoid for combining successful decoded values of type A
//
// Returns:
//
// A Monoid[Decode[I, A]] that combines applicative and alternative behaviors
//
// Example - Combining successful decoders:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// m := AlternativeMonoid[string](S.Monoid)
//
// decoder1 := func(input string) Validation[string] {
// return validation.Of("Hello")
// }
// decoder2 := func(input string) Validation[string] {
// return validation.Of(" World")
// }
//
// combined := m.Concat(decoder1, decoder2)
// result := combined("input")
// // Result: Success("Hello World") - values combined using string monoid
//
// Example - Fallback behavior:
//
// m := AlternativeMonoid[string](S.Monoid)
//
// failing := func(input string) Validation[string] {
// return either.Left[string](validation.Errors{
// {Value: input, Messsage: "primary failed"},
// })
// }
// fallback := func(input string) Validation[string] {
// return validation.Of("fallback value")
// }
//
// combined := m.Concat(failing, fallback)
// result := combined("input")
// // Result: Success("fallback value") - second decoder used as fallback
//
// Example - Error accumulation when both fail:
//
// m := AlternativeMonoid[string](S.Monoid)
//
// failing1 := func(input string) Validation[string] {
// return either.Left[string](validation.Errors{
// {Value: input, Messsage: "error 1"},
// })
// }
// failing2 := func(input string) Validation[string] {
// return either.Left[string](validation.Errors{
// {Value: input, Messsage: "error 2"},
// })
// }
//
// combined := m.Concat(failing1, failing2)
// result := combined("input")
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
//
// Example - Building decoder with multiple fallbacks:
//
// import N "github.com/IBM/fp-go/v2/number"
//
// m := AlternativeMonoid[string](N.MonoidSum[int]())
//
// // Try to parse from different formats
// parseJSON := func(input string) Validation[int] { /* ... */ }
// parseYAML := func(input string) Validation[int] { /* ... */ }
// parseINI := func(input string) Validation[int] { /* ... */ }
//
// // Combine with fallback chain
// decoder := m.Concat(m.Concat(parseJSON, parseYAML), parseINI)
// // Uses first successful parser, or accumulates all errors if all fail
//
// Example - Combining multiple configuration sources:
//
// type Config struct{ Port int }
// configMonoid := monoid.MakeMonoid(
// func(a, b Config) Config {
// if b.Port != 0 { return b }
// return a
// },
// Config{Port: 0},
// )
//
// m := AlternativeMonoid[map[string]any](configMonoid)
//
// fromEnv := func(data map[string]any) Validation[Config] { /* ... */ }
// fromFile := func(data map[string]any) Validation[Config] { /* ... */ }
// fromDefault := func(data map[string]any) Validation[Config] {
// return validation.Of(Config{Port: 8080})
// }
//
// // Try env, then file, then default
// decoder := m.Concat(m.Concat(fromEnv, fromFile), fromDefault)
// // Returns first successful config, or all errors if all fail
func AlternativeMonoid[I, A any](m Monoid[A]) Monoid[Decode[I, A]] {
return monoid.AlternativeMonoid(
Of[I, A],
MonadMap[I, A, func(A) A],
MonadAp[A, I, A],
MonadAlt[I, A],
m,
)
}
// AltMonoid creates a Monoid instance for Decode[I, A] using the Alt (alternative) operation.
// This monoid provides a way to combine decoders with fallback behavior, where the second
// decoder is used as an alternative if the first one fails.
//
// The Alt operation implements the "try first, fallback to second" pattern, which is useful
// for decoding scenarios where you want to attempt multiple decoding strategies in sequence
// and use the first one that succeeds.
//
// **Behavior**:
// - Empty: Returns the provided zero value (a lazy computation that produces a Decode[I, A])
// - Concat: Combines two decoders using Alt semantics:
// - If first succeeds: returns the first result (second is never evaluated)
// - If first fails: tries the second decoder as fallback
// - If both fail: **aggregates errors from both decoders**
//
// **Error Aggregation**: When both decoders fail, all validation errors from both attempts
// are combined using the Errors monoid. This ensures complete visibility into why all
// alternatives failed.
//
// This is different from [AlternativeMonoid] in that:
// - AltMonoid uses a custom zero value (provided by the user)
// - AlternativeMonoid derives the zero from an inner monoid
// - AltMonoid is simpler and only provides fallback behavior
// - AlternativeMonoid combines applicative and alternative behaviors
//
// Type Parameters:
// - I: The input type being decoded
// - A: The output type after successful decoding
//
// Parameters:
// - zero: A lazy computation that produces the identity/empty Decode[I, A].
// This is typically a decoder that always succeeds with a default value, or could be
// a decoder that always fails representing "no decoding attempted"
//
// Returns:
//
// A Monoid[Decode[I, A]] that combines decoders with fallback behavior
//
// Example - Using default value as zero:
//
// m := AltMonoid(func() Decode[string, int] {
// return Of[string](0)
// })
//
// failing := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "failed"},
// })
// }
// succeeding := func(input string) Validation[int] {
// return validation.Of(42)
// }
//
// combined := m.Concat(failing, succeeding)
// result := combined("input")
// // Result: Success(42) - falls back to second decoder
//
// empty := m.Empty()
// result2 := empty("input")
// // Result: Success(0) - the provided zero value
//
// Example - Chaining multiple fallbacks:
//
// m := AltMonoid(func() Decode[string, Config] {
// return Of[string](defaultConfig)
// })
//
// primary := parseFromPrimarySource // Fails
// secondary := parseFromSecondarySource // Fails
// tertiary := parseFromTertiarySource // Succeeds
//
// // Chain fallbacks
// decoder := m.Concat(m.Concat(primary, secondary), tertiary)
// result := decoder("input")
// // Result: Success from tertiary - uses first successful decoder
//
// Example - Error aggregation when all fail:
//
// m := AltMonoid(func() Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Messsage: "no default available"},
// })
// }
// })
//
// failing1 := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "error 1"},
// })
// }
// failing2 := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "error 2"},
// })
// }
//
// combined := m.Concat(failing1, failing2)
// result := combined("input")
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
//
// Example - Building a decoder pipeline with fallbacks:
//
// m := AltMonoid(func() Decode[string, Config] {
// return Of[string](defaultConfig)
// })
//
// // Try multiple decoding sources in order
// decoders := []Decode[string, Config]{
// loadFromFile("config.json"), // Try file first
// loadFromEnv, // Then environment
// loadFromRemote("api.example.com"), // Then remote API
// }
//
// // Fold using the monoid to get first successful config
// result := array.MonoidFold(m)(decoders)
// // Result: First successful config, or defaultConfig if all fail
//
// Example - Comparing with AlternativeMonoid:
//
// // AltMonoid - simple fallback with custom zero
// altM := AltMonoid(func() Decode[string, int] {
// return Of[string](0)
// })
//
// // AlternativeMonoid - combines values when both succeed
// import N "github.com/IBM/fp-go/v2/number"
// altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
//
// decoder1 := Of[string](10)
// decoder2 := Of[string](32)
//
// // AltMonoid: returns first success (10)
// result1 := altM.Concat(decoder1, decoder2)("input")
// // Result: Success(10)
//
// // AlternativeMonoid: combines both successes (10 + 32 = 42)
// result2 := altMonoid.Concat(decoder1, decoder2)("input")
// // Result: Success(42)
func AltMonoid[I, A any](zero Lazy[Decode[I, A]]) Monoid[Decode[I, A]] {
return monoid.AltMonoid(
zero,
MonadAlt[I, A],
)
}

View File

@@ -0,0 +1,970 @@
package decode
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
MO "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/optics/codec/validation"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestApplicativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := ApplicativeMonoid[string](S.Monoid)
t.Run("empty returns decoder that succeeds with empty string", func(t *testing.T) {
empty := m.Empty()
result := empty("any input")
assert.Equal(t, validation.Of(""), result)
})
t.Run("concat combines successful decoders", func(t *testing.T) {
decoder1 := Of[string]("Hello")
decoder2 := Of[string](" World")
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.Equal(t, validation.Of("Hello World"), result)
})
t.Run("concat with failure returns failure", func(t *testing.T) {
decoder1 := Of[string]("Hello")
decoder2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "decode failed"},
})
}
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "decode failed", errors[0].Messsage)
})
t.Run("concat accumulates all errors from both failures", func(t *testing.T) {
decoder1 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
decoder2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("concat with empty preserves decoder", func(t *testing.T) {
decoder := Of[string]("test")
empty := m.Empty()
result1 := m.Concat(decoder, empty)("input")
result2 := m.Concat(empty, decoder)("input")
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
})
})
t.Run("with int addition monoid", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := ApplicativeMonoid[string](intMonoid)
t.Run("empty returns decoder with zero", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("concat adds decoded values", func(t *testing.T) {
decoder1 := Of[string](10)
decoder2 := Of[string](32)
combined := m.Concat(decoder1, decoder2)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("multiple concat operations", func(t *testing.T) {
decoder1 := Of[string](1)
decoder2 := Of[string](2)
decoder3 := Of[string](3)
decoder4 := Of[string](4)
combined := m.Concat(m.Concat(m.Concat(decoder1, decoder2), decoder3), decoder4)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
})
})
t.Run("with map input type", func(t *testing.T) {
m := ApplicativeMonoid[map[string]any](S.Monoid)
t.Run("combines decoders with different inputs", func(t *testing.T) {
decoder1 := func(data map[string]any) Validation[string] {
if name, ok := data["firstName"].(string); ok {
return validation.Of(name)
}
return either.Left[string](validation.Errors{
{Messsage: "missing firstName"},
})
}
decoder2 := func(data map[string]any) Validation[string] {
if name, ok := data["lastName"].(string); ok {
return validation.Of(" " + name)
}
return either.Left[string](validation.Errors{
{Messsage: "missing lastName"},
})
}
combined := m.Concat(decoder1, decoder2)
// Test success case
result1 := combined(map[string]any{
"firstName": "John",
"lastName": "Doe",
})
value1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "John Doe", value1)
// Test failure case - both fields missing
result2 := combined(map[string]any{})
assert.True(t, either.IsLeft(result2))
errors := either.MonadFold(result2,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 2)
})
})
}
func TestMonoidLaws(t *testing.T) {
t.Run("ApplicativeMonoid satisfies monoid laws", func(t *testing.T) {
m := ApplicativeMonoid[string](S.Monoid)
decoder1 := Of[string]("a")
decoder2 := Of[string]("b")
t.Run("left identity", func(t *testing.T) {
// empty + a = a
result := m.Concat(m.Empty(), decoder1)("input")
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("right identity", func(t *testing.T) {
// a + empty = a
result := m.Concat(decoder1, m.Empty())("input")
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("associativity", func(t *testing.T) {
decoder3 := Of[string]("c")
// (a + b) + c = a + (b + c)
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
})
})
}
func TestApplicativeMonoidWithFailures(t *testing.T) {
m := ApplicativeMonoid[string](S.Monoid)
t.Run("failure propagates through concat", func(t *testing.T) {
decoder1 := Of[string]("a")
decoder2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error"},
})
}
decoder3 := Of[string]("c")
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 1)
})
t.Run("multiple failures accumulate", func(t *testing.T) {
decoder1 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
decoder2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
decoder3 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 3"},
})
}
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
result := combined("input")
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 3)
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
assert.Contains(t, messages, "error 3")
})
}
func TestApplicativeMonoidEdgeCases(t *testing.T) {
t.Run("with custom struct monoid", func(t *testing.T) {
type Counter struct{ Count int }
counterMonoid := MO.MakeMonoid(
func(a, b Counter) Counter { return Counter{Count: a.Count + b.Count} },
Counter{Count: 0},
)
m := ApplicativeMonoid[string](counterMonoid)
decoder1 := Of[string](Counter{Count: 5})
decoder2 := Of[string](Counter{Count: 10})
combined := m.Concat(decoder1, decoder2)
result := combined("input")
value := either.MonadFold(result,
func(Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, 15, value.Count)
})
t.Run("empty concat empty", func(t *testing.T) {
m := ApplicativeMonoid[string](S.Monoid)
combined := m.Concat(m.Empty(), m.Empty())
result := combined("input")
value := either.MonadFold(result,
func(Errors) string { return "ERROR" },
F.Identity[string],
)
assert.Equal(t, "", value)
})
t.Run("with different input types", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := ApplicativeMonoid[int](intMonoid)
decoder1 := func(input int) Validation[int] {
return validation.Of(input * 2)
}
decoder2 := func(input int) Validation[int] {
return validation.Of(input + 10)
}
combined := m.Concat(decoder1, decoder2)
result := combined(5)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
// (5 * 2) + (5 + 10) = 10 + 15 = 25
assert.Equal(t, 25, value)
})
}
func TestApplicativeMonoidRealWorldScenarios(t *testing.T) {
t.Run("combining configuration from multiple sources", func(t *testing.T) {
type Config struct {
Host string
Port int
}
// Monoid that combines configs (last non-empty wins for strings, sum for ints)
configMonoid := MO.MakeMonoid(
func(a, b Config) Config {
host := a.Host
if b.Host != "" {
host = b.Host
}
return Config{
Host: host,
Port: a.Port + b.Port,
}
},
Config{Host: "", Port: 0},
)
m := ApplicativeMonoid[map[string]any](configMonoid)
decoder1 := func(data map[string]any) Validation[Config] {
if host, ok := data["host"].(string); ok {
return validation.Of(Config{Host: host, Port: 0})
}
return either.Left[Config](validation.Errors{
{Messsage: "missing host"},
})
}
decoder2 := func(data map[string]any) Validation[Config] {
if port, ok := data["port"].(int); ok {
return validation.Of(Config{Host: "", Port: port})
}
return either.Left[Config](validation.Errors{
{Messsage: "missing port"},
})
}
combined := m.Concat(decoder1, decoder2)
// Success case
result := combined(map[string]any{
"host": "localhost",
"port": 8080,
})
config := either.MonadFold(result,
func(Errors) Config { return Config{} },
F.Identity[Config],
)
assert.Equal(t, "localhost", config.Host)
assert.Equal(t, 8080, config.Port)
})
t.Run("aggregating validation results", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := ApplicativeMonoid[string](intMonoid)
// Decoder that extracts and validates a number
makeDecoder := func(value int, shouldFail bool) Decode[string, int] {
return func(input string) Validation[int] {
if shouldFail {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "validation failed"},
})
}
return validation.Of(value)
}
}
// All succeed - values are summed
decoder1 := makeDecoder(10, false)
decoder2 := makeDecoder(20, false)
decoder3 := makeDecoder(12, false)
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
// Some fail - errors are accumulated
decoder4 := makeDecoder(10, true)
decoder5 := makeDecoder(20, true)
combinedFail := m.Concat(decoder4, decoder5)
resultFail := combinedFail("input")
assert.True(t, either.IsLeft(resultFail))
errors := either.MonadFold(resultFail,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 2)
})
}
// TestAlternativeMonoid tests the AlternativeMonoid function
func TestAlternativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := AlternativeMonoid[string](S.Monoid)
t.Run("empty returns decoder that succeeds with empty string", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
assert.Equal(t, validation.Of(""), result)
})
t.Run("concat combines successful decoders using monoid", func(t *testing.T) {
decoder1 := Of[string]("Hello")
decoder2 := Of[string](" World")
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.Equal(t, validation.Of("Hello World"), result)
})
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
failing := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "first failed"},
})
}
succeeding := Of[string]("fallback")
combined := m.Concat(failing, succeeding)
result := combined("input")
assert.Equal(t, validation.Of("fallback"), result)
})
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
failing2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
combined := m.Concat(failing1, failing2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both decoders")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("concat with empty preserves decoder", func(t *testing.T) {
decoder := Of[string]("test")
empty := m.Empty()
result1 := m.Concat(decoder, empty)("input")
result2 := m.Concat(empty, decoder)("input")
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
})
})
t.Run("with int addition monoid", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := AlternativeMonoid[string](intMonoid)
t.Run("empty returns decoder with zero", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
decoder1 := Of[string](10)
decoder2 := Of[string](32)
combined := m.Concat(decoder1, decoder2)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("concat uses fallback when first fails", func(t *testing.T) {
failing := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
succeeding := Of[string](42)
combined := m.Concat(failing, succeeding)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("multiple concat operations", func(t *testing.T) {
decoder1 := Of[string](1)
decoder2 := Of[string](2)
decoder3 := Of[string](3)
decoder4 := Of[string](4)
combined := m.Concat(m.Concat(m.Concat(decoder1, decoder2), decoder3), decoder4)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
})
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AlternativeMonoid[string](S.Monoid)
decoder1 := Of[string]("a")
decoder2 := Of[string]("b")
decoder3 := Of[string]("c")
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), decoder1)("input")
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("right identity", func(t *testing.T) {
result := m.Concat(decoder1, m.Empty())("input")
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("associativity", func(t *testing.T) {
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
})
})
t.Run("error aggregation with multiple failures", func(t *testing.T) {
m := AlternativeMonoid[string](S.Monoid)
failing1 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
failing2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
failing3 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 3"},
})
}
combined := m.Concat(m.Concat(failing1, failing2), failing3)
result := combined("input")
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 3, "Should aggregate errors from all decoders")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
assert.Contains(t, messages, "error 3")
})
}
// TestAltMonoid tests the AltMonoid function
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
m := AltMonoid(func() Decode[string, int] {
return Of[string](0)
})
t.Run("empty returns the provided zero decoder", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
assert.Equal(t, validation.Of(0), result)
})
t.Run("concat returns first decoder when it succeeds", func(t *testing.T) {
decoder1 := Of[string](42)
decoder2 := Of[string](100)
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.Equal(t, validation.Of(42), result)
})
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
failing := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
succeeding := Of[string](42)
combined := m.Concat(failing, succeeding)
result := combined("input")
assert.Equal(t, validation.Of(42), result)
})
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
failing2 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
combined := m.Concat(failing1, failing2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both decoders")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
})
t.Run("with failing zero", func(t *testing.T) {
m := AltMonoid(func() Decode[string, int] {
return func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "no default available"},
})
}
})
t.Run("empty returns the failing zero decoder", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
assert.True(t, either.IsLeft(result))
})
t.Run("concat with all failures aggregates errors", func(t *testing.T) {
failing1 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
failing2 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
combined := m.Concat(failing1, failing2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors")
})
})
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Decode[string, string] {
return Of[string]("default")
})
primary := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "primary failed"},
})
}
secondary := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "secondary failed"},
})
}
tertiary := Of[string]("tertiary value")
combined := m.Concat(m.Concat(primary, secondary), tertiary)
result := combined("input")
assert.Equal(t, validation.Of("tertiary value"), result)
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AltMonoid(func() Decode[string, int] {
return Of[string](0)
})
decoder1 := Of[string](1)
decoder2 := Of[string](2)
decoder3 := Of[string](3)
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), decoder1)("input")
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
// With AltMonoid, first success wins, so empty (0) is returned
assert.Equal(t, 0, value)
})
t.Run("right identity", func(t *testing.T) {
result := m.Concat(decoder1, m.Empty())("input")
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
// First decoder succeeds, so 1 is returned
assert.Equal(t, 1, value)
})
t.Run("associativity", func(t *testing.T) {
// For AltMonoid, first success wins
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
leftVal := either.MonadFold(left,
func(Errors) int { return -1 },
F.Identity[int],
)
rightVal := either.MonadFold(right,
func(Errors) int { return -1 },
F.Identity[int],
)
// Both should return 1 (first success)
assert.Equal(t, 1, leftVal)
assert.Equal(t, 1, rightVal)
})
})
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
// AltMonoid - first success wins
altM := AltMonoid(func() Decode[string, int] {
return Of[string](0)
})
// AlternativeMonoid - combines successes
altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
decoder1 := Of[string](10)
decoder2 := Of[string](32)
// AltMonoid: returns first success (10)
result1 := altM.Concat(decoder1, decoder2)("input")
value1 := either.MonadFold(result1,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value1, "AltMonoid returns first success")
// AlternativeMonoid: combines both successes (10 + 32 = 42)
result2 := altMonoid.Concat(decoder1, decoder2)("input")
value2 := either.MonadFold(result2,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value2, "AlternativeMonoid combines successes")
})
t.Run("error aggregation with context", func(t *testing.T) {
m := AltMonoid(func() Decode[string, int] {
return Of[string](0)
})
failing1 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{
Value: input,
Messsage: "parse error",
Context: validation.Context{{Key: "field", Type: "int"}},
},
})
}
failing2 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{
Value: input,
Messsage: "validation error",
Context: validation.Context{{Key: "value", Type: "int"}},
},
})
}
combined := m.Concat(failing1, failing2)
result := combined("abc")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should have errors from both decoders")
// Verify that errors with context are present
hasParseError := false
hasValidationError := false
for _, err := range errors {
if err.Messsage == "parse error" {
hasParseError = true
assert.NotNil(t, err.Context)
}
if err.Messsage == "validation error" {
hasValidationError = true
assert.NotNil(t, err.Context)
}
}
assert.True(t, hasParseError, "Should have parse error")
assert.True(t, hasValidationError, "Should have validation error")
})
}

View File

@@ -17,11 +17,60 @@ package decode
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
)
type (
// Errors is a collection of validation errors that occurred during decoding.
// This is an alias for validation.Errors, which is []*ValidationError.
//
// Errors accumulates multiple validation failures, allowing decoders to report
// all problems at once rather than failing on the first error. This is particularly
// useful for form validation, API request validation, and configuration parsing
// where users benefit from seeing all issues simultaneously.
//
// The Errors type forms a Semigroup and Monoid, enabling:
// - Concatenation: Combining errors from multiple decoders
// - Accumulation: Collecting errors through applicative operations
// - Empty value: An empty slice representing no errors (success)
//
// Each error in the collection is a *ValidationError containing:
// - Value: The actual value that failed validation
// - Context: The path to the value in nested structures
// - Message: Human-readable error description
// - Cause: Optional underlying error
//
// Example:
//
// // Multiple validation failures
// errors := Errors{
// &validation.ValidationError{
// Value: "",
// Context: []validation.ContextEntry{{Key: "name"}},
// Messsage: "name is required",
// },
// &validation.ValidationError{
// Value: "invalid@",
// Context: []validation.ContextEntry{{Key: "email"}},
// Messsage: "invalid email format",
// },
// }
//
// // Create a failed validation with these errors
// result := validation.Failures[User](errors)
//
// // Errors can be combined using the monoid
// moreErrors := Errors{
// &validation.ValidationError{
// Value: -1,
// Context: []validation.ContextEntry{{Key: "age"}},
// Messsage: "age must be positive",
// },
// }
// allErrors := append(errors, moreErrors...)
Errors = validation.Errors
// Validation represents the result of a validation operation that may contain
@@ -219,4 +268,79 @@ type (
// LetL(nameLens, normalize),
// )
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Monoid represents an algebraic structure with an associative binary operation
// and an identity element. This is an alias for monoid.Monoid[A].
//
// A Monoid[A] consists of:
// - Concat: func(A, A) A - An associative binary operation
// - Empty: func() A - An identity element
//
// In the decode context, monoids are used to combine multiple decoders or
// validation results. The most common use case is combining validation errors
// from multiple decoders using the Errors monoid.
//
// Properties:
// - Associativity: Concat(Concat(a, b), c) == Concat(a, Concat(b, c))
// - Identity: Concat(Empty(), a) == a == Concat(a, Empty())
//
// Common monoid instances:
// - Errors: Combines validation errors from multiple sources
// - Array: Concatenates arrays of decoded values
// - String: Concatenates strings
//
// Example:
//
// // Combine validation errors from multiple decoders
// errorsMonoid := validation.GetMonoid[int]()
//
// // Decode multiple fields and combine errors
// result1 := decodeField1(data) // Validation[string]
// result2 := decodeField2(data) // Validation[int]
//
// // If both fail, errors are combined using the monoid
// combined := errorsMonoid.Concat(result1, result2)
//
// // The monoid's Empty() provides a successful validation with no errors
// empty := errorsMonoid.Empty() // Success with no value
Monoid[A any] = monoid.Monoid[A]
// Lazy represents a deferred computation that produces a value of type A.
// This is an alias for lazy.Lazy[A], which is func() A.
//
// In the decode context, Lazy is used to defer expensive computations or
// recursive decoder definitions until they are actually needed. This is
// particularly important for:
// - Recursive data structures (e.g., trees, linked lists)
// - Expensive default values
// - Breaking circular dependencies in decoder definitions
//
// A Lazy[A] is simply a function that takes no arguments and returns A.
// The computation is only executed when the function is called, allowing
// for lazy evaluation and recursive definitions.
//
// Example:
//
// // Define a recursive decoder for a tree structure
// type Tree struct {
// Value int
// Children []Tree
// }
//
// // Use Lazy to break the circular dependency
// var decodeTree Decode[map[string]any, Tree]
// decodeTree = func(data map[string]any) Validation[Tree] {
// // Lazy evaluation allows referencing decodeTree within itself
// childrenDecoder := Array(Lazy(func() Decode[map[string]any, Tree] {
// return decodeTree
// }))
// // ... rest of decoder implementation
// }
//
// // Lazy default value that's only computed if needed
// expensiveDefault := Lazy(func() Config {
// // This computation only runs if the decode fails
// return computeExpensiveDefaultConfig()
// })
Lazy[A any] = lazy.Lazy[A]
)

View File

@@ -18,11 +18,10 @@ package codec
import (
"fmt"
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/optics/codec/validation"
)
// encodeEither creates an encoder for Either[A, B] values.
@@ -151,28 +150,20 @@ func validateEither[A, B, O, I any](
rightItem Type[B, O, I],
) Validate[I, either.Either[A, B]] {
return func(i I) Decode[Context, either.Either[A, B]] {
valRight := rightItem.Validate(i)
valLeft := leftItem.Validate(i)
valRight := F.Pipe1(
rightItem.Validate,
validate.Map[I, B](either.Right[A]),
)
return func(ctx Context) Validation[either.Either[A, B]] {
valLeft := F.Pipe1(
leftItem.Validate,
validate.Map[I, A](either.Left[B]),
)
resRight := valRight(ctx)
return either.Fold(
func(rightErrors validate.Errors) Validation[either.Either[A, B]] {
resLeft := valLeft(ctx)
return either.Fold(
func(leftErrors validate.Errors) Validation[either.Either[A, B]] {
return validation.Failures[either.Either[A, B]](array.Concat(leftErrors)(rightErrors))
},
F.Flow2(either.Left[B, A], validation.Of),
)(resLeft)
},
F.Flow2(either.Right[A, B], validation.Of),
)(resRight)
}
}
return F.Pipe1(
valRight,
validate.Alt(lazy.Of(valLeft)),
)
}
// Either creates a codec for Either[A, B] values.
@@ -265,12 +256,9 @@ func Either[A, B, O, I any](
leftItem Type[A, O, I],
rightItem Type[B, O, I],
) Type[either.Either[A, B], O, I] {
name := fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name())
isEither := Is[either.Either[A, B]]()
return MakeType(
name,
isEither,
fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name()),
Is[either.Either[A, B]](),
validateEither(leftItem, rightItem),
encodeEither(leftItem, rightItem),
)

View File

@@ -342,8 +342,27 @@ func TestEitherErrorAccumulation(t *testing.T) {
require.NotNil(t, errors)
// Should have errors from both string and int validation attempts
assert.NotEmpty(t, errors)
assert.GreaterOrEqual(t, len(errors), 2, "Should have at least 2 errors (one from Right validation, one from Left validation)")
// Verify we have errors from both validation attempts
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
// Check that we have errors related to both validations
hasIntError := false
hasStringError := false
for _, msg := range messages {
if msg == "expected integer string" || msg == "must be positive" {
hasIntError = true
}
if msg == "must not be empty" {
hasStringError = true
}
}
assert.True(t, hasIntError, "Should have error from integer validation (Right branch)")
assert.True(t, hasStringError, "Should have error from string validation (Left branch)")
})
}
// Made with Bob

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,6 +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 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

@@ -0,0 +1,661 @@
package validate
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
// TestMonadChainLeft tests the MonadChainLeft function
func TestMonadChainLeft(t *testing.T) {
t.Run("transforms failures while preserving successes", func(t *testing.T) {
// Create a failing validator
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "validation failed"},
})
}
}
// Handler that recovers from specific errors
handler := func(errs Errors) Validate[string, int] {
for _, err := range errs {
if err.Messsage == "validation failed" {
return Of[string, int](0) // recover with default
}
}
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](errs)
}
}
}
validator := MonadChainLeft(failingValidator, handler)
res := validator("input")(nil)
assert.Equal(t, validation.Of(0), res, "Should recover from failure")
})
t.Run("preserves success values unchanged", func(t *testing.T) {
successValidator := Of[string, int](42)
handler := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "should not be called"},
})
}
}
}
validator := MonadChainLeft(successValidator, handler)
res := validator("input")(nil)
assert.Equal(t, validation.Of(42), res, "Success should pass through unchanged")
})
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "original error"},
})
}
}
handler := func(errs Errors) Validate[string, string] {
return func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Messsage: "additional error"},
})
}
}
}
validator := MonadChainLeft(failingValidator, handler)
res := validator("input")(nil)
assert.True(t, either.IsLeft(res))
errors := either.MonadFold(res,
reader.Ask[Errors](),
func(string) Errors { return nil },
)
assert.Len(t, errors, 2, "Should aggregate both errors")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "original error")
assert.Contains(t, messages, "additional error")
})
t.Run("adds context to errors", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "invalid format"},
})
}
}
addContext := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
Messsage: "failed to validate user age",
},
})
}
}
}
validator := MonadChainLeft(failingValidator, addContext)
res := validator("abc")(nil)
assert.True(t, either.IsLeft(res))
errors := either.MonadFold(res,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.Len(t, errors, 2, "Should have both original and context errors")
})
t.Run("works with different input types", func(t *testing.T) {
type Config struct {
Port int
}
failingValidator := func(cfg Config) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: cfg.Port, Messsage: "invalid port"},
})
}
}
handler := func(errs Errors) Validate[Config, string] {
return Of[Config, string]("default-value")
}
validator := MonadChainLeft(failingValidator, handler)
res := validator(Config{Port: 9999})(nil)
assert.Equal(t, validation.Of("default-value"), res)
})
t.Run("handler can access original input", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "parse failed"},
})
}
}
handler := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
// Handler can use the original input to make decisions
if input == "special" {
return validation.Of(999)
}
return validation.Of(0)
}
}
}
validator := MonadChainLeft(failingValidator, handler)
res1 := validator("special")(nil)
assert.Equal(t, validation.Of(999), res1)
res2 := validator("other")(nil)
assert.Equal(t, validation.Of(0), res2)
})
t.Run("is equivalent to ChainLeft", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error"},
})
}
}
handler := func(errs Errors) Validate[string, int] {
return Of[string, int](42)
}
// MonadChainLeft - direct application
result1 := MonadChainLeft(failingValidator, handler)("input")(nil)
// ChainLeft - curried for pipelines
result2 := ChainLeft(handler)(failingValidator)("input")(nil)
assert.Equal(t, result1, result2, "MonadChainLeft and ChainLeft should produce identical results")
})
t.Run("chains multiple error transformations", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error1"},
})
}
}
handler1 := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "error2"},
})
}
}
}
handler2 := func(errs Errors) Validate[string, int] {
// Check if we can recover
for _, err := range errs {
if err.Messsage == "error1" {
return Of[string, int](100) // recover
}
}
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](errs)
}
}
}
// Chain handlers
validator := MonadChainLeft(MonadChainLeft(failingValidator, handler1), handler2)
res := validator("input")(nil)
// Should recover because error1 is present
assert.Equal(t, validation.Of(100), res)
})
t.Run("does not call handler on success", func(t *testing.T) {
successValidator := Of[string, int](42)
handlerCalled := false
handler := func(errs Errors) Validate[string, int] {
handlerCalled = true
return Of[string, int](0)
}
validator := MonadChainLeft(successValidator, handler)
res := validator("input")(nil)
assert.Equal(t, validation.Of(42), res)
assert.False(t, handlerCalled, "Handler should not be called on success")
})
}
// TestMonadAlt tests the MonadAlt function
func TestMonadAlt(t *testing.T) {
t.Run("returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
}
result := MonadAlt(validator1, validator2)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("returns second validator when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "first failed"},
})
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
}
result := MonadAlt(failing, fallback)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
result := MonadAlt(failing1, failing2)("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1", "Should contain error from first validator")
assert.Contains(t, messages, "error 2", "Should contain error from second validator")
})
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
evaluated := false
validator2 := func() Validate[string, int] {
evaluated = true
return Of[string, int](100)
}
result := MonadAlt(validator1, validator2)("input")(nil)
assert.Equal(t, validation.Of(42), result)
assert.False(t, evaluated, "Second validator should not be evaluated")
})
t.Run("works with different types", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
}
fallback := func() Validate[string, string] {
return Of[string, string]("fallback")
}
result := MonadAlt(failing, fallback)("input")(nil)
assert.Equal(t, validation.Of("fallback"), result)
})
t.Run("chains multiple alternatives", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
succeeding := func() Validate[string, int] {
return Of[string, int](42)
}
// Chain: try failing1, then failing2, then succeeding
result := MonadAlt(MonadAlt(failing1, failing2), succeeding)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("works with complex input types", func(t *testing.T) {
type Config struct {
Port int
}
failing := func(cfg Config) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: cfg.Port, Messsage: "invalid port"},
})
}
}
fallback := func() Validate[Config, string] {
return Of[Config, string]("default")
}
result := MonadAlt(failing, fallback)(Config{Port: 9999})(nil)
assert.Equal(t, validation.Of("default"), result)
})
t.Run("preserves error context", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{
Value: input,
Messsage: "parse error",
Context: validation.Context{{Key: "field", Type: "int"}},
},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{
Value: input,
Messsage: "validation error",
Context: validation.Context{{Key: "value", Type: "int"}},
},
})
}
}
}
result := MonadAlt(failing1, failing2)("abc")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should have errors from both validators")
// Verify that errors with context are present
hasParseError := false
hasValidationError := false
for _, err := range errors {
if err.Messsage == "parse error" {
hasParseError = true
assert.NotNil(t, err.Context)
}
if err.Messsage == "validation error" {
hasValidationError = true
assert.NotNil(t, err.Context)
}
}
assert.True(t, hasParseError, "Should have parse error")
assert.True(t, hasValidationError, "Should have validation error")
})
}
// TestAlt tests the Alt function
func TestAlt(t *testing.T) {
t.Run("returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
}
withAlt := Alt(validator2)
result := withAlt(validator1)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("returns second validator when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "first failed"},
})
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
}
withAlt := Alt(fallback)
result := withAlt(failing)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
withAlt := Alt(failing2)
result := withAlt(failing1)("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
evaluated := false
validator2 := func() Validate[string, int] {
evaluated = true
return Of[string, int](100)
}
withAlt := Alt(validator2)
result := withAlt(validator1)("input")(nil)
assert.Equal(t, validation.Of(42), result)
assert.False(t, evaluated, "Second validator should not be evaluated")
})
t.Run("can be used in pipelines", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
succeeding := func() Validate[string, int] {
return Of[string, int](42)
}
// Use F.Pipe to chain alternatives
validator := F.Pipe2(
failing1,
Alt(failing2),
Alt(succeeding),
)
result := validator("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("is equivalent to MonadAlt", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error"},
})
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
}
// Alt - curried for pipelines
result1 := Alt(fallback)(failing)("input")(nil)
// MonadAlt - direct application
result2 := MonadAlt(failing, fallback)("input")(nil)
assert.Equal(t, result1, result2, "Alt and MonadAlt should produce identical results")
})
}
// TestMonadAltAndAltEquivalence tests that MonadAlt and Alt are equivalent
func TestMonadAltAndAltEquivalence(t *testing.T) {
t.Run("both produce same results for success", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
}
resultMonadAlt := MonadAlt(validator1, validator2)("input")(nil)
resultAlt := Alt(validator2)(validator1)("input")(nil)
assert.Equal(t, resultMonadAlt, resultAlt)
})
t.Run("both produce same results for fallback", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
}
resultMonadAlt := MonadAlt(failing, fallback)("input")(nil)
resultAlt := Alt(fallback)(failing)("input")(nil)
assert.Equal(t, resultMonadAlt, resultAlt)
})
t.Run("both produce same results for error aggregation", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
resultMonadAlt := MonadAlt(failing1, failing2)("input")(nil)
resultAlt := Alt(failing2)(failing1)("input")(nil)
// Both should fail
assert.True(t, either.IsLeft(resultMonadAlt))
assert.True(t, either.IsLeft(resultAlt))
// Both should have same errors
errorsMonadAlt := either.MonadFold(resultMonadAlt,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
errorsAlt := either.MonadFold(resultAlt,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.Equal(t, len(errorsMonadAlt), len(errorsAlt))
})
}

View File

@@ -122,3 +122,268 @@ func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
m,
)
}
// AlternativeMonoid creates a Monoid instance for Validate[I, A] that combines both
// applicative and alternative semantics.
//
// This function creates a monoid that:
// 1. When both validators succeed: Combines their results using the provided monoid operation
// 2. When one validator fails: Uses the successful validator's result (alternative behavior)
// 3. When both validators fail: Aggregates all errors from both validators
//
// This is a hybrid approach that combines:
// - ApplicativeMonoid: Combines successful results using the monoid operation
// - AltMonoid: Provides fallback behavior when validators fail
//
// # Type Parameters
//
// - I: The input type that validators accept
// - A: The output type that validators produce (must have a Monoid instance)
//
// # Parameters
//
// - m: A Monoid[A] that defines how to combine values of type A
//
// # Returns
//
// A Monoid[Validate[I, A]] that combines validators using both applicative and alternative semantics.
//
// # Behavior Details
//
// The AlternativeMonoid differs from ApplicativeMonoid in how it handles mixed success/failure:
//
// - **Both succeed**: Results are combined using the monoid operation (like ApplicativeMonoid)
// - **First succeeds, second fails**: Returns the first result (alternative fallback)
// - **First fails, second succeeds**: Returns the second result (alternative fallback)
// - **Both fail**: Aggregates errors from both validators
//
// # Example: String Concatenation with Fallback
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// m := validate.AlternativeMonoid[string, string](S.Monoid)
//
// // Both succeed - results are concatenated
// validator1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.Success("Hello")
// }
// }
// validator2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.Success(" World")
// }
// }
// combined := m.Concat(validator1, validator2)
// result := combined("input")(nil)
// // result is validation.Success("Hello World")
//
// # Example: Fallback Behavior
//
// // First fails, second succeeds - uses second result
// failing := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.FailureWithMessage[string](input, "first failed")(ctx)
// }
// }
// succeeding := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.Success("fallback")
// }
// }
// combined := m.Concat(failing, succeeding)
// result := combined("input")(nil)
// // result is validation.Success("fallback")
//
// # Example: Error Aggregation
//
// // Both fail - errors are aggregated
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.FailureWithMessage[string](input, "error 1")(ctx)
// }
// }
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.FailureWithMessage[string](input, "error 2")(ctx)
// }
// }
// combined := m.Concat(failing1, failing2)
// result := combined("input")(nil)
// // result contains both "error 1" and "error 2"
//
// # Comparison with Other Monoids
//
// - **ApplicativeMonoid**: Always combines results when both succeed, fails if either fails
// - **AlternativeMonoid**: Combines results when both succeed, provides fallback when one fails
// - **AltMonoid**: Always uses first success, never combines results
//
// # Use Cases
//
// - Validation with fallback strategies and result combination
// - Building validators that accumulate results but provide alternatives
// - Configuration loading with multiple sources and merging
// - Data aggregation with error recovery
//
// # Notes
//
// - Both validators receive the same input value I
// - The empty element of the monoid serves as the identity for the Concat operation
// - Error aggregation ensures no validation failures are lost
// - This follows both applicative and alternative functor laws
//
// # See Also
//
// - ApplicativeMonoid: For pure applicative combination without fallback
// - AltMonoid: For pure alternative behavior without result combination
// - MonadAlt: The underlying alternative operation
func AlternativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
return monoid.AlternativeMonoid(
Of[I, A],
MonadMap[I, A, func(A) A],
MonadAp[A, I, A],
MonadAlt[I, A],
m,
)
}
// AltMonoid creates a Monoid instance for Validate[I, A] using alternative semantics
// with a provided zero/default validator.
//
// This function creates a monoid where:
// 1. The first successful validator wins (no result combination)
// 2. If the first fails, the second is tried as a fallback
// 3. If both fail, errors are aggregated
// 4. The provided zero validator serves as the identity element
//
// Unlike AlternativeMonoid, AltMonoid does NOT combine successful results - it always
// returns the first success. This makes it ideal for fallback chains and default values.
//
// # Type Parameters
//
// - I: The input type that validators accept
// - A: The output type that validators produce
//
// # Parameters
//
// - zero: A lazy Validate[I, A] that serves as the identity element. This is typically
// a validator that always succeeds with a default value, but can also be a failing
// validator if no default is appropriate.
//
// # Returns
//
// A Monoid[Validate[I, A]] that combines validators 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 validator is used as fallback
//
// # Example: Default Value Fallback
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// )
//
// // Create a monoid with a default value of 0
// m := validate.AltMonoid(func() validate.Validate[string, int] {
// return validate.Of[string, int](0)
// })
//
// // First validator succeeds - returns 42, second is not evaluated
// validator1 := validate.Of[string, int](42)
// validator2 := validate.Of[string, int](100)
// combined := m.Concat(validator1, validator2)
// result := combined("input")(nil)
// // result is validation.Success(42)
//
// # Example: Fallback Chain
//
// // Try primary, then fallback, then default
// m := validate.AltMonoid(func() validate.Validate[string, string] {
// return validate.Of[string, string]("default")
// })
//
// primary := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.FailureWithMessage[string](input, "primary failed")(ctx)
// }
// }
// secondary := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.Success("secondary value")
// }
// }
//
// // Chain: try primary, then secondary, then default
// combined := m.Concat(m.Concat(primary, secondary), m.Empty())
// result := combined("input")(nil)
// // result is validation.Success("secondary value")
//
// # Example: Error Aggregation
//
// // Both fail - errors are aggregated
// m := validate.AltMonoid(func() validate.Validate[string, int] {
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "no default")(ctx)
// }
// }
// })
//
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "error 1")(ctx)
// }
// }
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "error 2")(ctx)
// }
// }
//
// combined := m.Concat(failing1, failing2)
// result := combined("input")(nil)
// // result contains both "error 1" and "error 2"
//
// # Comparison with Other Monoids
//
// - **ApplicativeMonoid**: Combines results when both succeed using monoid operation
// - **AlternativeMonoid**: Combines results when both succeed, provides fallback when one fails
// - **AltMonoid**: First success wins, never combines results (pure alternative)
//
// # Use Cases
//
// - Configuration loading with fallback sources (try file, then env, then default)
// - Validation with default values
// - Parser combinators with alternative branches
// - Error recovery with multiple strategies
//
// # Notes
//
// - The zero validator is lazily evaluated, only when needed
// - First success short-circuits evaluation (second validator not called)
// - Error aggregation ensures all validation failures are reported
// - This follows the alternative functor laws
//
// # See Also
//
// - AlternativeMonoid: For combining results when both succeed
// - ApplicativeMonoid: For pure applicative combination
// - MonadAlt: The underlying alternative operation
// - Alt: The curried version for pipeline composition
func AltMonoid[I, A any](zero Lazy[Validate[I, A]]) Monoid[Validate[I, A]] {
return monoid.AltMonoid(
zero,
MonadAlt[I, A],
)
}

View File

@@ -1,475 +1,397 @@
// 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 validate
import (
"testing"
E "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
MO "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/optics/codec/validation"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
var (
intAddMonoid = N.MonoidSum[int]()
strMonoid = S.Monoid
)
// TestAlternativeMonoid tests the AlternativeMonoid function
func TestAlternativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := AlternativeMonoid[string, string](S.Monoid)
// Helper function to create a successful validator
func successValidator[I, A any](value A) Validate[I, A] {
return func(input I) Reader[validation.Context, validation.Validation[A]] {
return func(ctx validation.Context) validation.Validation[A] {
return validation.Success(value)
}
}
}
t.Run("empty returns validator that succeeds with empty string", func(t *testing.T) {
empty := m.Empty()
result := empty("input")(nil)
// Helper function to create a failing validator
func failureValidator[I, A any](message string) Validate[I, A] {
return func(input I) Reader[validation.Context, validation.Validation[A]] {
return validation.FailureWithMessage[A](input, message)
}
}
assert.Equal(t, validation.Of(""), result)
})
// Helper function to create a validator that uses the input
func inputDependentValidator[A any](f func(A) A) Validate[A, A] {
return func(input A) Reader[validation.Context, validation.Validation[A]] {
return func(ctx validation.Context) validation.Validation[A] {
return validation.Success(f(input))
}
}
}
t.Run("concat combines successful validators using monoid", func(t *testing.T) {
validator1 := Of[string, string]("Hello")
validator2 := Of[string, string](" World")
// TestApplicativeMonoid_EmptyElement tests the empty element of the monoid
func TestApplicativeMonoid_EmptyElement(t *testing.T) {
t.Run("int addition monoid", func(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
empty := m.Empty()
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
result := empty("test")(nil)
assert.Equal(t, validation.Of("Hello World"), result)
})
assert.Equal(t, validation.Of(0), result)
})
t.Run("string concatenation monoid", func(t *testing.T) {
m := ApplicativeMonoid[int](strMonoid)
empty := m.Empty()
result := empty(42)(nil)
assert.Equal(t, validation.Of(""), result)
})
}
// TestApplicativeMonoid_ConcatSuccesses tests concatenating two successful validators
func TestApplicativeMonoid_ConcatSuccesses(t *testing.T) {
t.Run("int addition", func(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](5)
v2 := successValidator[string](3)
combined := m.Concat(v1, v2)
result := combined("input")(nil)
assert.Equal(t, validation.Of(8), result)
})
t.Run("string concatenation", func(t *testing.T) {
m := ApplicativeMonoid[int](strMonoid)
v1 := successValidator[int]("Hello")
v2 := successValidator[int](" World")
combined := m.Concat(v1, v2)
result := combined(42)(nil)
assert.Equal(t, validation.Of("Hello World"), result)
})
}
// TestApplicativeMonoid_ConcatWithFailure tests concatenating validators where one fails
func TestApplicativeMonoid_ConcatWithFailure(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
t.Run("left failure", func(t *testing.T) {
v1 := failureValidator[string, int]("left error")
v2 := successValidator[string](5)
combined := m.Concat(v1, v2)
result := combined("input")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 1)
assert.Equal(t, "left error", errors[0].Messsage)
})
t.Run("right failure", func(t *testing.T) {
v1 := successValidator[string](5)
v2 := failureValidator[string, int]("right error")
combined := m.Concat(v1, v2)
result := combined("input")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 1)
assert.Equal(t, "right error", errors[0].Messsage)
})
t.Run("both failures", func(t *testing.T) {
v1 := failureValidator[string, int]("left error")
v2 := failureValidator[string, int]("right error")
combined := m.Concat(v1, v2)
result := combined("input")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
// Note: The current implementation returns the first error encountered
assert.GreaterOrEqual(t, len(errors), 1)
// At least one of the errors should be present
hasError := false
for _, err := range errors {
if err.Messsage == "left error" || err.Messsage == "right error" {
hasError = true
break
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "first failed"},
})
}
}
}
assert.True(t, hasError, "Should contain at least one validation error")
})
}
succeeding := Of[string, string]("fallback")
// TestApplicativeMonoid_LeftIdentity tests the left identity law
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
v := successValidator[string](42)
assert.Equal(t, validation.Of("fallback"), result)
})
// empty <> v == v
combined := m.Concat(m.Empty(), v)
resultCombined := combined("test")(nil)
resultOriginal := v("test")(nil)
assert.Equal(t, resultOriginal, resultCombined)
}
// TestApplicativeMonoid_RightIdentity tests the right identity law
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v := successValidator[string](42)
// v <> empty == v
combined := m.Concat(v, m.Empty())
resultCombined := combined("test")(nil)
resultOriginal := v("test")(nil)
assert.Equal(t, resultOriginal, resultCombined)
}
// TestApplicativeMonoid_Associativity tests the associativity law
func TestApplicativeMonoid_Associativity(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](1)
v2 := successValidator[string](2)
v3 := successValidator[string](3)
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
left := m.Concat(m.Concat(v1, v2), v3)
right := m.Concat(v1, m.Concat(v2, v3))
resultLeft := left("test")(nil)
resultRight := right("test")(nil)
assert.Equal(t, resultRight, resultLeft)
// Both should equal 6
assert.Equal(t, validation.Of(6), resultLeft)
}
// TestApplicativeMonoid_AssociativityWithFailures tests associativity with failures
func TestApplicativeMonoid_AssociativityWithFailures(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](1)
v2 := failureValidator[string, int]("error 2")
v3 := successValidator[string](3)
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
left := m.Concat(m.Concat(v1, v2), v3)
right := m.Concat(v1, m.Concat(v2, v3))
resultLeft := left("test")(nil)
resultRight := right("test")(nil)
// Both should fail with the same error
assert.True(t, E.IsLeft(resultLeft))
assert.True(t, E.IsLeft(resultRight))
_, errorsLeft := E.Unwrap(resultLeft)
_, errorsRight := E.Unwrap(resultRight)
assert.Len(t, errorsLeft, 1)
assert.Len(t, errorsRight, 1)
assert.Equal(t, "error 2", errorsLeft[0].Messsage)
assert.Equal(t, "error 2", errorsRight[0].Messsage)
}
// TestApplicativeMonoid_MultipleValidators tests combining multiple validators
func TestApplicativeMonoid_MultipleValidators(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](10)
v2 := successValidator[string](20)
v3 := successValidator[string](30)
v4 := successValidator[string](40)
// Chain multiple concat operations
combined := m.Concat(
m.Concat(
m.Concat(v1, v2),
v3,
),
v4,
)
result := combined("test")(nil)
assert.Equal(t, validation.Of(100), result)
}
// TestApplicativeMonoid_InputDependent tests validators that depend on input
func TestApplicativeMonoid_InputDependent(t *testing.T) {
m := ApplicativeMonoid[int](intAddMonoid)
// Validator that doubles the input
v1 := inputDependentValidator(N.Mul(2))
// Validator that adds 10 to the input
v2 := inputDependentValidator(N.Add(10))
combined := m.Concat(v1, v2)
result := combined(5)(nil)
// (5 * 2) + (5 + 10) = 10 + 15 = 25
assert.Equal(t, validation.Of(25), result)
}
// TestApplicativeMonoid_ContextPropagation tests that context is properly propagated
func TestApplicativeMonoid_ContextPropagation(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
// Create a validator that captures the context
var capturedContext validation.Context
v1 := func(input string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
capturedContext = ctx
return validation.Success(5)
}
}
v2 := successValidator[string](3)
combined := m.Concat(v1, v2)
// Create a context with some entries
ctx := validation.Context{
{Key: "field1", Type: "int"},
{Key: "field2", Type: "string"},
}
result := combined("test")(ctx)
assert.True(t, E.IsRight(result))
assert.Equal(t, ctx, capturedContext)
}
// TestApplicativeMonoid_ErrorAccumulation tests that errors are accumulated
func TestApplicativeMonoid_ErrorAccumulation(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := failureValidator[string, int]("error 1")
v2 := failureValidator[string, int]("error 2")
v3 := failureValidator[string, int]("error 3")
combined := m.Concat(m.Concat(v1, v2), v3)
result := combined("test")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
// Note: The current implementation returns the first error encountered
// At least one error should be present
assert.GreaterOrEqual(t, len(errors), 1)
hasError := false
for _, err := range errors {
if err.Messsage == "error 1" || err.Messsage == "error 2" || err.Messsage == "error 3" {
hasError = true
break
}
}
assert.True(t, hasError, "Should contain at least one validation error")
}
// TestApplicativeMonoid_MixedSuccessFailure tests mixing successes and failures
func TestApplicativeMonoid_MixedSuccessFailure(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](10)
v2 := failureValidator[string, int]("error in v2")
v3 := successValidator[string](20)
v4 := failureValidator[string, int]("error in v4")
combined := m.Concat(
m.Concat(
m.Concat(v1, v2),
v3,
),
v4,
)
result := combined("test")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
// Note: The current implementation returns the first error encountered
// At least one error should be present
assert.GreaterOrEqual(t, len(errors), 1)
hasError := false
for _, err := range errors {
if err.Messsage == "error in v2" || err.Messsage == "error in v4" {
hasError = true
break
}
}
assert.True(t, hasError, "Should contain at least one validation error")
}
// TestApplicativeMonoid_DifferentInputTypes tests with different input types
func TestApplicativeMonoid_DifferentInputTypes(t *testing.T) {
t.Run("struct input", func(t *testing.T) {
type Config struct {
Port int
Timeout int
}
m := ApplicativeMonoid[Config](intAddMonoid)
v1 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.Success(cfg.Port)
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
}
v2 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.Success(cfg.Timeout)
failing2 := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
combined := m.Concat(v1, v2)
result := combined(Config{Port: 8080, Timeout: 30})(nil)
combined := m.Concat(failing1, failing2)
result := combined("input")(nil)
assert.Equal(t, validation.Of(8110), result) // 8080 + 30
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("concat with empty preserves validator", func(t *testing.T) {
validator := Of[string, string]("test")
empty := m.Empty()
result1 := m.Concat(validator, empty)("input")(nil)
result2 := m.Concat(empty, validator)("input")(nil)
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
})
})
}
// TestApplicativeMonoid_StringConcatenation tests string concatenation scenarios
func TestApplicativeMonoid_StringConcatenation(t *testing.T) {
m := ApplicativeMonoid[string](strMonoid)
t.Run("build sentence", func(t *testing.T) {
v1 := successValidator[string]("The")
v2 := successValidator[string](" quick")
v3 := successValidator[string](" brown")
v4 := successValidator[string](" fox")
combined := m.Concat(
m.Concat(
m.Concat(v1, v2),
v3,
),
v4,
t.Run("with int addition monoid", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := AlternativeMonoid[string, int](intMonoid)
result := combined("input")(nil)
t.Run("empty returns validator with zero", func(t *testing.T) {
empty := m.Empty()
result := empty("input")(nil)
assert.Equal(t, validation.Of("The quick brown fox"), result)
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
validator1 := Of[string, int](10)
validator2 := Of[string, int](32)
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("concat uses fallback when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
}
succeeding := Of[string, int](42)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("multiple concat operations", func(t *testing.T) {
validator1 := Of[string, int](1)
validator2 := Of[string, int](2)
validator3 := Of[string, int](3)
validator4 := Of[string, int](4)
combined := m.Concat(m.Concat(m.Concat(validator1, validator2), validator3), validator4)
result := combined("input")(nil)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
})
})
t.Run("with empty strings", func(t *testing.T) {
v1 := successValidator[string]("Hello")
v2 := successValidator[string]("")
v3 := successValidator[string]("World")
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AlternativeMonoid[string, string](S.Monoid)
combined := m.Concat(m.Concat(v1, v2), v3)
result := combined("input")(nil)
validator1 := Of[string, string]("a")
validator2 := Of[string, string]("b")
validator3 := Of[string, string]("c")
assert.Equal(t, validation.Of("HelloWorld"), result)
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), validator1)("input")(nil)
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("right identity", func(t *testing.T) {
result := m.Concat(validator1, m.Empty())("input")(nil)
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("associativity", func(t *testing.T) {
left := m.Concat(m.Concat(validator1, validator2), validator3)("input")(nil)
right := m.Concat(validator1, m.Concat(validator2, validator3))("input")(nil)
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
})
})
}
// Benchmark tests
func BenchmarkApplicativeMonoid_ConcatSuccesses(b *testing.B) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](5)
v2 := successValidator[string](3)
combined := m.Concat(v1, v2)
// TestAltMonoid tests the AltMonoid function
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
m := AltMonoid(func() Validate[string, int] {
return Of[string, int](0)
})
b.ResetTimer()
for range b.N {
_ = combined("test")(nil)
}
}
func BenchmarkApplicativeMonoid_ConcatFailures(b *testing.B) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := failureValidator[string, int]("error 1")
v2 := failureValidator[string, int]("error 2")
combined := m.Concat(v1, v2)
b.ResetTimer()
for range b.N {
_ = combined("test")(nil)
}
}
func BenchmarkApplicativeMonoid_MultipleConcat(b *testing.B) {
m := ApplicativeMonoid[string](intAddMonoid)
validators := make([]Validate[string, int], 10)
for i := range validators {
validators[i] = successValidator[string](i)
}
// Chain all validators
combined := validators[0]
for i := 1; i < len(validators); i++ {
combined = m.Concat(combined, validators[i])
}
b.ResetTimer()
for range b.N {
_ = combined("test")(nil)
}
t.Run("empty returns the provided zero validator", func(t *testing.T) {
empty := m.Empty()
result := empty("input")(nil)
assert.Equal(t, validation.Of(0), result)
})
t.Run("concat returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := Of[string, int](100)
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
}
succeeding := Of[string, int](42)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
combined := m.Concat(failing1, failing2)
result := combined("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
})
t.Run("with failing zero", func(t *testing.T) {
m := AltMonoid(func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "no default available"},
})
}
}
})
t.Run("empty returns the failing zero validator", func(t *testing.T) {
empty := m.Empty()
result := empty("input")(nil)
assert.True(t, either.IsLeft(result))
})
t.Run("concat with all failures aggregates errors", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
combined := m.Concat(failing1, failing2)
result := combined("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors")
})
})
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Validate[string, string] {
return Of[string, string]("default")
})
primary := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "primary failed"},
})
}
}
secondary := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "secondary failed"},
})
}
}
tertiary := Of[string, string]("tertiary value")
combined := m.Concat(m.Concat(primary, secondary), tertiary)
result := combined("input")(nil)
assert.Equal(t, validation.Of("tertiary value"), result)
})
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
// AltMonoid - first success wins
altM := AltMonoid(func() Validate[string, int] {
return Of[string, int](0)
})
// AlternativeMonoid - combines successes
altMonoid := AlternativeMonoid[string, int](N.MonoidSum[int]())
validator1 := Of[string, int](10)
validator2 := Of[string, int](32)
// AltMonoid: returns first success (10)
result1 := altM.Concat(validator1, validator2)("input")(nil)
value1 := either.MonadFold(result1,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value1, "AltMonoid returns first success")
// AlternativeMonoid: combines both successes (10 + 32 = 42)
result2 := altMonoid.Concat(validator1, validator2)("input")(nil)
value2 := either.MonadFold(result2,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value2, "AlternativeMonoid combines successes")
})
}

View File

@@ -17,6 +17,7 @@ package validate
import (
"github.com/IBM/fp-go/v2/endomorphism"
"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/validation"
@@ -271,4 +272,6 @@ type (
// lower := strings.ToLower // Endomorphism[string]
// normalize := compose(trim, lower) // Endomorphism[string]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Lazy[A any] = lazy.Lazy[A]
)

View File

@@ -119,6 +119,7 @@
package validate
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/optics/codec/decode"
"github.com/IBM/fp-go/v2/reader"
@@ -309,6 +310,364 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
)
}
// ChainLeft sequences a computation on the failure (Left) channel of a validation.
//
// This function operates on the error path of validation, allowing you to transform,
// enrich, or recover from validation failures. It's the dual of Chain - while Chain
// operates on success values, ChainLeft operates on error values.
//
// # Key Behavior
//
// **Critical difference from standard Either operations**: This validation-specific
// implementation **aggregates errors** using the Errors monoid. When the transformation
// function returns a failure, both the original errors AND the new errors are combined,
// ensuring comprehensive error reporting.
//
// 1. **Success Pass-Through**: If validation succeeds, the handler is never called and
// the success value passes through unchanged.
//
// 2. **Error Recovery**: The handler can recover from failures by returning a successful
// validation, converting Left to Right.
//
// 3. **Error Aggregation**: When the handler also returns a failure, both the original
// errors and the new errors are combined using the Errors monoid.
//
// 4. **Input Access**: The handler returns a Validate[I, A] function, giving it access
// to the original input value I for context-aware error handling.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
// is called only when validation fails, receiving the accumulated errors.
//
// # Returns
//
// An Operator[I, A, A] that transforms validators by handling their error cases.
//
// # Example: Error Recovery
//
// // Validator that may fail
// validatePositive := func(n int) Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// if n > 0 {
// return validation.Success(n)
// }
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
// }
// }
//
// // Recover from specific errors with a default value
// withDefault := ChainLeft(func(errs Errors) Validate[int, int] {
// for _, err := range errs {
// if err.Messsage == "must be positive" {
// return Of[int](0) // recover with default
// }
// }
// return func(input int) Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return either.Left[int](errs)
// }
// }
// })
//
// validator := withDefault(validatePositive)
// result := validator(-5)(nil)
// // Result: Success(0) - recovered from failure
//
// # Example: Error Context Addition
//
// // Add contextual information to errors
// addContext := ChainLeft(func(errs Errors) Validate[string, int] {
// return func(input string) Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return either.Left[int](validation.Errors{
// {
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to validate user age",
// },
// })
// }
// }
// })
//
// validator := addContext(someValidator)
// // Errors will include both original error and context
//
// # Example: Input-Dependent Recovery
//
// // Recover with different defaults based on input
// smartDefault := ChainLeft(func(errs Errors) Validate[string, int] {
// return func(input string) Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// // Use input to determine appropriate default
// if strings.Contains(input, "http") {
// return validation.Of(80)
// }
// if strings.Contains(input, "https") {
// return validation.Of(443)
// }
// return validation.Of(8080)
// }
// }
// })
//
// # Notes
//
// - Errors are accumulated, not replaced - this ensures no validation failures are lost
// - The handler has access to both the errors and the original input
// - Success values bypass the handler completely
// - This enables sophisticated error handling strategies including recovery, enrichment, and transformation
// - Use OrElse as a semantic alias when emphasizing fallback/alternative logic
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return readert.Chain[Validate[I, A]](
decode.ChainLeft,
f,
)
}
// MonadChainLeft sequences a computation on the failure (Left) channel of a validation.
//
// This is the direct application version of ChainLeft. It operates on the error path
// of validation, allowing you to transform, enrich, or recover from validation failures.
// It's the dual of Chain - while Chain operates on success values, MonadChainLeft
// operates on error values.
//
// # Key Behavior
//
// **Critical difference from standard Either operations**: This validation-specific
// implementation **aggregates errors** using the Errors monoid. When the transformation
// function returns a failure, both the original errors AND the new errors are combined,
// ensuring comprehensive error reporting.
//
// 1. **Success Pass-Through**: If validation succeeds, the handler is never called and
// the success value passes through unchanged.
//
// 2. **Error Recovery**: The handler can recover from failures by returning a successful
// validation, converting Left to Right.
//
// 3. **Error Aggregation**: When the handler also returns a failure, both the original
// errors and the new errors are combined using the Errors monoid.
//
// 4. **Input Access**: The handler returns a Validate[I, A] function, giving it access
// to the original input value I for context-aware error handling.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - fa: The Validate[I, A] to transform
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
// is called only when validation fails, receiving the accumulated errors.
//
// # Returns
//
// A Validate[I, A] that handles error cases according to the provided function.
//
// # Example: Error Recovery
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// )
//
// // Validator that may fail
// validatePositive := func(n int) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// if n > 0 {
// return validation.Success(n)
// }
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
// }
// }
//
// // Recover from specific errors with a default value
// withDefault := func(errs validation.Errors) validate.Validate[int, int] {
// for _, err := range errs {
// if err.Messsage == "must be positive" {
// return validate.Of[int](0) // recover with default
// }
// }
// // Propagate other errors
// return func(input int) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return either.Left[int](errs)
// }
// }
// }
//
// validator := validate.MonadChainLeft(validatePositive, withDefault)
// result := validator(-5)(nil)
// // Result: Success(0) - recovered from failure
//
// # Example: Error Context Addition
//
// // Add contextual information to errors
// addContext := func(errs validation.Errors) validate.Validate[string, int] {
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// // Add context error (will be aggregated with original)
// return either.Left[int](validation.Errors{
// {
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to validate user age",
// },
// })
// }
// }
// }
//
// validator := validate.MonadChainLeft(someValidator, addContext)
// // Errors will include both original error and context
//
// # Example: Input-Dependent Recovery
//
// // Recover with different defaults based on input
// smartDefault := func(errs validation.Errors) validate.Validate[string, int] {
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// // Use input to determine appropriate default
// if strings.Contains(input, "http:") {
// return validation.Success(80)
// }
// if strings.Contains(input, "https:") {
// return validation.Success(443)
// }
// return validation.Success(8080)
// }
// }
// }
//
// validator := validate.MonadChainLeft(parsePort, smartDefault)
//
// # Notes
//
// - Errors are accumulated, not replaced - this ensures no validation failures are lost
// - The handler has access to both the errors and the original input
// - Success values bypass the handler completely
// - This is the direct application version of ChainLeft
// - This enables sophisticated error handling strategies including recovery, enrichment, and transformation
//
// # See Also
//
// - ChainLeft: The curried, point-free version
// - OrElse: Semantic alias for ChainLeft emphasizing fallback logic
// - MonadAlt: Simplified alternative that ignores error details
// - Alt: Curried version of MonadAlt
func MonadChainLeft[I, A any](fa Validate[I, A], f Kleisli[I, Errors, A]) Validate[I, A] {
return readert.MonadChain(
decode.MonadChainLeft,
fa,
f,
)
}
// OrElse provides an alternative validation when the primary validation fails.
//
// This is a semantic alias for ChainLeft with identical behavior. The name "OrElse"
// emphasizes the intent of providing fallback or alternative validation logic, making
// code more readable when that's the primary use case.
//
// # Relationship to ChainLeft
//
// **OrElse and ChainLeft are functionally identical** - they produce exactly the same
// results for all inputs. The choice between them is purely about code readability:
//
// - Use **OrElse** when emphasizing fallback/alternative validation logic
// - Use **ChainLeft** when emphasizing technical error channel transformation
//
// Both maintain the critical property of **error aggregation**, ensuring all validation
// failures are preserved and reported together.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
// is called only when validation fails, receiving the accumulated errors.
//
// # Returns
//
// An Operator[I, A, A] that transforms validators by providing alternative validation.
//
// # Example: Fallback Validation
//
// // Primary validator that may fail
// validateFromConfig := func(key string) Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// // Try to get value from config
// if value, ok := config[key]; ok {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not found in config")(ctx)
// }
// }
//
// // Use OrElse for semantic clarity - "try config, or else use environment"
// withEnvFallback := OrElse(func(errs Errors) Validate[string, string] {
// return func(key string) Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if value := os.Getenv(key); value != "" {
// return validation.Success(value)
// }
// return either.Left[string](errs) // propagate original errors
// }
// }
// })
//
// validator := withEnvFallback(validateFromConfig)
// result := validator("DATABASE_URL")(nil)
// // Tries config first, falls back to environment variable
//
// # Example: Default Value on Failure
//
// // Provide a default value when validation fails
// withDefault := OrElse(func(errs Errors) Validate[int, int] {
// return Of[int](0) // default to 0 on any failure
// })
//
// validator := withDefault(someValidator)
// result := validator(input)(nil)
// // Always succeeds, using default value if validation fails
//
// # Example: Pipeline with Multiple Fallbacks
//
// // Build a validation pipeline with multiple fallback strategies
// validator := F.Pipe2(
// validateFromDatabase,
// OrElse(func(errs Errors) Validate[string, Config] {
// // Try cache as first fallback
// return validateFromCache
// }),
// OrElse(func(errs Errors) Validate[string, Config] {
// // Use default config as final fallback
// return Of[string](defaultConfig)
// }),
// )
// // Tries database, then cache, then default
//
// # Notes
//
// - Identical behavior to ChainLeft - they are aliases
// - Errors are accumulated when transformations fail
// - Success values pass through unchanged
// - The handler has access to both errors and original input
// - Choose OrElse for better readability when providing alternatives
// - See ChainLeft documentation for detailed behavior and additional examples
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return ChainLeft(f)
}
// MonadAp applies a validator containing a function to a validator containing a value.
//
// This is the applicative apply operation for Validate. It allows you to apply
@@ -409,3 +768,218 @@ func Ap[B, I, A any](fa Validate[I, A]) Operator[I, func(A) B, B] {
fa,
)
}
// Alt provides an alternative validator when the primary validator fails.
//
// This is the curried, point-free version of MonadAlt. It creates an operator that
// transforms a validator by adding a fallback alternative. When the first validator
// fails, the second (lazily evaluated) validator is tried. If both fail, errors are
// aggregated.
//
// Alt implements the Alternative typeclass pattern, providing a way to express
// "try this, or else try that" logic in a composable way.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - second: A lazy Validate[I, A] that serves as the fallback. It's only evaluated
// if the first validator fails.
//
// # Returns
//
// An Operator[I, A, A] that transforms validators by adding alternative fallback logic.
//
// # Behavior
//
// - **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
//
// # Example: Fallback Validation
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// )
//
// // Primary validator that may fail
// validateFromConfig := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// // Try to get value from config
// if value, ok := config[key]; ok {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not in config")(ctx)
// }
// }
//
// // Fallback to environment variable
// validateFromEnv := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if value := os.Getenv(key); value != "" {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not in env")(ctx)
// }
// }
//
// // Use Alt to add fallback - point-free style
// withFallback := validate.Alt(func() validate.Validate[string, string] {
// return validateFromEnv
// })
//
// validator := withFallback(validateFromConfig)
// result := validator("DATABASE_URL")(nil)
// // Tries config first, falls back to environment variable
//
// # Example: Pipeline with Multiple Alternatives
//
// // Chain multiple alternatives using function composition
// validator := F.Pipe2(
// validateFromDatabase,
// validate.Alt(func() validate.Validate[string, Config] {
// return validateFromCache
// }),
// validate.Alt(func() validate.Validate[string, Config] {
// return validate.Of[string](defaultConfig)
// }),
// )
// // Tries database, then cache, then default
//
// # Notes
//
// - The second validator is lazily evaluated for efficiency
// - First success short-circuits evaluation
// - Errors are aggregated when both fail
// - This is the point-free version of MonadAlt
// - Useful for building validation pipelines with F.Pipe
//
// # See Also
//
// - MonadAlt: The direct application version
// - ChainLeft: The more general error transformation operator
// - OrElse: Semantic alias for ChainLeft
// - AltMonoid: For combining multiple alternatives with monoid structure
func Alt[I, A any](second Lazy[Validate[I, A]]) Operator[I, A, A] {
return ChainLeft(function.Ignore1of1[Errors](second))
}
// MonadAlt provides an alternative validator when the primary validator fails.
//
// This is the direct application version of Alt. It takes two validators and returns
// a new validator that tries the first, and if it fails, tries the second. If both
// fail, errors from both are aggregated.
//
// MonadAlt implements the Alternative typeclass pattern, enabling "try this, or else
// try that" logic with comprehensive error reporting.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - first: The primary Validate[I, A] to try first
// - second: A lazy Validate[I, A] that serves as the fallback. It's only evaluated
// if the first validator fails.
//
// # Returns
//
// A Validate[I, A] that tries the first validator, falling back to the second if needed.
//
// # Behavior
//
// - **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
//
// # Example: Configuration with Fallback
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// )
//
// // Primary validator
// validateFromConfig := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if value, ok := config[key]; ok {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not in config")(ctx)
// }
// }
//
// // Fallback validator
// validateFromEnv := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if value := os.Getenv(key); value != "" {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not in env")(ctx)
// }
// }
//
// // Combine with MonadAlt
// validator := validate.MonadAlt(
// validateFromConfig,
// func() validate.Validate[string, string] { return validateFromEnv },
// )
// result := validator("DATABASE_URL")(nil)
// // Tries config first, falls back to environment variable
//
// # Example: Multiple Fallbacks
//
// // Chain multiple alternatives
// validator := validate.MonadAlt(
// validate.MonadAlt(
// validateFromDatabase,
// func() validate.Validate[string, Config] { return validateFromCache },
// ),
// func() validate.Validate[string, Config] { return validate.Of[string](defaultConfig) },
// )
// // Tries database, then cache, then default
//
// # Example: Error Aggregation
//
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "error 1")(ctx)
// }
// }
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "error 2")(ctx)
// }
// }
//
// validator := validate.MonadAlt(
// failing1,
// func() validate.Validate[string, int] { return failing2 },
// )
// result := validator("input")(nil)
// // result contains both "error 1" and "error 2"
//
// # Notes
//
// - The second validator is lazily evaluated for efficiency
// - First success short-circuits evaluation (second not called)
// - Errors are aggregated when both fail
// - This is equivalent to Alt but with direct application
// - Both validators receive the same input value
//
// # See Also
//
// - Alt: The curried, point-free version
// - MonadChainLeft: The underlying error transformation operation
// - OrElse: Semantic alias for ChainLeft
// - AltMonoid: For combining multiple alternatives with monoid structure
func MonadAlt[I, A any](first Validate[I, A], second Lazy[Validate[I, A]]) Validate[I, A] {
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
}

View File

@@ -849,3 +849,428 @@ func TestFunctorLaws(t *testing.T) {
}
})
}
// TestChainLeft tests the ChainLeft function
func TestChainLeft(t *testing.T) {
t.Run("transforms failures while preserving successes", func(t *testing.T) {
// Create a failing validator
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "validation failed")(ctx)
}
}
// Handler that recovers from specific errors
handler := ChainLeft(func(errs Errors) Validate[int, int] {
for _, err := range errs {
if err.Messsage == "validation failed" {
return Of[int](0) // recover with default
}
}
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return E.Left[int](errs)
}
}
})
validator := handler(failingValidator)
result := validator(-5)(nil)
assert.Equal(t, validation.Of(0), result, "Should recover from failure")
})
t.Run("preserves success values unchanged", func(t *testing.T) {
successValidator := Of[int](42)
handler := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](input, "should not be called")(ctx)
}
}
})
validator := handler(successValidator)
result := validator(100)(nil)
assert.Equal(t, validation.Of(42), result, "Success should pass through unchanged")
})
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
failingValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
return validation.FailureWithMessage[string](s, "original error")(ctx)
}
}
handler := ChainLeft(func(errs Errors) Validate[string, string] {
return func(input string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
return validation.FailureWithMessage[string](input, "additional error")(ctx)
}
}
})
validator := handler(failingValidator)
result := validator("test")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 2, "Should aggregate both errors")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "original error")
assert.Contains(t, messages, "additional error")
})
t.Run("adds context to errors", func(t *testing.T) {
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "invalid value")(ctx)
}
}
addContext := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return E.Left[int](validation.Errors{
{
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
Messsage: "failed to validate user age",
},
})
}
}
})
validator := addContext(failingValidator)
result := validator(150)(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 2, "Should have both original and context errors")
})
t.Run("can be composed in pipeline", func(t *testing.T) {
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "error1")(ctx)
}
}
handler1 := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](input, "error2")(ctx)
}
}
})
handler2 := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](input, "error3")(ctx)
}
}
})
validator := handler2(handler1(failingValidator))
result := validator(42)(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.GreaterOrEqual(t, len(errors), 2, "Should accumulate errors through pipeline")
})
t.Run("provides access to original input", func(t *testing.T) {
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "failed")(ctx)
}
}
// Handler uses input to determine recovery strategy
handler := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
// Use input value to decide on recovery
if input < 0 {
return validation.Of(0)
}
if input > 100 {
return validation.Of(100)
}
return E.Left[int](errs)
}
}
})
validator := handler(failingValidator)
result1 := validator(-10)(nil)
assert.Equal(t, validation.Of(0), result1, "Should recover negative to 0")
result2 := validator(150)(nil)
assert.Equal(t, validation.Of(100), result2, "Should recover large to 100")
})
t.Run("works with different input and output types", func(t *testing.T) {
// Validator that converts string to int
parseValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](s, "parse failed")(ctx)
}
}
// Handler that provides default based on input string
handler := ChainLeft(func(errs Errors) Validate[string, int] {
return func(input string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
if input == "default" {
return validation.Of(42)
}
return E.Left[int](errs)
}
}
})
validator := handler(parseValidator)
result := validator("default")(nil)
assert.Equal(t, validation.Of(42), result)
})
}
// TestOrElse tests the OrElse function
func TestOrElse(t *testing.T) {
t.Run("provides fallback for failing validation", func(t *testing.T) {
// Primary validator that fails
primaryValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
return validation.FailureWithMessage[string](s, "not found")(ctx)
}
}
// Use OrElse to provide fallback
withFallback := OrElse(func(errs Errors) Validate[string, string] {
return Of[string]("default value")
})
validator := withFallback(primaryValidator)
result := validator("missing")(nil)
assert.Equal(t, validation.Of("default value"), result)
})
t.Run("preserves success values unchanged", func(t *testing.T) {
successValidator := Of[string]("success")
withFallback := OrElse(func(errs Errors) Validate[string, string] {
return Of[string]("fallback")
})
validator := withFallback(successValidator)
result := validator("input")(nil)
assert.Equal(t, validation.Of("success"), result, "Should not use fallback for success")
})
t.Run("aggregates errors when fallback also fails", func(t *testing.T) {
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "primary failed")(ctx)
}
}
withFallback := OrElse(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](input, "fallback failed")(ctx)
}
}
})
validator := withFallback(failingValidator)
result := validator(42)(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 2, "Should aggregate both errors")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "primary failed")
assert.Contains(t, messages, "fallback failed")
})
t.Run("supports multiple fallback strategies", func(t *testing.T) {
failingValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
return validation.FailureWithMessage[string](s, "not in database")(ctx)
}
}
// First fallback: try cache
tryCache := OrElse(func(errs Errors) Validate[string, string] {
return func(input string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
if input == "cached" {
return validation.Of("from cache")
}
return E.Left[string](errs)
}
}
})
// Second fallback: use default
useDefault := OrElse(func(errs Errors) Validate[string, string] {
return Of[string]("default")
})
// Compose fallbacks
validator := useDefault(tryCache(failingValidator))
// Test with cached value
result1 := validator("cached")(nil)
assert.Equal(t, validation.Of("from cache"), result1)
// Test with non-cached value (should use default)
result2 := validator("other")(nil)
assert.Equal(t, validation.Of("default"), result2)
})
t.Run("provides input-dependent fallback", func(t *testing.T) {
failingValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](s, "parse failed")(ctx)
}
}
// Fallback with different defaults based on input
smartFallback := OrElse(func(errs Errors) Validate[string, int] {
return func(input string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
// Provide context-aware defaults
if input == "http" {
return validation.Of(80)
}
if input == "https" {
return validation.Of(443)
}
return validation.Of(8080)
}
}
})
validator := smartFallback(failingValidator)
result1 := validator("http")(nil)
assert.Equal(t, validation.Of(80), result1)
result2 := validator("https")(nil)
assert.Equal(t, validation.Of(443), result2)
result3 := validator("other")(nil)
assert.Equal(t, validation.Of(8080), result3)
})
t.Run("is equivalent to ChainLeft", func(t *testing.T) {
// Create identical handlers
handler := func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
if input < 0 {
return validation.Of(0)
}
return E.Left[int](errs)
}
}
}
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "failed")(ctx)
}
}
// Apply with ChainLeft
withChainLeft := ChainLeft(handler)(failingValidator)
// Apply with OrElse
withOrElse := OrElse(handler)(failingValidator)
// Test with same inputs
inputs := []int{-10, 0, 10, -5, 100}
for _, input := range inputs {
result1 := withChainLeft(input)(nil)
result2 := withOrElse(input)(nil)
// Results should be identical
assert.Equal(t, E.IsLeft(result1), E.IsLeft(result2))
if E.IsRight(result1) {
val1, _ := E.Unwrap(result1)
val2, _ := E.Unwrap(result2)
assert.Equal(t, val1, val2, "OrElse and ChainLeft should produce identical results")
}
}
})
t.Run("works in complex validation pipeline", func(t *testing.T) {
type Config struct {
Port int
Host string
}
// Validator that tries to parse config
parseConfig := func(s string) Reader[validation.Context, validation.Validation[Config]] {
return func(ctx validation.Context) validation.Validation[Config] {
return validation.FailureWithMessage[Config](s, "invalid config")(ctx)
}
}
// Fallback to environment variables
tryEnv := OrElse(func(errs Errors) Validate[string, Config] {
return func(input string) Reader[validation.Context, validation.Validation[Config]] {
return func(ctx validation.Context) validation.Validation[Config] {
// Simulate env var lookup
if input == "from_env" {
return validation.Of(Config{Port: 8080, Host: "localhost"})
}
return E.Left[Config](errs)
}
}
})
// Final fallback to defaults
useDefaults := OrElse(func(errs Errors) Validate[string, Config] {
return Of[string](Config{Port: 3000, Host: "0.0.0.0"})
})
// Build pipeline
validator := useDefaults(tryEnv(parseConfig))
// Test with env fallback
result1 := validator("from_env")(nil)
assert.True(t, E.IsRight(result1))
if E.IsRight(result1) {
cfg, _ := E.Unwrap(result1)
assert.Equal(t, 8080, cfg.Port)
assert.Equal(t, "localhost", cfg.Host)
}
// Test with default fallback
result2 := validator("other")(nil)
assert.True(t, E.IsRight(result2))
if E.IsRight(result2) {
cfg, _ := E.Unwrap(result2)
assert.Equal(t, 3000, cfg.Port)
assert.Equal(t, "0.0.0.0", cfg.Host)
}
})
}

View File

@@ -0,0 +1,162 @@
# OrElse is Equivalent to ChainLeft
## Overview
In [`optics/codec/validation/monad.go`](monad.go:474-476), the [`OrElse`](monad.go:474) function is defined as a simple alias for [`ChainLeft`](monad.go:304):
```go
//go:inline
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
return ChainLeft(f)
}
```
This means **`OrElse` and `ChainLeft` are functionally identical** - they produce exactly the same results for all inputs.
## Why Have Both?
While they are technically the same, they serve different **semantic purposes**:
### ChainLeft - Technical Perspective
[`ChainLeft`](monad.go:304-309) emphasizes the **technical operation**: it chains a computation on the Left (failure) channel of the Either/Validation monad. This name comes from category theory and functional programming terminology.
### OrElse - Semantic Perspective
[`OrElse`](monad.go:474-476) emphasizes the **intent**: it provides an alternative or fallback when validation fails. The name reads naturally in code: "try this validation, **or else** try this alternative."
## Key Behavior
Both functions share the same critical behavior that distinguishes them from standard Either operations:
### Error Aggregation
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid. This ensures no validation errors are lost.
```go
// Example: Error aggregation
result := OrElse(func(errs Errors) Validation[string] {
return Failures[string](Errors{
&ValidationError{Messsage: "additional error"},
})
})(Failures[string](Errors{
&ValidationError{Messsage: "original error"},
}))
// Result contains BOTH errors: ["original error", "additional error"]
```
### Success Pass-Through
Success values pass through unchanged - the function is never called:
```go
result := OrElse(func(errs Errors) Validation[int] {
return Failures[int](Errors{
&ValidationError{Messsage: "never called"},
})
})(Success(42))
// Result: Success(42) - unchanged
```
### Error Recovery
The function can recover from failures by returning a Success:
```go
recoverFromNotFound := OrElse(func(errs Errors) Validation[int] {
for _, err := range errs {
if err.Messsage == "not found" {
return Success(0) // recover with default
}
}
return Failures[int](errs)
})
result := recoverFromNotFound(Failures[int](Errors{
&ValidationError{Messsage: "not found"},
}))
// Result: Success(0) - recovered from failure
```
## Use Cases
### 1. Fallback Validation (OrElse reads better)
```go
validatePositive := func(x int) Validation[int] {
if x > 0 {
return Success(x)
}
return Failures[int](Errors{
&ValidationError{Messsage: "must be positive"},
})
}
// Use OrElse for semantic clarity
withDefault := OrElse(func(errs Errors) Validation[int] {
return Success(1) // default to 1 if validation fails
})
result := F.Pipe1(validatePositive(-5), withDefault)
// Result: Success(1)
```
### 2. Error Context Addition (ChainLeft reads better)
```go
addContext := ChainLeft(func(errs Errors) Validation[string] {
return Failures[string](Errors{
&ValidationError{
Messsage: "validation failed in user.email field",
},
})
})
result := F.Pipe1(
Failures[string](Errors{
&ValidationError{Messsage: "invalid format"},
}),
addContext,
)
// Result contains: ["invalid format", "validation failed in user.email field"]
```
### 3. Pipeline Composition
Both can be used in pipelines, with errors accumulating at each step:
```go
result := F.Pipe2(
Failures[int](Errors{
&ValidationError{Messsage: "database error"},
}),
OrElse(func(errs Errors) Validation[int] {
return Failures[int](Errors{
&ValidationError{Messsage: "context added"},
})
}),
OrElse(func(errs Errors) Validation[int] {
return Failures[int](errs) // propagate
}),
)
// Errors accumulate at each step in the pipeline
```
## Verification
The test suite in [`monad_test.go`](monad_test.go:1698) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
- ✅ Identical behavior for Success values
- ✅ Identical behavior for error recovery
- ✅ Identical behavior for error aggregation
- ✅ Identical behavior in pipeline composition
- ✅ Identical behavior for multiple error scenarios
Run the tests:
```bash
go test -v -run TestOrElse ./optics/codec/validation
```
## Conclusion
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**:
- Use **`OrElse`** when emphasizing fallback/alternative validation logic
- Use **`ChainLeft`** when emphasizing technical error channel transformation
Both maintain the critical validation property of **error aggregation**, ensuring all validation failures are preserved and reported together.

View File

@@ -17,12 +17,7 @@ func TestDo(t *testing.T) {
}
result := Do(State{})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{}, value)
assert.Equal(t, Of(State{}), result)
})
t.Run("creates successful validation with initialized state", func(t *testing.T) {
@@ -33,24 +28,19 @@ func TestDo(t *testing.T) {
initial := State{x: 42, y: "hello"}
result := Do(initial)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, initial, value)
assert.Equal(t, Of(initial), result)
})
t.Run("works with different types", func(t *testing.T) {
intResult := Do(0)
assert.True(t, either.IsRight(intResult))
assert.Equal(t, Of(0), intResult)
strResult := Do("")
assert.True(t, either.IsRight(strResult))
assert.Equal(t, Of(""), strResult)
type Custom struct{ Value int }
customResult := Do(Custom{Value: 100})
assert.True(t, either.IsRight(customResult))
assert.Equal(t, Of(Custom{Value: 100}), customResult)
})
}
@@ -71,12 +61,7 @@ func TestBind(t *testing.T) {
}, func(s State) Validation[int] { return Success(10) }),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 42, y: 10}, value)
assert.Equal(t, Of(State{x: 42, y: 10}), result)
})
t.Run("propagates failure", func(t *testing.T) {
@@ -115,12 +100,7 @@ func TestBind(t *testing.T) {
}),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 10, y: 20}, value)
assert.Equal(t, Success(State{x: 10, y: 20}), result)
})
}
@@ -138,12 +118,7 @@ func TestLet(t *testing.T) {
}, func(s State) int { return s.x * 2 }),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 5, computed: 10}, value)
assert.Equal(t, Of(State{x: 5, computed: 10}), result)
})
t.Run("preserves failure", func(t *testing.T) {
@@ -180,12 +155,7 @@ func TestLet(t *testing.T) {
}, func(s State) int { return s.z * 3 }),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
assert.Equal(t, Of(State{x: 60, y: 10, z: 20}), result)
})
}
@@ -203,12 +173,7 @@ func TestLetTo(t *testing.T) {
}, "example"),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 5, name: "example"}, value)
assert.Equal(t, Of(State{x: 5, name: "example"}), result)
})
t.Run("preserves failure", func(t *testing.T) {
@@ -239,12 +204,7 @@ func TestLetTo(t *testing.T) {
}, true),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{name: "app", version: 2, active: true}, value)
assert.Equal(t, Of(State{name: "app", version: 2, active: true}), result)
})
}
@@ -259,12 +219,7 @@ func TestBindTo(t *testing.T) {
BindTo(func(x int) State { return State{value: x} }),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{value: 42}, value)
assert.Equal(t, Of(State{value: 42}), result)
})
t.Run("preserves failure", func(t *testing.T) {
@@ -289,12 +244,7 @@ func TestBindTo(t *testing.T) {
BindTo(func(s string) StringState { return StringState{text: s} }),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) StringState { return StringState{} },
F.Identity[StringState],
)
assert.Equal(t, StringState{text: "hello"}, value)
assert.Equal(t, Of(StringState{text: "hello"}), result)
})
}
@@ -312,12 +262,7 @@ func TestApS(t *testing.T) {
}, Success(42)),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 42}, value)
assert.Equal(t, Of(State{x: 42}), result)
})
t.Run("accumulates errors from both validations", func(t *testing.T) {
@@ -350,12 +295,7 @@ func TestApS(t *testing.T) {
}, Success(20)),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 10, y: 20}, value)
assert.Equal(t, Of(State{x: 10, y: 20}), result)
})
}
@@ -384,14 +324,11 @@ func TestApSL(t *testing.T) {
),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) Person { return Person{} },
F.Identity[Person],
)
assert.Equal(t, "Alice", value.Name)
assert.Equal(t, "Main St", value.Address.Street)
assert.Equal(t, "NYC", value.Address.City)
expected := Person{
Name: "Alice",
Address: Address{Street: "Main St", City: "NYC"},
}
assert.Equal(t, Of(expected), result)
})
t.Run("accumulates errors", func(t *testing.T) {
@@ -434,12 +371,7 @@ func TestBindL(t *testing.T) {
BindL(valueLens, increment),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, Counter{Value: 43}, value)
assert.Equal(t, Of(Counter{Value: 43}), result)
})
t.Run("fails validation based on current value", func(t *testing.T) {
@@ -494,12 +426,7 @@ func TestLetL(t *testing.T) {
LetL(valueLens, double),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, Counter{Value: 42}, value)
assert.Equal(t, Of(Counter{Value: 42}), result)
})
t.Run("preserves failure", func(t *testing.T) {
@@ -521,12 +448,7 @@ func TestLetL(t *testing.T) {
LetL(valueLens, double),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, Counter{Value: 30}, value)
assert.Equal(t, Of(Counter{Value: 30}), result)
})
}
@@ -547,12 +469,7 @@ func TestLetToL(t *testing.T) {
LetToL(debugLens, false),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) Config { return Config{} },
F.Identity[Config],
)
assert.Equal(t, Config{Debug: false, Timeout: 30}, value)
assert.Equal(t, Of(Config{Debug: false, Timeout: 30}), result)
})
t.Run("preserves failure", func(t *testing.T) {
@@ -574,12 +491,7 @@ func TestLetToL(t *testing.T) {
LetToL(timeoutLens, 60),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) Config { return Config{} },
F.Identity[Config],
)
assert.Equal(t, Config{Debug: false, Timeout: 60}, value)
assert.Equal(t, Of(Config{Debug: false, Timeout: 60}), result)
})
}
@@ -622,13 +534,7 @@ func TestBindOperationsComposition(t *testing.T) {
}),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) User { return User{} },
F.Identity[User],
)
assert.Equal(t, "Alice", value.Name)
assert.Equal(t, 25, value.Age)
assert.Equal(t, "Alice@example.com", value.Email)
expected := User{Name: "Alice", Age: 25, Email: "Alice@example.com"}
assert.Equal(t, Of(expected), result)
})
}

View File

@@ -469,3 +469,173 @@ func Applicative[A, B any]() applicative.Applicative[A, B, Validation[A], Valida
errorsMonoid,
)
}
//go:inline
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
return ChainLeft(f)
}
// MonadAlt implements the Alternative operation for Validation, providing fallback behavior.
// If the first validation fails, it evaluates and returns the second validation as an alternative.
// If the first validation succeeds, it returns the first validation without evaluating the second.
//
// This is the fundamental operation for the Alt typeclass, enabling "try first, fallback to second"
// semantics. It's particularly useful for:
// - Providing default values when validation fails
// - Trying multiple validation strategies in sequence
// - Building validation pipelines with fallback logic
// - Implementing optional validation with defaults
//
// **Key behavior**: When both validations fail, MonadAlt DOES accumulate errors from both
// validations using the Errors monoid. This is different from standard Either Alt behavior.
// The error accumulation happens through the underlying ChainLeft/chainErrors mechanism.
//
// The second parameter is lazy (Lazy[Validation[A]]) to avoid unnecessary computation when
// the first validation succeeds. The second validation is only evaluated if needed.
//
// Behavior:
// - First succeeds: returns first validation (second is not evaluated)
// - First fails, second succeeds: returns second validation
// - Both fail: aggregates errors from both validations
//
// This is useful for:
// - Fallback values: provide defaults when primary validation fails
// - Alternative strategies: try different validation approaches
// - Optional validation: make validation optional with a default
// - Chaining attempts: try multiple sources until one succeeds
//
// Type Parameters:
// - A: The type of the successful value
//
// Parameters:
// - first: The primary validation to try
// - second: A lazy computation producing the fallback validation (only evaluated if first fails)
//
// Returns:
//
// The first validation if it succeeds, otherwise the second validation
//
// Example - Fallback to default:
//
// primary := parseConfig("config.json") // Fails
// fallback := func() Validation[Config] {
// return Success(defaultConfig)
// }
// result := MonadAlt(primary, fallback)
// // Result: Success(defaultConfig)
//
// Example - First succeeds (second not evaluated):
//
// primary := Success(42)
// fallback := func() Validation[int] {
// panic("never called") // This won't execute
// }
// result := MonadAlt(primary, fallback)
// // Result: Success(42)
//
// Example - Chaining multiple alternatives:
//
// result := MonadAlt(
// parseFromEnv("API_KEY"),
// func() Validation[string] {
// return MonadAlt(
// parseFromFile(".env"),
// func() Validation[string] {
// return Success("default-key")
// },
// )
// },
// )
// // Tries: env var → file → default (uses first that succeeds)
//
// Example - Error accumulation when both fail:
//
// v1 := Failures[int](Errors{
// &ValidationError{Messsage: "error 1"},
// &ValidationError{Messsage: "error 2"},
// })
// v2 := func() Validation[int] {
// return Failures[int](Errors{
// &ValidationError{Messsage: "error 3"},
// })
// }
// result := MonadAlt(v1, v2)
// // Result: Failures with ALL errors ["error 1", "error 2", "error 3"]
// // The errors from v1 are aggregated with errors from v2
func MonadAlt[A any](first Validation[A], second Lazy[Validation[A]]) Validation[A] {
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
}
// Alt is the curried version of [MonadAlt].
// Returns a function that provides fallback behavior for a Validation.
//
// This is useful for creating reusable fallback operators that can be applied
// to multiple validations, or for use in function composition pipelines.
//
// The returned function takes a validation and returns either that validation
// (if successful) or the provided alternative (if the validation fails).
//
// Type Parameters:
// - A: The type of the successful value
//
// Parameters:
// - second: A lazy computation producing the fallback validation
//
// Returns:
//
// A function that takes a Validation[A] and returns a Validation[A] with fallback behavior
//
// Example - Creating a reusable fallback operator:
//
// withDefault := Alt(func() Validation[int] {
// return Success(0)
// })
//
// result1 := withDefault(parseNumber("42")) // Success(42)
// result2 := withDefault(parseNumber("abc")) // Success(0) - fallback
// result3 := withDefault(parseNumber("123")) // Success(123)
//
// Example - Using in a pipeline:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe2(
// parseFromEnv("CONFIG_PATH"),
// Alt(func() Validation[string] {
// return parseFromFile("config.json")
// }),
// Alt(func() Validation[string] {
// return Success("./default-config.json")
// }),
// )
// // Tries: env var → file → default path
//
// Example - Combining with Map:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe2(
// validatePositive(-5), // Fails
// Alt(func() Validation[int] { return Success(1) }),
// Map(func(x int) int { return x * 2 }),
// )
// // Result: Success(2) - uses fallback value 1, then doubles it
//
// Example - Multiple fallback layers:
//
// primaryFallback := Alt(func() Validation[Config] {
// return loadFromFile("backup.json")
// })
// secondaryFallback := Alt(func() Validation[Config] {
// return Success(defaultConfig)
// })
//
// result := F.Pipe2(
// loadFromFile("config.json"),
// primaryFallback,
// secondaryFallback,
// )
// // Tries: config.json → backup.json → default
func Alt[A any](second Lazy[Validation[A]]) Operator[A, A] {
return ChainLeft(function.Ignore1of1[Errors](second))
}

View File

@@ -15,24 +15,19 @@ func TestOf(t *testing.T) {
t.Run("creates successful validation", func(t *testing.T) {
result := Of(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("works with different types", func(t *testing.T) {
strResult := Of("hello")
assert.True(t, either.IsRight(strResult))
assert.Equal(t, Of("hello"), strResult)
boolResult := Of(true)
assert.True(t, either.IsRight(boolResult))
assert.Equal(t, Of(true), boolResult)
type Custom struct{ Value int }
customResult := Of(Custom{Value: 100})
assert.True(t, either.IsRight(customResult))
assert.Equal(t, Of(Custom{Value: 100}), customResult)
})
t.Run("is equivalent to Success", func(t *testing.T) {
@@ -49,12 +44,7 @@ func TestMap(t *testing.T) {
double := N.Mul(2)
result := Map(double)(Of(21))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("preserves failure", func(t *testing.T) {
@@ -85,23 +75,14 @@ func TestMap(t *testing.T) {
Map(toString),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "30", value)
assert.Equal(t, Of("30"), result)
})
t.Run("type transformation", func(t *testing.T) {
length := func(s string) int { return len(s) }
result := Map(length)(Of("hello"))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 5, value)
assert.Equal(t, Of(5), result)
})
}
@@ -113,12 +94,7 @@ func TestAp(t *testing.T) {
result := Ap[int](valueValidation)(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("accumulates errors when value fails", func(t *testing.T) {
@@ -184,12 +160,7 @@ func TestAp(t *testing.T) {
result := Ap[string](valueValidation)(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "UPPER:hello", value)
assert.Equal(t, Of("UPPER:hello"), result)
})
t.Run("accumulates multiple validation errors from different sources", func(t *testing.T) {
@@ -296,17 +267,8 @@ func TestApWithOperator(t *testing.T) {
result1 := operator(Of(double))
result2 := operator(Of(triple))
val1 := either.MonadFold(result1,
func(Errors) int { return 0 },
F.Identity[int],
)
val2 := either.MonadFold(result2,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, val1)
assert.Equal(t, 63, val2)
assert.Equal(t, Of(42), result1)
assert.Equal(t, Of(63), result2)
})
}
@@ -324,14 +286,8 @@ func TestApplicative(t *testing.T) {
result1 := app1.Of(42)
result2 := app2.Of(43)
assert.True(t, either.IsRight(result1))
assert.True(t, either.IsRight(result2))
val1 := either.MonadFold(result1, func(Errors) int { return 0 }, F.Identity[int])
val2 := either.MonadFold(result2, func(Errors) int { return 0 }, F.Identity[int])
assert.Equal(t, 42, val1)
assert.Equal(t, 43, val2)
assert.Equal(t, Of(42), result1)
assert.Equal(t, Of(43), result2)
})
}
@@ -340,36 +296,18 @@ func TestApplicativeOf(t *testing.T) {
t.Run("wraps a value in Validation context", func(t *testing.T) {
result := app.Of(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("wraps string value", func(t *testing.T) {
app := Applicative[string, int]()
result := app.Of("hello")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "hello", value)
assert.Equal(t, Of("hello"), result)
})
t.Run("wraps zero value", func(t *testing.T) {
result := app.Of(0)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
assert.Equal(t, Of(0), result)
})
t.Run("wraps complex types", func(t *testing.T) {
@@ -381,12 +319,7 @@ func TestApplicativeOf(t *testing.T) {
user := User{Name: "Alice", Age: 30}
result := app.Of(user)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) User { return User{} },
F.Identity[User],
)
assert.Equal(t, user, value)
assert.Equal(t, Of(user), result)
})
}
@@ -397,12 +330,7 @@ func TestApplicativeMap(t *testing.T) {
double := N.Mul(2)
result := app.Map(double)(app.Of(21))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("maps type conversion", func(t *testing.T) {
@@ -410,18 +338,13 @@ func TestApplicativeMap(t *testing.T) {
toString := func(x int) string { return fmt.Sprintf("%d", x) }
result := app.Map(toString)(app.Of(42))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "42", value)
assert.Equal(t, Of("42"), result)
})
t.Run("maps identity function", func(t *testing.T) {
result := app.Map(F.Identity[int])(app.Of(42))
assert.True(t, either.IsRight(result))
assert.Equal(t, Of(42), result)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
@@ -458,12 +381,7 @@ func TestApplicativeMap(t *testing.T) {
app.Map(toString),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "30", value)
assert.Equal(t, Of("30"), result)
})
}
@@ -477,12 +395,7 @@ func TestApplicativeAp(t *testing.T) {
result := app.Ap(valueValidation)(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("accumulates errors when value fails", func(t *testing.T) {
@@ -549,12 +462,7 @@ func TestApplicativeAp(t *testing.T) {
result := app.Ap(valueValidation)(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "value:42", value)
assert.Equal(t, Of("value:42"), result)
})
t.Run("accumulates multiple errors from different sources", func(t *testing.T) {
@@ -596,12 +504,7 @@ func TestApplicativeComposition(t *testing.T) {
app.Map(double),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("composes multiple Map operations", func(t *testing.T) {
@@ -615,12 +518,7 @@ func TestApplicativeComposition(t *testing.T) {
app.Map(toString),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "42", value)
assert.Equal(t, Of("42"), result)
})
t.Run("composes Map and Ap", func(t *testing.T) {
@@ -636,12 +534,7 @@ func TestApplicativeComposition(t *testing.T) {
result := app.Ap(valueValidation)(ioFunc)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 21, value)
assert.Equal(t, Of(21), result)
})
}
@@ -709,12 +602,7 @@ func TestApplicativeMultipleArguments(t *testing.T) {
// Apply to second argument
result := app.Ap(app.Of(32))(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("applies curried three-argument function", func(t *testing.T) {
@@ -735,12 +623,7 @@ func TestApplicativeMultipleArguments(t *testing.T) {
funcValidation2 := Ap[func(int) int](app.Of(20))(funcValidation1)
result := Ap[int](app.Of(12))(funcValidation2)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("accumulates errors from multiple arguments", func(t *testing.T) {
@@ -774,12 +657,7 @@ func TestApplicativeWithDifferentTypes(t *testing.T) {
toString := func(x int) string { return fmt.Sprintf("%d", x) }
result := app.Map(toString)(app.Of(42))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "42", value)
assert.Equal(t, Of("42"), result)
})
t.Run("string to int", func(t *testing.T) {
@@ -787,12 +665,7 @@ func TestApplicativeWithDifferentTypes(t *testing.T) {
toLength := func(s string) int { return len(s) }
result := app.Map(toLength)(app.Of("hello"))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 5, value)
assert.Equal(t, Of(5), result)
})
t.Run("bool to string", func(t *testing.T) {
@@ -805,12 +678,7 @@ func TestApplicativeWithDifferentTypes(t *testing.T) {
}
result := app.Map(toString)(app.Of(true))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "true", value)
assert.Equal(t, Of("true"), result)
})
}
@@ -858,14 +726,8 @@ func TestApplicativeRealWorldScenario(t *testing.T) {
// Use the standalone Ap function with proper type parameters
result := Ap[User](email)(Ap[func(string) User](age)(Ap[func(int) func(string) User](name)(Of(makeUser))))
assert.True(t, either.IsRight(result))
user := either.MonadFold(result,
func(Errors) User { return User{} },
F.Identity[User],
)
assert.Equal(t, "Alice", user.Name)
assert.Equal(t, 25, user.Age)
assert.Equal(t, "alice@example.com", user.Email)
expectedUser := User{Name: "Alice", Age: 25, Email: "alice@example.com"}
assert.Equal(t, Of(expectedUser), result)
})
t.Run("accumulates all validation errors", func(t *testing.T) {
@@ -1432,12 +1294,7 @@ func TestMonadMap(t *testing.T) {
return fmt.Sprintf("Value: %d", x)
})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "Value: 42", value)
assert.Equal(t, Of("Value: 42"), result)
})
t.Run("computing derived values", func(t *testing.T) {
@@ -1451,12 +1308,7 @@ func TestMonadMap(t *testing.T) {
func(u User) string { return u.FirstName + " " + u.LastName },
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "John Doe", value)
assert.Equal(t, Of("John Doe"), result)
})
t.Run("chaining multiple MonadMap operations", func(t *testing.T) {
@@ -1464,12 +1316,7 @@ func TestMonadMap(t *testing.T) {
step2 := MonadMap(step1, N.Mul(2))
step3 := MonadMap(step2, func(x int) string { return fmt.Sprintf("%d", x) })
assert.True(t, either.IsRight(step3))
value := either.MonadFold(step3,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "30", value)
assert.Equal(t, Of("30"), step3)
})
t.Run("identity function", func(t *testing.T) {
@@ -1592,12 +1439,7 @@ func TestMonadAp(t *testing.T) {
toString := func(x int) string { return fmt.Sprintf("Value: %d", x) }
result := MonadAp(Of(toString), Of(42))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "Value: 42", value)
assert.Equal(t, Of("Value: 42"), result)
})
t.Run("curried function application", func(t *testing.T) {
@@ -1657,12 +1499,7 @@ func TestMonadChain(t *testing.T) {
},
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "Value: 42", value)
assert.Equal(t, Of("Value: 42"), result)
})
t.Run("short-circuits on first failure", func(t *testing.T) {
@@ -1725,12 +1562,7 @@ func TestMonadChain(t *testing.T) {
},
)
assert.True(t, either.IsRight(step3))
value := either.MonadFold(step3,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "30", value)
assert.Equal(t, Of("30"), step3)
})
t.Run("conditional validation", func(t *testing.T) {
@@ -1859,11 +1691,205 @@ func TestChain(t *testing.T) {
result := F.Pipe1(Of(42), toString)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "Value: 42", value)
assert.Equal(t, Of("Value: 42"), result)
})
}
func TestOrElse(t *testing.T) {
t.Run("OrElse is equivalent to ChainLeft - Success case", func(t *testing.T) {
handler := func(errs Errors) Validation[int] {
return Failures[int](Errors{
&ValidationError{Messsage: "should not be called"},
})
}
// Test with OrElse
resultOrElse := OrElse(handler)(Success(42))
// Test with ChainLeft
resultChainLeft := ChainLeft(handler)(Success(42))
assert.Equal(t, resultChainLeft, resultOrElse, "OrElse and ChainLeft should produce identical results for Success")
assert.Equal(t, Success(42), resultOrElse)
})
t.Run("OrElse is equivalent to ChainLeft - Failure recovery", func(t *testing.T) {
handler := func(errs Errors) Validation[int] {
if len(errs) > 0 && errs[0].Messsage == "not found" {
return Success(0)
}
return Failures[int](errs)
}
input := Failures[int](Errors{
&ValidationError{Messsage: "not found"},
})
// Test with OrElse
resultOrElse := OrElse(handler)(input)
// Test with ChainLeft
resultChainLeft := ChainLeft(handler)(input)
assert.Equal(t, resultChainLeft, resultOrElse, "OrElse and ChainLeft should produce identical results for recovery")
assert.Equal(t, Success(0), resultOrElse)
})
t.Run("OrElse is equivalent to ChainLeft - Error aggregation", func(t *testing.T) {
handler := func(errs Errors) Validation[string] {
return Failures[string](Errors{
&ValidationError{Messsage: "additional error"},
})
}
input := Failures[string](Errors{
&ValidationError{Messsage: "original error"},
})
// Test with OrElse
resultOrElse := OrElse(handler)(input)
// Test with ChainLeft
resultChainLeft := ChainLeft(handler)(input)
assert.Equal(t, resultChainLeft, resultOrElse, "OrElse and ChainLeft should produce identical results for error aggregation")
// Verify both aggregate errors
assert.True(t, either.IsLeft(resultOrElse))
errors := either.MonadFold(resultOrElse,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 2, "Should aggregate both errors")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "original error")
assert.Contains(t, messages, "additional error")
})
t.Run("OrElse in pipeline composition", func(t *testing.T) {
addContext := OrElse(func(errs Errors) Validation[int] {
return Failures[int](Errors{
&ValidationError{Messsage: "context added"},
})
})
recoverFromNotFound := OrElse(func(errs Errors) Validation[int] {
for _, err := range errs {
if err.Messsage == "not found" {
return Success(0)
}
}
return Failures[int](errs)
})
// Test error aggregation in pipeline
// When chaining OrElse operations, errors accumulate at each step
result1 := F.Pipe2(
Failures[int](Errors{
&ValidationError{Messsage: "database error"},
}),
addContext,
recoverFromNotFound,
)
assert.True(t, either.IsLeft(result1))
errors := either.MonadFold(result1,
F.Identity[Errors],
func(int) Errors { return nil },
)
// First OrElse adds "context added" to "database error" = 2 errors
// Second OrElse adds those 2 errors again (via recoverFromNotFound returning errs) = 4 total
assert.Len(t, errors, 4, "Should aggregate errors from pipeline (errors accumulate at each step)")
// Test recovery in pipeline
result2 := F.Pipe2(
Failures[int](Errors{
&ValidationError{Messsage: "not found"},
}),
addContext,
recoverFromNotFound,
)
assert.Equal(t, Success(0), result2, "Should recover from 'not found' error")
})
t.Run("OrElse semantic meaning - fallback validation", func(t *testing.T) {
// OrElse provides a semantic name for fallback/alternative validation
// It reads naturally: "try this validation, or else try this alternative"
validatePositive := func(x int) Validation[int] {
if x > 0 {
return Success(x)
}
return Failures[int](Errors{
&ValidationError{Messsage: "must be positive"},
})
}
// Use OrElse to provide a fallback: if validation fails, use default value
withDefault := OrElse(func(errs Errors) Validation[int] {
return Success(1) // default to 1 if validation fails
})
result := F.Pipe1(
validatePositive(-5),
withDefault,
)
assert.Equal(t, Success(1), result, "OrElse provides fallback value")
})
t.Run("OrElse vs ChainLeft - identical behavior verification", func(t *testing.T) {
// Create various test scenarios
scenarios := []struct {
name string
input Validation[int]
handler func(Errors) Validation[int]
}{
{
name: "Success value",
input: Success(42),
handler: func(errs Errors) Validation[int] {
return Failures[int](Errors{&ValidationError{Messsage: "error"}})
},
},
{
name: "Failure with recovery",
input: Failures[int](Errors{&ValidationError{Messsage: "error"}}),
handler: func(errs Errors) Validation[int] {
return Success(0)
},
},
{
name: "Failure with error transformation",
input: Failures[int](Errors{&ValidationError{Messsage: "error1"}}),
handler: func(errs Errors) Validation[int] {
return Failures[int](Errors{&ValidationError{Messsage: "error2"}})
},
},
{
name: "Multiple errors aggregation",
input: Failures[int](Errors{
&ValidationError{Messsage: "error1"},
&ValidationError{Messsage: "error2"},
}),
handler: func(errs Errors) Validation[int] {
return Failures[int](Errors{
&ValidationError{Messsage: "error3"},
&ValidationError{Messsage: "error4"},
})
},
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
resultOrElse := OrElse(scenario.handler)(scenario.input)
resultChainLeft := ChainLeft(scenario.handler)(scenario.input)
assert.Equal(t, resultChainLeft, resultOrElse,
"OrElse and ChainLeft must produce identical results for: %s", scenario.name)
})
}
})
}

View File

@@ -52,3 +52,177 @@ func ApplicativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
m,
)
}
// AlternativeMonoid creates a Monoid instance for Validation[A] using the Alternative pattern.
// This combines the applicative error-accumulation behavior with the alternative fallback behavior,
// allowing you to both accumulate errors and provide fallback alternatives.
//
// The Alternative pattern provides two key operations:
// - Applicative operations (Of, Map, Ap): accumulate errors when combining validations
// - Alternative operation (Alt): provide fallback when a validation fails
//
// This monoid is particularly useful when you want to:
// - Try multiple validation strategies and fall back to alternatives
// - Combine successful values using the provided monoid
// - Accumulate all errors from failed attempts
// - Build validation pipelines with fallback logic
//
// The resulting monoid:
// - Empty: Returns a successful validation with the empty value from the inner monoid
// - Concat: Combines two validations using both applicative and alternative semantics:
// - If first succeeds and second succeeds: combines values using inner monoid
// - If first fails: tries second as fallback (alternative behavior)
// - If both fail: accumulates all errors
//
// Type Parameters:
// - A: The type of the successful value
//
// Parameters:
// - m: The monoid for combining successful values of type A
//
// Returns:
//
// A Monoid[Validation[A]] that combines applicative and alternative behaviors
//
// Example - Combining successful validations:
//
// import "github.com/IBM/fp-go/v2/string"
//
// m := AlternativeMonoid(string.Monoid)
// v1 := Success("Hello")
// v2 := Success(" World")
// result := m.Concat(v1, v2)
// // Result: Success("Hello World")
//
// Example - Fallback behavior:
//
// m := AlternativeMonoid(string.Monoid)
// v1 := Failures[string](Errors{&ValidationError{Messsage: "first failed"}})
// v2 := Success("fallback value")
// result := m.Concat(v1, v2)
// // Result: Success("fallback value") - second validation used as fallback
//
// Example - Error accumulation when both fail:
//
// m := AlternativeMonoid(string.Monoid)
// v1 := Failures[string](Errors{&ValidationError{Messsage: "error 1"}})
// v2 := Failures[string](Errors{&ValidationError{Messsage: "error 2"}})
// result := m.Concat(v1, v2)
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
//
// Example - Building validation with fallbacks:
//
// import N "github.com/IBM/fp-go/v2/number"
//
// m := AlternativeMonoid(N.MonoidSum[int]())
//
// // Try to parse from different sources
// fromEnv := parseFromEnv() // Fails
// fromConfig := parseFromConfig() // Succeeds with 42
// fromDefault := Success(0) // Default fallback
//
// result := m.Concat(m.Concat(fromEnv, fromConfig), fromDefault)
// // Result: Success(42) - uses first successful validation
func AlternativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
return M.AlternativeMonoid(
Of[A],
MonadMap[A, func(A) A],
MonadAp[A, A],
MonadAlt[A],
m,
)
}
// AltMonoid creates a Monoid instance for Validation[A] using the Alt (alternative) operation.
// This monoid provides a way to combine validations with fallback behavior, where the second
// validation is used as an alternative if the first one fails.
//
// The Alt operation implements the "try first, fallback to second" pattern, which is useful
// for validation scenarios where you want to attempt multiple validation strategies in sequence
// and use the first one that succeeds.
//
// The resulting monoid:
// - Empty: Returns the provided zero value (a lazy computation that produces a Validation[A])
// - Concat: Combines two validations using Alt semantics:
// - If first succeeds: returns the first validation (ignores second)
// - If first fails: returns the second validation as fallback
//
// This is different from [AlternativeMonoid] in that:
// - AltMonoid uses a custom zero value (provided by the user)
// - AlternativeMonoid derives the zero from an inner monoid
// - AltMonoid is simpler and only provides fallback behavior
// - AlternativeMonoid combines applicative and alternative behaviors
//
// Type Parameters:
// - A: The type of the successful value
//
// Parameters:
// - zero: A lazy computation that produces the identity/empty Validation[A].
// This is typically a successful validation with a default value, or could be
// a failure representing "no validation attempted"
//
// Returns:
//
// A Monoid[Validation[A]] that combines validations with fallback behavior
//
// Example - Using default value as zero:
//
// m := AltMonoid(func() Validation[int] { return Success(0) })
//
// v1 := Failures[int](Errors{&ValidationError{Messsage: "failed"}})
// v2 := Success(42)
//
// result := m.Concat(v1, v2)
// // Result: Success(42) - falls back to second validation
//
// empty := m.Empty()
// // Result: Success(0) - the provided zero value
//
// Example - Chaining multiple fallbacks:
//
// m := AltMonoid(func() Validation[string] {
// return Success("default")
// })
//
// primary := parseFromPrimarySource() // Fails
// secondary := parseFromSecondary() // Fails
// tertiary := parseFromTertiary() // Succeeds with "value"
//
// result := m.Concat(m.Concat(primary, secondary), tertiary)
// // Result: Success("value") - uses first successful validation
//
// Example - All validations fail:
//
// m := AltMonoid(func() Validation[int] {
// return Failures[int](Errors{&ValidationError{Messsage: "no default"}})
// })
//
// v1 := Failures[int](Errors{&ValidationError{Messsage: "error 1"}})
// v2 := Failures[int](Errors{&ValidationError{Messsage: "error 2"}})
//
// result := m.Concat(v1, v2)
// // Result: Failures with errors from v2: ["error 2"]
// // Note: Unlike AlternativeMonoid, errors are NOT accumulated
//
// Example - Building a validation pipeline with fallbacks:
//
// m := AltMonoid(func() Validation[Config] {
// return Success(defaultConfig)
// })
//
// // Try multiple configuration sources in order
// configs := []Validation[Config]{
// loadFromFile("config.json"), // Try file first
// loadFromEnv(), // Then environment
// loadFromRemote("api.example.com"), // Then remote API
// }
//
// // Fold using the monoid to get first successful config
// result := A.MonoidFold(m)(configs)
// // Result: First successful config, or defaultConfig if all fail
func AltMonoid[A any](zero Lazy[Validation[A]]) Monoid[Validation[A]] {
return M.AltMonoid(
zero,
MonadAlt[A],
)
}

View File

@@ -74,12 +74,7 @@ func TestApplicativeMonoid(t *testing.T) {
t.Run("empty returns successful validation with empty string", func(t *testing.T) {
empty := m.Empty()
assert.True(t, either.IsRight(empty))
value := either.MonadFold(empty,
func(Errors) string { return "ERROR" },
F.Identity[string],
)
assert.Equal(t, "", value)
assert.Equal(t, Success(""), empty)
})
t.Run("concat combines successful validations", func(t *testing.T) {
@@ -88,12 +83,7 @@ func TestApplicativeMonoid(t *testing.T) {
result := m.Concat(v1, v2)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "Hello World", value)
assert.Equal(t, Success("Hello World"), result)
})
t.Run("concat with failure returns failure", func(t *testing.T) {
@@ -141,17 +131,8 @@ func TestApplicativeMonoid(t *testing.T) {
result1 := m.Concat(v, empty)
result2 := m.Concat(empty, v)
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
assert.Equal(t, Of("test"), result1)
assert.Equal(t, Of("test"), result2)
})
})
@@ -166,11 +147,7 @@ func TestApplicativeMonoid(t *testing.T) {
t.Run("empty returns zero", func(t *testing.T) {
empty := m.Empty()
value := either.MonadFold(empty,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
assert.Equal(t, Of(0), empty)
})
t.Run("concat adds values", func(t *testing.T) {
@@ -179,11 +156,7 @@ func TestApplicativeMonoid(t *testing.T) {
result := m.Concat(v1, v2)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("multiple concat operations", func(t *testing.T) {
@@ -194,11 +167,7 @@ func TestApplicativeMonoid(t *testing.T) {
result := m.Concat(m.Concat(m.Concat(v1, v2), v3), v4)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
assert.Equal(t, Of(10), result)
})
})
}
@@ -245,21 +214,13 @@ func TestMonoidLaws(t *testing.T) {
t.Run("left identity", func(t *testing.T) {
// empty + a = a
result := m.Concat(m.Empty(), v1)
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
assert.Equal(t, Of("a"), result)
})
t.Run("right identity", func(t *testing.T) {
// a + empty = a
result := m.Concat(v1, m.Empty())
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
assert.Equal(t, Of("a"), result)
})
t.Run("associativity", func(t *testing.T) {
@@ -268,17 +229,8 @@ func TestMonoidLaws(t *testing.T) {
left := m.Concat(m.Concat(v1, v2), v3)
right := m.Concat(v1, m.Concat(v2, v3))
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
assert.Equal(t, Of("abc"), left)
assert.Equal(t, Of("abc"), right)
})
})
}
@@ -332,11 +284,7 @@ func TestApplicativeMonoidEdgeCases(t *testing.T) {
result := m.Concat(v1, v2)
value := either.MonadFold(result,
func(Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, 15, value.Count)
assert.Equal(t, Of(Counter{Count: 15}), result)
})
t.Run("empty concat empty", func(t *testing.T) {
@@ -344,10 +292,6 @@ func TestApplicativeMonoidEdgeCases(t *testing.T) {
result := m.Concat(m.Empty(), m.Empty())
value := either.MonadFold(result,
func(Errors) string { return "ERROR" },
F.Identity[string],
)
assert.Equal(t, "", value)
assert.Equal(t, Of(""), result)
})
}

View File

@@ -18,6 +18,7 @@ package validation
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
@@ -257,4 +258,6 @@ type (
// double := func(x int) int { return x * 2 } // Endomorphism[int]
// result := LetL(lens, double)(Success(21)) // Success(42)
Endomorphism[A any] = endomorphism.Endomorphism[A]
Lazy[A any] = lazy.Lazy[A]
)

View File

@@ -186,12 +186,7 @@ func TestSuccess(t *testing.T) {
t.Run("creates right either with value", func(t *testing.T) {
result := Success(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Success(42), result)
})
t.Run("works with different types", func(t *testing.T) {
@@ -327,7 +322,7 @@ func TestValidationIntegration(t *testing.T) {
&ValidationError{Value: "bad", Messsage: "error"},
})
assert.True(t, either.IsRight(success))
assert.Equal(t, Success(42), success)
assert.True(t, either.IsLeft(failure))
})
@@ -512,12 +507,7 @@ func TestToResult(t *testing.T) {
result := ToResult(validation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(error) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, either.Of[error](42), result)
})
t.Run("converts failed validation to result with error", func(t *testing.T) {
@@ -569,23 +559,18 @@ func TestToResult(t *testing.T) {
// String type
strValidation := Success("hello")
strResult := ToResult(strValidation)
assert.True(t, either.IsRight(strResult))
assert.Equal(t, either.Of[error]("hello"), strResult)
// Bool type
boolValidation := Success(true)
boolResult := ToResult(boolValidation)
assert.True(t, either.IsRight(boolResult))
assert.Equal(t, either.Of[error](true), boolResult)
// Struct type
type User struct{ Name string }
userValidation := Success(User{Name: "Alice"})
userResult := ToResult(userValidation)
assert.True(t, either.IsRight(userResult))
user := either.MonadFold(userResult,
func(error) User { return User{} },
F.Identity[User],
)
assert.Equal(t, "Alice", user.Name)
assert.Equal(t, either.Of[error](User{Name: "Alice"}), userResult)
})
t.Run("preserves error context in result", func(t *testing.T) {

View File

@@ -1104,6 +1104,8 @@ func After[R, E, A any](timestamp time.Time) Operator[R, E, A, A] {
// If the ReaderIOEither is Left, it applies the provided function to the error value,
// which returns a new ReaderIOEither that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative IO computations
// that need access to configuration or dependencies. The error type can be widened from E1 to E2.
//

View File

@@ -531,3 +531,205 @@ func TestReadIO(t *testing.T) {
assert.Equal(t, E.Right[error](25), result)
})
}
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
// This test verifies that both functions produce the same results for all scenarios with reader context:
// - Left values with error recovery using reader context
// - Left values with error transformation
// - Right values passing through unchanged
// - Error type widening
func TestChainLeftIdenticalToOrElse(t *testing.T) {
type Config struct {
fallbackValue int
retryEnabled bool
}
// Test 1: Left value with error recovery using reader context
t.Run("Left value recovery with reader - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
if e == "recoverable" {
return func(cfg Config) IOE.IOEither[string, int] {
return IOE.Right[string](cfg.fallbackValue)
}
}
return Left[Config, int](e)
}
input := Left[Config, int]("recoverable")
cfg := Config{fallbackValue: 42, retryEnabled: true}
// Using ChainLeft
resultChainLeft := ChainLeft(recoveryFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(recoveryFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](42), resultChainLeft)
})
// Test 2: Left value with error transformation
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
transformFn := func(e string) ReaderIOEither[Config, string, int] {
return Left[Config, int]("transformed: " + e)
}
input := Left[Config, int]("original error")
cfg := Config{fallbackValue: 0, retryEnabled: false}
// Using ChainLeft
resultChainLeft := ChainLeft(transformFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(transformFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
})
// Test 3: Right value - both should pass through unchanged
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
handlerFn := func(e string) ReaderIOEither[Config, string, int] {
return Left[Config, int]("should not be called")
}
input := Right[Config, string](100)
cfg := Config{fallbackValue: 0, retryEnabled: false}
// Using ChainLeft
resultChainLeft := ChainLeft(handlerFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(handlerFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](100), resultChainLeft)
})
// Test 4: Error type widening
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
widenFn := func(e string) ReaderIOEither[Config, int, int] {
return Left[Config, int](404)
}
input := Left[Config, int]("not found")
cfg := Config{fallbackValue: 0, retryEnabled: false}
// Using ChainLeft
resultChainLeft := ChainLeft(widenFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(widenFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int](404), resultChainLeft)
})
// Test 5: Composition in pipeline with reader context
t.Run("Pipeline composition with reader - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
if e == "network error" {
return func(cfg Config) IOE.IOEither[string, int] {
if cfg.retryEnabled {
return IOE.Right[string](cfg.fallbackValue)
}
return IOE.Left[int]("retry disabled")
}
}
return Left[Config, int](e)
}
input := Left[Config, int]("network error")
cfg := Config{fallbackValue: 99, retryEnabled: true}
// Using ChainLeft in pipeline
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))(cfg)()
// Using OrElse in pipeline
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](99), resultChainLeft)
})
// Test 6: Multiple chained operations with reader context
t.Run("Multiple operations with reader - ChainLeft equals OrElse", func(t *testing.T) {
handler1 := func(e string) ReaderIOEither[Config, string, int] {
if e == "error1" {
return Right[Config, string](1)
}
return Left[Config, int](e)
}
handler2 := func(e string) ReaderIOEither[Config, string, int] {
if e == "error2" {
return func(cfg Config) IOE.IOEither[string, int] {
return IOE.Right[string](cfg.fallbackValue)
}
}
return Left[Config, int](e)
}
input := Left[Config, int]("error2")
cfg := Config{fallbackValue: 2, retryEnabled: false}
// Using ChainLeft
resultChainLeft := F.Pipe2(
input,
ChainLeft(handler1),
ChainLeft(handler2),
)(cfg)()
// Using OrElse
resultOrElse := F.Pipe2(
input,
OrElse(handler1),
OrElse(handler2),
)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](2), resultChainLeft)
})
// Test 7: Reader context is properly threaded through both functions
t.Run("Reader context threading - ChainLeft equals OrElse", func(t *testing.T) {
var chainLeftCfg, orElseCfg *Config
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
return func(cfg Config) IOE.IOEither[string, int] {
// Capture the config to verify it's passed correctly
if chainLeftCfg == nil {
chainLeftCfg = &cfg
} else {
orElseCfg = &cfg
}
return IOE.Right[string](cfg.fallbackValue)
}
}
input := Left[Config, int]("error")
cfg := Config{fallbackValue: 123, retryEnabled: true}
// Using ChainLeft
resultChainLeft := ChainLeft(recoveryFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(recoveryFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](123), resultChainLeft)
// Verify both received the same config
assert.NotNil(t, chainLeftCfg)
assert.NotNil(t, orElseCfg)
assert.Equal(t, *chainLeftCfg, *orElseCfg)
assert.Equal(t, cfg, *chainLeftCfg)
})
}

View File

@@ -475,6 +475,8 @@ func Alt[A any](that Lazy[Result[A]]) Operator[A, A] {
// If the Result is Left, it applies the provided function to the error value,
// which returns a new Result that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative computations.
//
// Example:
@@ -667,6 +669,8 @@ func InstanceOf[A any](a any) Result[A] {
// - Error transformation: changing error types or adding context
// - Fallback logic: providing alternative computations when errors occur
//
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// The function parameter receives the error value and must return a new Result[A].
// This allows you to:
// - Recover by returning Right[error](value)
@@ -711,6 +715,8 @@ func MonadChainLeft[A any](fa Result[A], f Kleisli[error, A]) Result[A] {
// ChainLeft is the curried version of [MonadChainLeft].
// Returns a function that transforms Left (error) values while preserving Right values.
//
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This curried form is particularly useful in functional pipelines and for creating
// reusable error handlers that can be composed with other operations.
//