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

Compare commits

...

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
77a8cc6b09 fix: implement ApSO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:44:27 +01:00
Dr. Carsten Leue
bc8743fdfc fix: build error
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:21:37 +01:00
Dr. Carsten Leue
1837d3f86d fix: add semigroup helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 16:39:05 +01:00
19 changed files with 1583 additions and 48 deletions

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -388,8 +387,8 @@ func generateApplyHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -266,8 +265,8 @@ func generateBindHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -189,8 +188,8 @@ func generateDIHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -148,8 +147,8 @@ func generateEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -18,7 +18,6 @@ package cli
import (
"fmt"
"os"
"time"
)
func writePackage(f *os.File, pkg string) {
@@ -26,6 +25,6 @@ func writePackage(f *os.File, pkg string) {
fmt.Fprintf(f, "package %s\n\n", pkg)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
}

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -62,8 +61,8 @@ func generateIdentityHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -71,8 +70,8 @@ func generateIOHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -219,8 +218,8 @@ func generateIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -234,8 +233,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -76,8 +75,8 @@ func generateIOOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -148,8 +147,8 @@ func generateOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -378,8 +377,8 @@ func generatePipeHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -118,8 +117,8 @@ func generateReaderHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -131,8 +130,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -233,8 +232,8 @@ func generateReaderIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -246,8 +245,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -22,7 +22,6 @@ import (
"os"
"path/filepath"
"strings"
"time"
C "github.com/urfave/cli/v3"
)
@@ -399,8 +398,8 @@ func generateTupleHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

299
v2/optics/codec/bind.go Normal file
View File

@@ -0,0 +1,299 @@
// 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/optics/codec/validate"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/semigroup"
)
// ApSL creates an applicative sequencing operator for codecs using a lens.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs,
// allowing you to build up complex codecs by combining a base codec with a field
// accessed through a lens. It's particularly useful for building struct codecs
// field-by-field in a composable way.
//
// The function combines:
// - Encoding: Extracts the field value using the lens, encodes it with fa, and
// combines it with the base encoding using the monoid
// - Validation: Validates the field using the lens and combines the validation
// with the base validation
//
// # 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
// - fa: A Type[T, O, I] codec for the field type T
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the field
// specified by the lens.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - 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:
// - Validate the field using fa.Validate through the lens
// - Combine with the base validation
//
// 3. **Type Checking**: Preserves the base type checker
//
// # 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 Person struct {
// Name string
// Age int
// }
//
// // Lenses for Person fields
// nameLens := lens.MakeLens(
// func(p *Person) string { return p.Name },
// func(p *Person, name string) *Person { p.Name = name; return p },
// )
//
// // Build a Person codec field by field
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSL(S.Monoid, nameLens, codec.String),
// // ... add more fields
// )
//
// # Use Cases
//
// - Building struct codecs incrementally
// - Composing codecs for nested structures
// - Creating type-safe serialization/deserialization
// - Implementing Do-notation style codec construction
//
// # Notes
//
// - The monoid determines how encoded outputs are combined
// - The lens must be total (handle all cases safely)
// - This is typically used with other ApS functions to build complete codecs
// - The name is automatically generated for debugging purposes
//
// See also:
// - validate.ApSL: The underlying validation combinator
// - reader.ApplicativeMonoid: The monoid-based applicative instance
// - Lens: The optic for accessing struct fields
func ApSL[S, T, O, I any](
m Monoid[O],
l Lens[S, T],
fa Type[T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("ApS[%s x %s]", l, fa)
rm := reader.ApplicativeMonoid[S](m)
encConcat := F.Pipe1(
F.Flow2(
l.Get,
fa.Encode,
),
semigroup.AppendTo(rm),
)
valConcat := validate.ApSL(l, fa.Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
valConcat,
),
encConcat(t.Encode),
)
}
}
// ApSO creates an applicative sequencing operator for codecs using an optional.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs
// with optional fields, allowing you to build up complex codecs by combining a base
// codec with a field that may or may not be present. It's particularly useful for
// building struct codecs with optional fields in a composable way.
//
// The function combines:
// - Encoding: Attempts to extract the optional field value, encodes it if present,
// and combines it with the base encoding using the monoid. If the field is absent,
// only the base encoding is used.
// - Validation: Validates the optional field and combines the validation with the
// base validation using applicative semantics (error accumulation).
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The optional field type accessed by the optional
// - 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
// - o: An Optional[S, T] that focuses on a field in S that may not exist
// - fa: A Type[T, O, I] codec for the optional field type T
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the optional field
// specified by the optional.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Try to extract the optional field T using o.GetOption
// - If present (Some(T)): Encode T to O using fa.Encode and combine with base using monoid
// - If absent (None): Return only the base encoding unchanged
//
// 2. **Validation**: When validating input I:
// - Validate the optional field using fa.Validate through o.Set
// - Combine with the base validation using applicative semantics
// - Accumulates all validation errors from both base and field
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Difference from ApSL
//
// Unlike ApSL which works with required fields via Lens, ApSO handles optional fields:
// - ApSL: Field always exists, always encoded
// - ApSO: Field may not exist, only encoded when present
// - ApSO uses Optional.GetOption which returns Option[T]
// - ApSO gracefully handles missing fields without errors
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/optional"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Nickname *string // Optional field
// }
//
// // Optional for Person.Nickname
// nicknameOpt := optional.MakeOptional(
// func(p Person) option.Option[string] {
// if p.Nickname != nil {
// return option.Some(*p.Nickname)
// }
// return option.None[string]()
// },
// func(p Person, nick string) Person {
// p.Nickname = &nick
// return p
// },
// )
//
// // Build a Person codec with optional nickname
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
// )
//
// // Encoding with nickname present
// p1 := Person{Name: "Alice", Nickname: ptr("Ali")}
// encoded1 := personCodec.Encode(p1) // Includes nickname
//
// // Encoding with nickname absent
// p2 := Person{Name: "Bob", Nickname: nil}
// encoded2 := personCodec.Encode(p2) // No nickname in output
//
// # Use Cases
//
// - Building struct codecs with optional/nullable fields
// - Handling pointer fields that may be nil
// - Composing codecs for structures with optional nested data
// - Creating flexible serialization that omits absent fields
//
// # Notes
//
// - The monoid determines how encoded outputs are combined when field is present
// - When the optional field is absent, encoding returns base encoding unchanged
// - Validation still accumulates errors even for optional fields
// - The name is automatically generated for debugging purposes
//
// # See Also
//
// - ApSL: For required fields using Lens
// - validate.ApS: The underlying validation combinator
// - Optional: The optic for accessing optional fields
func ApSO[S, T, O, I any](
m Monoid[O],
o Optional[S, T],
fa Type[T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("ApS[%s x %s]", o, fa)
encConcat := F.Flow2(
o.GetOption,
option.Map(F.Flow2(
fa.Encode,
semigroup.AppendTo(m),
)),
)
valConcat := validate.ApS(o.Set, fa.Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
valConcat,
),
func(s S) O {
to := t.Encode(s)
return F.Pipe2(
encConcat(s),
option.Flap[O](to),
option.GetOrElse(lazy.Of(to)),
)
},
)
}
}

View File

@@ -0,0 +1,818 @@
// 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 (
"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/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// Test types for ApSL
type Person struct {
Name string
Age int
}
func TestApSL_EncodingCombination(t *testing.T) {
t.Run("combines encodings using monoid", func(t *testing.T) {
// Create a 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}
},
)
// Create base codec that 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:" },
)
// Create field codec for Name
nameCodec := 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],
)
// Apply ApSL to combine encodings
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding - should concatenate base encoding with field encoding
person := Person{Name: "Alice", Age: 30}
encoded := enhancedCodec.Encode(person)
// The monoid concatenates: base encoding + field encoding
// Note: The order depends on how the monoid is applied in ApSL
assert.Contains(t, encoded, "Person:")
assert.Contains(t, encoded, "Alice")
})
}
func TestApSL_ValidationCombination(t *testing.T) {
t.Run("validates field through lens", func(t *testing.T) {
// Create a lens for Person.Age
ageLens := lens.MakeLens(
func(p Person) int { return p.Age },
func(p Person, age int) Person {
return Person{Name: p.Name, Age: age}
},
)
// Create base codec that always 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 "" },
)
// Create field codec for Age that validates positive numbers
ageCodec := MakeType(
"Age",
func(i any) validation.Result[int] {
if n, ok := i.(int); ok {
if n > 0 {
return validation.ToResult(validation.Success(n))
}
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: n,
Messsage: "age must be positive",
},
}))
}
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected int",
},
}))
},
func(i any) Decode[Context, int] {
return func(ctx Context) validation.Validation[int] {
if n, ok := i.(int); ok {
if n > 0 {
return validation.Success(n)
}
return validation.FailureWithMessage[int](n, "age must be positive")(ctx)
}
return validation.FailureWithMessage[int](i, "expected int")(ctx)
}
},
strconv.Itoa,
)
// Apply ApSL
operator := ApSL(S.Monoid, ageLens, ageCodec)
enhancedCodec := operator(baseCodec)
// Test with invalid age (negative) - field validation should fail
invalidPerson := Person{Name: "Charlie", Age: -5}
invalidResult := enhancedCodec.Decode(invalidPerson)
assert.True(t, either.IsLeft(invalidResult), "Should fail with negative age")
// Extract and verify we have errors
errors := either.MonadFold(invalidResult,
F.Identity[validation.Errors],
func(Person) validation.Errors { return nil },
)
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
func TestApSL_TypeChecking(t *testing.T) {
t.Run("preserves base type checker", func(t *testing.T) {
// Create a 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}
},
)
// Create base codec with type checker
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 "" },
)
// Create field codec
nameCodec := 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],
)
// Apply ApSL
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Test type checking with valid type
person := Person{Name: "Eve", Age: 22}
isResult := enhancedCodec.Is(person)
assert.True(t, either.IsRight(isResult), "Should accept Person type")
// Test type checking with invalid type
invalidResult := enhancedCodec.Is("not a person")
assert.True(t, either.IsLeft(invalidResult), "Should reject non-Person type")
})
}
func TestApSL_Naming(t *testing.T) {
t.Run("generates descriptive name", func(t *testing.T) {
// Create a 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}
},
)
// Create base codec
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 "" },
)
// Create field codec
nameCodec := 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],
)
// Apply ApSL
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Check that the name includes ApS
name := enhancedCodec.Name()
assert.Contains(t, name, "ApS", "Name should contain 'ApS'")
})
}
func TestApSL_ErrorAccumulation(t *testing.T) {
t.Run("accumulates validation errors", func(t *testing.T) {
// Create a lens for Person.Age
ageLens := lens.MakeLens(
func(p Person) int { return p.Age },
func(p Person, age int) Person {
return Person{Name: p.Name, Age: age}
},
)
// Create base codec that fails validation
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "base validation error",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
return validation.FailureWithMessage[Person](i, "base validation error")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec that also fails
ageCodec := MakeType(
"Age",
func(i any) validation.Result[int] {
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "age validation error",
},
}))
},
func(i any) Decode[Context, int] {
return func(ctx Context) validation.Validation[int] {
return validation.FailureWithMessage[int](i, "age validation error")(ctx)
}
},
strconv.Itoa,
)
// Apply ApSL
operator := ApSL(S.Monoid, ageLens, ageCodec)
enhancedCodec := operator(baseCodec)
// Test validation - should accumulate errors
person := Person{Name: "Dave", Age: 30}
result := enhancedCodec.Decode(person)
// Should fail
assert.True(t, either.IsLeft(result), "Should fail validation")
// Extract errors
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(Person) validation.Errors { return nil },
)
// Should have errors from both base and field validation
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
// Test types for ApSO
type PersonWithNickname struct {
Name string
Nickname *string
}
func TestApSO_EncodingWithPresentField(t *testing.T) {
t.Run("encodes optional field when present", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec that encodes to "Person:"
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "Person:" },
)
// Create field codec for Nickname
nicknameCodec := MakeType(
"Nickname",
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],
)
// Apply ApSO to combine encodings
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding with nickname present
nickname := "Ali"
person := PersonWithNickname{Name: "Alice", Nickname: &nickname}
encoded := enhancedCodec.Encode(person)
// Should include both base and nickname
assert.Contains(t, encoded, "Person:")
assert.Contains(t, encoded, "Ali")
})
}
func TestApSO_EncodingWithAbsentField(t *testing.T) {
t.Run("omits optional field when absent", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "Person:Bob" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
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],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding with nickname absent
person := PersonWithNickname{Name: "Bob", Nickname: nil}
encoded := enhancedCodec.Encode(person)
// Should only have base encoding
assert.Equal(t, "Person:Bob", encoded)
})
}
func TestApSO_TypeChecking(t *testing.T) {
t.Run("preserves base type checker", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec with type checker
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
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],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test type checking with valid type
nickname := "Eve"
person := PersonWithNickname{Name: "Eve", Nickname: &nickname}
isResult := enhancedCodec.Is(person)
assert.True(t, either.IsRight(isResult), "Should accept PersonWithNickname type")
// Test type checking with invalid type
invalidResult := enhancedCodec.Is("not a person")
assert.True(t, either.IsLeft(invalidResult), "Should reject non-PersonWithNickname type")
})
}
func TestApSO_Naming(t *testing.T) {
t.Run("generates descriptive name", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
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],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Check that the name includes ApS
name := enhancedCodec.Name()
assert.Contains(t, name, "ApS", "Name should contain 'ApS'")
})
}
func TestApSO_ErrorAccumulation(t *testing.T) {
t.Run("accumulates validation errors", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec that fails validation
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "base validation error",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
return validation.FailureWithMessage[PersonWithNickname](i, "base validation error")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec that also fails
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "nickname validation error",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
return validation.FailureWithMessage[string](i, "nickname validation error")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test validation with present nickname - should accumulate errors
nickname := "Dave"
person := PersonWithNickname{Name: "Dave", Nickname: &nickname}
result := enhancedCodec.Decode(person)
// Should fail
assert.True(t, either.IsLeft(result), "Should fail validation")
// Extract errors
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(PersonWithNickname) validation.Errors { return nil },
)
// Should have errors from both base and field validation
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
// Made with Bob

View File

@@ -10,6 +10,8 @@ import (
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/decoder"
"github.com/IBM/fp-go/v2/optics/encoder"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
@@ -338,4 +340,104 @@ type (
// - ApplicativeMonoid: Combines successful results using inner monoid
// - AlternativeMonoid: Combines applicative and alternative behaviors
Monoid[A any] = monoid.Monoid[A]
// Lens is an optic that focuses on a specific field within a product type S.
// It provides a way to get and set a field of type A within a structure of type S.
//
// A Lens[S, A] represents a relationship between a source type S and a focus type A,
// where the focus always exists (unlike Optional which may not exist).
//
// Lens operations:
// - Get: Extract the field value A from structure S
// - Set: Update the field value A in structure S, returning a new S
//
// Lens laws:
// 1. GetSet: If you get a value and then set it back, nothing changes
// Set(Get(s))(s) = s
// 2. SetGet: If you set a value, you can get it back
// Get(Set(a)(s)) = a
// 3. SetSet: Setting twice is the same as setting once with the final value
// Set(b)(Set(a)(s)) = Set(b)(s)
//
// In the codec context, lenses are used with ApSL to build codecs for struct fields:
// - Extract field values for encoding
// - Update field values during validation
// - Compose codec operations on nested structures
//
// Example:
// 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 },
// )
//
// // Use with ApSL to build a codec
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSL(S.Monoid, nameLens, codec.String),
// )
//
// See also:
// - ApSL: Applicative sequencing with lens
// - Optional: For fields that may not exist
Lens[S, A any] = lens.Lens[S, A]
// Optional is an optic that focuses on a field within a product type S that may not exist.
// It provides a way to get and set an optional field of type A within a structure of type S.
//
// An Optional[S, A] represents a relationship between a source type S and a focus type A,
// where the focus may or may not be present (unlike Lens where it always exists).
//
// Optional operations:
// - GetOption: Try to extract the field value, returning Option[A]
// - Set: Update the field value if it exists, returning a new S
//
// Optional laws:
// 1. GetSet (No-op on None): If GetOption returns None, Set has no effect
// GetOption(s) = None => Set(a)(s) = s
// 2. SetGet (Get what you Set): If GetOption returns Some, you can get back what you set
// GetOption(s) = Some(_) => GetOption(Set(a)(s)) = Some(a)
// 3. SetSet (Last Set Wins): Setting twice is the same as setting once with the final value
// Set(b)(Set(a)(s)) = Set(b)(s)
//
// In the codec context, optionals are used with ApSO to build codecs for optional fields:
// - Extract optional field values for encoding (only if present)
// - Update optional field values during validation
// - Handle nullable or pointer fields gracefully
// - Compose codec operations on structures with optional data
//
// Example:
// type Person struct {
// Name string
// Nickname *string // Optional field
// }
//
// nicknameOpt := optional.MakeOptional(
// func(p Person) option.Option[string] {
// if p.Nickname != nil {
// return option.Some(*p.Nickname)
// }
// return option.None[string]()
// },
// func(p Person, nick string) Person {
// p.Nickname = &nick
// return p
// },
// )
//
// // Use with ApSO to build a codec with optional field
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
// )
//
// // Encoding omits the field when absent
// p1 := Person{Name: "Alice", Nickname: nil}
// encoded := personCodec.Encode(p1) // No nickname in output
//
// See also:
// - ApSO: Applicative sequencing with optional
// - Lens: For fields that always exist
Optional[S, A any] = optional.Optional[S, A]
)

View File

@@ -123,3 +123,78 @@ func Last[A any]() Semigroup[A] {
func ToMagma[A any](s Semigroup[A]) M.Magma[A] {
return s
}
// ConcatWith creates a curried version of the Concat operation with the left argument fixed first.
// It returns a function that takes the left operand and returns another function that takes
// the right operand and performs the concatenation.
//
// This is useful for partial application and function composition patterns.
//
// # Type Parameters
//
// - A: The type of elements in the semigroup
//
// # Parameters
//
// - s: The semigroup to use for concatenation
//
// # Returns
//
// - func(A) func(A) A: A curried function that takes left then right operand
//
// # Example Usage
//
// import N "github.com/IBM/fp-go/v2/number"
// sum := N.SemigroupSum[int]()
// concatWith := ConcatWith(sum)
// add5 := concatWith(5)
// result := add5(3) // 5 + 3 = 8
//
// # See Also
//
// - AppendTo: Similar but fixes the right argument first
func ConcatWith[A any](s Semigroup[A]) func(A) func(A) A {
return func(l A) func(A) A {
return func(r A) A {
return s.Concat(l, r)
}
}
}
// AppendTo creates a curried version of the Concat operation with the right argument fixed first.
// It returns a function that takes the right operand and returns another function that takes
// the left operand and performs the concatenation.
//
// This is useful for partial application where you want to fix the second argument first,
// which is common in append-style operations.
//
// # Type Parameters
//
// - A: The type of elements in the semigroup
//
// # Parameters
//
// - s: The semigroup to use for concatenation
//
// # Returns
//
// - func(A) func(A) A: A curried function that takes right then left operand
//
// # Example Usage
//
// import S "github.com/IBM/fp-go/v2/string"
// strConcat := S.Semigroup
// appendTo := AppendTo(strConcat)
// addSuffix := appendTo("!")
// result := addSuffix("Hello") // "Hello" + "!" = "Hello!"
//
// # See Also
//
// - ConcatWith: Similar but fixes the left argument first
func AppendTo[A any](s Semigroup[A]) func(A) func(A) A {
return func(r A) func(A) A {
return func(l A) A {
return s.Concat(l, r)
}
}
}

View File

@@ -444,3 +444,261 @@ func BenchmarkFunctionSemigroup(b *testing.B) {
combined("hello")
}
}
// Test ConcatWith function
func TestConcatWith(t *testing.T) {
t.Run("with integer addition", func(t *testing.T) {
add := MakeSemigroup(func(a, b int) int { return a + b })
concatWith := ConcatWith(add)
// Fix left operand to 5
add5 := concatWith(5)
assert.Equal(t, 8, add5(3)) // 5 + 3 = 8
assert.Equal(t, 15, add5(10)) // 5 + 10 = 15
assert.Equal(t, 5, add5(0)) // 5 + 0 = 5
})
t.Run("with string concatenation", func(t *testing.T) {
concat := MakeSemigroup(func(a, b string) string { return a + b })
concatWith := ConcatWith(concat)
// Fix left operand to "Hello, "
greet := concatWith("Hello, ")
assert.Equal(t, "Hello, World", greet("World"))
assert.Equal(t, "Hello, Bob", greet("Bob"))
assert.Equal(t, "Hello, ", greet(""))
})
t.Run("with subtraction (non-commutative)", func(t *testing.T) {
sub := MakeSemigroup(func(a, b int) int { return a - b })
concatWith := ConcatWith(sub)
// Fix left operand to 10
subtract10 := concatWith(10)
assert.Equal(t, 7, subtract10(3)) // 10 - 3 = 7
assert.Equal(t, 5, subtract10(5)) // 10 - 5 = 5
assert.Equal(t, -5, subtract10(15)) // 10 - 15 = -5
})
t.Run("with First semigroup", func(t *testing.T) {
first := First[int]()
concatWith := ConcatWith(first)
// Fix left operand to 42
always42 := concatWith(42)
assert.Equal(t, 42, always42(1))
assert.Equal(t, 42, always42(100))
assert.Equal(t, 42, always42(0))
})
t.Run("with Last semigroup", func(t *testing.T) {
last := Last[string]()
concatWith := ConcatWith(last)
// Fix left operand to "first"
alwaysSecond := concatWith("first")
assert.Equal(t, "second", alwaysSecond("second"))
assert.Equal(t, "other", alwaysSecond("other"))
assert.Equal(t, "", alwaysSecond(""))
})
t.Run("currying behavior", func(t *testing.T) {
mul := MakeSemigroup(func(a, b int) int { return a * b })
concatWith := ConcatWith(mul)
// Create multiple partially applied functions
double := concatWith(2)
triple := concatWith(3)
quadruple := concatWith(4)
assert.Equal(t, 10, double(5)) // 2 * 5 = 10
assert.Equal(t, 15, triple(5)) // 3 * 5 = 15
assert.Equal(t, 20, quadruple(5)) // 4 * 5 = 20
})
t.Run("with complex types", func(t *testing.T) {
type Point struct {
X, Y int
}
pointAdd := MakeSemigroup(func(a, b Point) Point {
return Point{X: a.X + b.X, Y: a.Y + b.Y}
})
concatWith := ConcatWith(pointAdd)
// Fix left operand to origin offset
offset := concatWith(Point{X: 10, Y: 20})
assert.Equal(t, Point{X: 15, Y: 25}, offset(Point{X: 5, Y: 5}))
assert.Equal(t, Point{X: 10, Y: 20}, offset(Point{X: 0, Y: 0}))
})
}
// Test AppendTo function
func TestAppendTo(t *testing.T) {
t.Run("with integer addition", func(t *testing.T) {
add := MakeSemigroup(func(a, b int) int { return a + b })
appendTo := AppendTo(add)
// Fix right operand to 5
addTo5 := appendTo(5)
assert.Equal(t, 8, addTo5(3)) // 3 + 5 = 8
assert.Equal(t, 15, addTo5(10)) // 10 + 5 = 15
assert.Equal(t, 5, addTo5(0)) // 0 + 5 = 5
})
t.Run("with string concatenation", func(t *testing.T) {
concat := MakeSemigroup(func(a, b string) string { return a + b })
appendTo := AppendTo(concat)
// Fix right operand to "!"
addExclamation := appendTo("!")
assert.Equal(t, "Hello!", addExclamation("Hello"))
assert.Equal(t, "World!", addExclamation("World"))
assert.Equal(t, "!", addExclamation(""))
})
t.Run("with subtraction (non-commutative)", func(t *testing.T) {
sub := MakeSemigroup(func(a, b int) int { return a - b })
appendTo := AppendTo(sub)
// Fix right operand to 3
subtract3 := appendTo(3)
assert.Equal(t, 7, subtract3(10)) // 10 - 3 = 7
assert.Equal(t, 2, subtract3(5)) // 5 - 3 = 2
assert.Equal(t, -3, subtract3(0)) // 0 - 3 = -3
assert.Equal(t, -8, subtract3(-5)) // -5 - 3 = -8
})
t.Run("with First semigroup", func(t *testing.T) {
first := First[string]()
appendTo := AppendTo(first)
// Fix right operand to "second"
alwaysFirst := appendTo("second")
assert.Equal(t, "first", alwaysFirst("first"))
assert.Equal(t, "other", alwaysFirst("other"))
assert.Equal(t, "", alwaysFirst(""))
})
t.Run("with Last semigroup", func(t *testing.T) {
last := Last[int]()
appendTo := AppendTo(last)
// Fix right operand to 42
always42 := appendTo(42)
assert.Equal(t, 42, always42(1))
assert.Equal(t, 42, always42(100))
assert.Equal(t, 42, always42(0))
})
t.Run("currying behavior", func(t *testing.T) {
mul := MakeSemigroup(func(a, b int) int { return a * b })
appendTo := AppendTo(mul)
// Create multiple partially applied functions
multiplyBy2 := appendTo(2)
multiplyBy3 := appendTo(3)
multiplyBy4 := appendTo(4)
assert.Equal(t, 10, multiplyBy2(5)) // 5 * 2 = 10
assert.Equal(t, 15, multiplyBy3(5)) // 5 * 3 = 15
assert.Equal(t, 20, multiplyBy4(5)) // 5 * 4 = 20
})
t.Run("with complex types", func(t *testing.T) {
type Point struct {
X, Y int
}
pointAdd := MakeSemigroup(func(a, b Point) Point {
return Point{X: a.X + b.X, Y: a.Y + b.Y}
})
appendTo := AppendTo(pointAdd)
// Fix right operand to offset
addOffset := appendTo(Point{X: 10, Y: 20})
assert.Equal(t, Point{X: 15, Y: 25}, addOffset(Point{X: 5, Y: 5}))
assert.Equal(t, Point{X: 10, Y: 20}, addOffset(Point{X: 0, Y: 0}))
})
}
// Test ConcatWith vs AppendTo difference
func TestConcatWithVsAppendTo(t *testing.T) {
t.Run("demonstrates order difference with non-commutative operation", func(t *testing.T) {
sub := MakeSemigroup(func(a, b int) int { return a - b })
concatWith := ConcatWith(sub)
appendTo := AppendTo(sub)
// ConcatWith fixes left operand first
subtract10From := concatWith(10) // 10 - x
assert.Equal(t, 7, subtract10From(3)) // 10 - 3 = 7
// AppendTo fixes right operand first
subtract3From := appendTo(3) // x - 3
assert.Equal(t, 7, subtract3From(10)) // 10 - 3 = 7
// Same result but different partial application order
assert.Equal(t, subtract10From(3), subtract3From(10))
})
t.Run("demonstrates order difference with string concatenation", func(t *testing.T) {
concat := MakeSemigroup(func(a, b string) string { return a + b })
concatWith := ConcatWith(concat)
appendTo := AppendTo(concat)
// ConcatWith: prefix is fixed
addPrefix := concatWith("Hello, ")
assert.Equal(t, "Hello, World", addPrefix("World"))
// AppendTo: suffix is fixed
addSuffix := appendTo("!")
assert.Equal(t, "Hello!", addSuffix("Hello"))
// Different results due to different order
assert.NotEqual(t, addPrefix("test"), addSuffix("test"))
})
}
// Test composition with ConcatWith and AppendTo
func TestConcatWithAppendToComposition(t *testing.T) {
t.Run("composing multiple operations", func(t *testing.T) {
add := MakeSemigroup(func(a, b int) int { return a + b })
// Create a pipeline: add 5, then add 3
concatWith := ConcatWith(add)
appendTo := AppendTo(add)
add5 := concatWith(5)
add3 := appendTo(3)
// Apply both operations
result := add3(add5(2)) // (2 + 5) + 3 = 10
assert.Equal(t, 10, result)
})
}
// Benchmark ConcatWith
func BenchmarkConcatWith(b *testing.B) {
add := MakeSemigroup(func(a, b int) int { return a + b })
concatWith := ConcatWith(add)
add5 := concatWith(5)
b.ResetTimer()
for b.Loop() {
add5(3)
}
}
// Benchmark AppendTo
func BenchmarkAppendTo(b *testing.B) {
add := MakeSemigroup(func(a, b int) int { return a + b })
appendTo := AppendTo(add)
addTo5 := appendTo(5)
b.ResetTimer()
for b.Loop() {
addTo5(3)
}
}