1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-04 13:21:02 +02:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
94b1ea30d1 fix: improved doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-02 13:23:59 +01:00
Dr. Carsten Leue
a77d61f632 fix: add Bind for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-02 13:01:21 +01:00
renovate[bot]
66b2f57d73 fix(deps): update module github.com/urfave/cli/v3 to v3.7.0 (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 08:40:21 +00:00
4 changed files with 794 additions and 1 deletions

View File

@@ -4,7 +4,7 @@ go 1.24
require (
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.2
github.com/urfave/cli/v3 v3.7.0
)
require (

View File

@@ -6,6 +6,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -26,6 +26,83 @@ import (
"github.com/IBM/fp-go/v2/semigroup"
)
// Do creates the initial empty codec to be used as the starting point for
// do-notation style codec construction.
//
// This is the entry point for building up a struct codec field-by-field using
// the applicative and monadic sequencing operators ApSL, ApSO, and Bind.
// It wraps Empty and lifts a lazily-evaluated default Pair[O, A] into a
// Type[A, O, I] that ignores its input and always succeeds with the default value.
//
// # Type Parameters
//
// - I: The input type for decoding (what the codec reads from)
// - A: The target struct type being built up (what the codec decodes to)
// - O: The output type for encoding (what the codec writes to)
//
// # Parameters
//
// - e: A Lazy[Pair[O, A]] providing the initial default values:
// - pair.Head(e()): The default encoded output O (e.g. an empty monoid value)
// - pair.Tail(e()): The initial zero value of the struct A (e.g. MyStruct{})
//
// # Returns
//
// - A Type[A, O, I] that always decodes to the default A and encodes to the
// default O, regardless of input. This is then transformed by chaining
// ApSL, ApSO, or Bind operators to add fields one by one.
//
// # Example Usage
//
// Building a struct codec using do-notation style:
//
// import (
// "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/lazy"
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/lens"
// "github.com/IBM/fp-go/v2/pair"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Age int
// }
//
// nameLens := lens.MakeLens(
// func(p Person) string { return p.Name },
// func(p Person, name string) Person { p.Name = name; return p },
// )
// ageLens := lens.MakeLens(
// func(p Person) int { return p.Age },
// func(p Person, age int) Person { p.Age = age; return p },
// )
//
// personCodec := F.Pipe2(
// codec.Do[any, Person, string](lazy.Of(pair.MakePair("", Person{}))),
// codec.ApSL(S.Monoid, nameLens, codec.String()),
// codec.ApSL(S.Monoid, ageLens, codec.Int()),
// )
//
// # Notes
//
// - Do is typically the first call in a codec pipeline, followed by ApSL, ApSO, or Bind
// - The lazy pair should use the monoid's empty value for O and the zero value for A
// - For convenience, use Struct to create the initial codec for named struct types
//
// # See Also
//
// - Empty: The underlying codec constructor that Do delegates to
// - ApSL: Applicative sequencing for required struct fields via Lens
// - ApSO: Applicative sequencing for optional struct fields via Optional
// - Bind: Monadic sequencing for context-dependent field codecs
//
//go:inline
func Do[I, A, O any](e Lazy[Pair[O, A]]) Type[A, O, I] {
return Empty[I](e)
}
// ApSL creates an applicative sequencing operator for codecs using a lens.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs,
@@ -297,3 +374,132 @@ func ApSO[S, T, O, I any](
)
}
}
// Bind creates a monadic sequencing operator for codecs using a lens and a Kleisli arrow.
//
// This function implements the "Bind" (monadic bind / chain) pattern for codecs,
// allowing you to build up complex codecs where the codec for a field depends on
// the current decoded value of the struct. Unlike ApSL which uses a fixed field
// codec, Bind accepts a Kleisli arrow — a function from the current struct value S
// to a Type[T, O, I] — enabling context-sensitive codec construction.
//
// The function combines:
// - Encoding: Evaluates the Kleisli arrow f on the current struct value s to obtain
// the field codec, extracts the field T using the lens, encodes it with that codec,
// and combines it with the base encoding using the monoid.
// - Validation: Validates the base struct first (monadic sequencing), then uses the
// Kleisli arrow to obtain the field codec for the decoded struct value, and validates
// the field through the lens. Errors are propagated but NOT accumulated (fail-fast
// semantics, unlike ApSL which accumulates errors).
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The field type accessed by the lens
// - O: The output type for encoding (must have a monoid)
// - I: The input type for decoding
//
// # Parameters
//
// - m: A Monoid[O] for combining encoded outputs
// - l: A Lens[S, T] that focuses on a specific field in S
// - f: A Kleisli[S, T, O, I] — a function from S to Type[T, O, I] — that produces
// the field codec based on the current struct value
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the field
// specified by the lens, where the field codec is determined by the Kleisli arrow.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Evaluate f(s) to obtain the field codec fa
// - Extract the field T using l.Get
// - Encode T to O using fa.Encode
// - Combine with the base encoding using the monoid
//
// 2. **Validation**: When validating input I:
// - Run the base validation to obtain a decoded S (fail-fast: stop on base failure)
// - For the decoded S, evaluate f(s) to obtain the field codec fa
// - Validate the input I using fa.Validate
// - Set the validated T into S using l.Set
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Difference from ApSL
//
// Unlike ApSL which uses a fixed field codec:
// - ApSL: Field codec is fixed at construction time; errors are accumulated
// - Bind: Field codec depends on the current struct value (Kleisli arrow); validation
// uses monadic sequencing (fail-fast on base failure)
// - Bind is more powerful but less parallel than ApSL
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/lens"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Config struct {
// Mode string
// Value int
// }
//
// modeLens := lens.MakeLens(
// func(c Config) string { return c.Mode },
// func(c Config, mode string) Config { c.Mode = mode; return c },
// )
//
// // Build a Config codec where the Value codec depends on the Mode
// configCodec := F.Pipe1(
// codec.Struct[Config]("Config"),
// codec.Bind(S.Monoid, modeLens, func(c Config) codec.Type[string, string, any] {
// return codec.String()
// }),
// )
//
// # Use Cases
//
// - Building codecs where a field's codec depends on another field's value
// - Implementing discriminated unions or tagged variants
// - Context-sensitive validation (e.g., validate field B differently based on field A)
// - Dependent type-like patterns in codec construction
//
// # Notes
//
// - The monoid determines how encoded outputs are combined
// - The lens must be total (handle all cases safely)
// - Validation uses monadic (fail-fast) sequencing: if the base codec fails,
// the Kleisli arrow is never evaluated
// - The name is automatically generated for debugging purposes
//
// See also:
// - ApSL: Applicative sequencing with a fixed lens codec (error accumulation)
// - Kleisli: The function type from S to Type[T, O, I]
// - decode.Bind: The underlying decode-level bind combinator
func Bind[S, T, O, I any](
m Monoid[O],
l Lens[S, T],
f Kleisli[S, T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("Bind[%s]", l)
val := F.Curry2(Type[T, O, I].Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
validate.Bind(l.Set, F.Flow2(f, val)),
),
func(s S) O {
return m.Concat(t.Encode(s), f(s).Encode(l.Get(s)))
},
)
}
}

View File

@@ -814,3 +814,588 @@ func TestApSO_ErrorAccumulation(t *testing.T) {
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
// TestBind_EncodingCombination verifies that Bind combines the base encoding with
// the field encoding produced by the Kleisli arrow using the monoid.
func TestBind_EncodingCombination(t *testing.T) {
t.Run("combines base and field encodings using monoid", func(t *testing.T) {
// Lens for Person.Name
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Base codec encodes to "Person:"
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "Person:" },
)
// Kleisli arrow: always returns a string identity codec regardless of struct value
kleisli := func(p Person) Type[string, string, any] {
return MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected string"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
}
operator := Bind(S.Monoid, nameLens, kleisli)
enhancedCodec := operator(baseCodec)
person := Person{Name: "Alice", Age: 30}
encoded := enhancedCodec.Encode(person)
// Encoding should include both the base prefix and the field value
assert.Contains(t, encoded, "Person:")
assert.Contains(t, encoded, "Alice")
})
}
// TestBind_KleisliArrowReceivesCurrentValue verifies that the Kleisli arrow f
// receives the current struct value when producing the field codec.
func TestBind_KleisliArrowReceivesCurrentValue(t *testing.T) {
t.Run("kleisli arrow receives current struct value during encoding", func(t *testing.T) {
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Kleisli arrow that uses the struct value to produce a prefix in the encoding
var capturedPerson Person
kleisli := func(p Person) Type[string, string, any] {
capturedPerson = p
return MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected string"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
}
operator := Bind(S.Monoid, nameLens, kleisli)
enhancedCodec := operator(baseCodec)
person := Person{Name: "Bob", Age: 25}
enhancedCodec.Encode(person)
// The Kleisli arrow should have been called with the actual struct value
assert.Equal(t, person, capturedPerson)
})
}
// TestBind_ValidationSuccess verifies that Bind correctly validates and decodes
// a struct when both the base and field validations succeed.
func TestBind_ValidationSuccess(t *testing.T) {
t.Run("succeeds when base and field validations pass", func(t *testing.T) {
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// The field codec receives the same input I (any = Person struct).
// It must extract the Name field from the Person input.
kleisli := func(p Person) Type[string, string, any] {
return MakeType(
"Name",
func(i any) validation.Result[string] {
if person, ok := i.(Person); ok {
return validation.ToResult(validation.Success(person.Name))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if person, ok := i.(Person); ok {
return validation.Success(person.Name)
}
return validation.FailureWithMessage[string](i, "expected Person")(ctx)
}
},
F.Identity[string],
)
}
operator := Bind(S.Monoid, nameLens, kleisli)
enhancedCodec := operator(baseCodec)
person := Person{Name: "Carol", Age: 28}
result := enhancedCodec.Decode(person)
assert.True(t, either.IsRight(result), "Should succeed when both validations pass")
})
}
// TestBind_ValidationFailsOnBaseFailure verifies that Bind uses fail-fast (monadic)
// semantics: if the base codec fails, the Kleisli arrow is never evaluated.
func TestBind_ValidationFailsOnBaseFailure(t *testing.T) {
t.Run("fails fast when base validation fails", func(t *testing.T) {
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Base codec always fails
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "base always fails"},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
return validation.FailureWithMessage[Person](i, "base always fails")(ctx)
}
},
func(p Person) string { return "" },
)
kleisliCalled := false
kleisli := func(p Person) Type[string, string, any] {
kleisliCalled = true
return MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected string"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
}
operator := Bind(S.Monoid, nameLens, kleisli)
enhancedCodec := operator(baseCodec)
person := Person{Name: "Dave", Age: 40}
result := enhancedCodec.Decode(person)
assert.True(t, either.IsLeft(result), "Should fail when base validation fails")
assert.False(t, kleisliCalled, "Kleisli arrow should NOT be called when base fails")
})
}
// TestBind_ValidationFailsOnFieldFailure verifies that Bind propagates field
// validation errors when the Kleisli arrow's codec fails.
func TestBind_ValidationFailsOnFieldFailure(t *testing.T) {
t.Run("fails when field validation from kleisli codec fails", func(t *testing.T) {
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Base codec succeeds
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Kleisli arrow returns a codec that always fails regardless of input
kleisli := func(p Person) Type[string, string, any] {
return MakeType(
"Name",
func(i any) validation.Result[string] {
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "field always fails"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
return validation.FailureWithMessage[string](i, "field always fails")(ctx)
}
},
F.Identity[string],
)
}
operator := Bind(S.Monoid, nameLens, kleisli)
enhancedCodec := operator(baseCodec)
// The field codec receives the same input (Person) and always fails
person := Person{Name: "Eve", Age: 22}
result := enhancedCodec.Decode(person)
assert.True(t, either.IsLeft(result), "Should fail when field validation fails")
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(Person) validation.Errors { return nil },
)
assert.NotEmpty(t, errors, "Should have validation errors from field codec")
})
}
// TestBind_TypeCheckingPreserved verifies that Bind preserves the base type checker.
func TestBind_TypeCheckingPreserved(t *testing.T) {
t.Run("preserves base type checker", func(t *testing.T) {
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
kleisli := func(p Person) Type[string, string, any] {
return MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected string"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
}
operator := Bind(S.Monoid, nameLens, kleisli)
enhancedCodec := operator(baseCodec)
// Valid type
person := Person{Name: "Frank", Age: 35}
isResult := enhancedCodec.Is(person)
assert.True(t, either.IsRight(isResult), "Should accept Person type")
// Invalid type
invalidResult := enhancedCodec.Is("not a person")
assert.True(t, either.IsLeft(invalidResult), "Should reject non-Person type")
})
}
// TestBind_Naming verifies that Bind generates a descriptive name for the codec.
func TestBind_Naming(t *testing.T) {
t.Run("generates descriptive name containing Bind and lens info", func(t *testing.T) {
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
kleisli := func(p Person) Type[string, string, any] {
return MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected string"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
}
operator := Bind(S.Monoid, nameLens, kleisli)
enhancedCodec := operator(baseCodec)
name := enhancedCodec.Name()
assert.Contains(t, name, "Bind", "Name should contain 'Bind'")
})
}
// TestBind_DependentFieldCodec verifies that the Kleisli arrow can produce
// different codecs based on the current struct value (the key differentiator
// from ApSL).
//
// The field codec Type[T, O, I] receives the same input I as the base codec.
// It must extract the field value from that input. The Kleisli arrow f(s)
// produces a different codec depending on the already-decoded struct value s.
func TestBind_DependentFieldCodec(t *testing.T) {
t.Run("kleisli arrow produces different codecs based on struct value", func(t *testing.T) {
// Lens for Person.Name
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Base codec succeeds for any Person
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Kleisli arrow: the field codec receives the same input I (any = Person).
// It extracts the Name from the Person input.
// If the decoded struct's Age > 18, accept any name (including empty).
// If Age <= 18, reject empty names.
kleisli := func(p Person) Type[string, string, any] {
if p.Age > 18 {
// Adult: accept any name extracted from the Person input
return MakeType(
"AnyName",
func(i any) validation.Result[string] {
if person, ok := i.(Person); ok {
return validation.ToResult(validation.Success(person.Name))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if person, ok := i.(Person); ok {
return validation.Success(person.Name)
}
return validation.FailureWithMessage[string](i, "expected Person")(ctx)
}
},
F.Identity[string],
)
}
// Minor: reject empty names
return MakeType(
"NonEmptyName",
func(i any) validation.Result[string] {
if person, ok := i.(Person); ok {
if person.Name != "" {
return validation.ToResult(validation.Success(person.Name))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: person.Name, Messsage: "name must not be empty for minors"},
}))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{Value: i, Messsage: "expected Person"},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if person, ok := i.(Person); ok {
if person.Name != "" {
return validation.Success(person.Name)
}
return validation.FailureWithMessage[string](person.Name, "name must not be empty for minors")(ctx)
}
return validation.FailureWithMessage[string](i, "expected Person")(ctx)
}
},
F.Identity[string],
)
}
operator := Bind(S.Monoid, nameLens, kleisli)
enhancedCodec := operator(baseCodec)
// Adult (Age=30) with empty name: should succeed (adult codec accepts any name)
adultPerson := Person{Name: "", Age: 30}
adultResult := enhancedCodec.Decode(adultPerson)
assert.True(t, either.IsRight(adultResult), "Adult should accept empty name")
// Minor (Age=15) with empty name: should fail (minor codec rejects empty names)
minorPerson := Person{Name: "", Age: 15}
minorResult := enhancedCodec.Decode(minorPerson)
assert.True(t, either.IsLeft(minorResult), "Minor with empty name should fail")
// Minor (Age=15) with non-empty name: should succeed
minorWithName := Person{Name: "Junior", Age: 15}
minorWithNameResult := enhancedCodec.Decode(minorWithName)
assert.True(t, either.IsRight(minorWithNameResult), "Minor with non-empty name should succeed")
})
}