mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-28 13:12:03 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1837d3f86d | ||
|
|
b2d111e8ec |
@@ -16,25 +16,340 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
fa := Make[string, int]("foo")
|
||||
assert.Equal(t, fa, F.Pipe1(fa, Map[string](utils.Double)))
|
||||
// TestMake tests the Make constructor
|
||||
func TestMake(t *testing.T) {
|
||||
t.Run("creates Const with string value", func(t *testing.T) {
|
||||
c := Make[string, int]("hello")
|
||||
assert.Equal(t, "hello", Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("creates Const with int value", func(t *testing.T) {
|
||||
c := Make[int, string](42)
|
||||
assert.Equal(t, 42, Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("creates Const with struct value", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Name string
|
||||
Port int
|
||||
}
|
||||
cfg := Config{Name: "server", Port: 8080}
|
||||
c := Make[Config, bool](cfg)
|
||||
assert.Equal(t, cfg, Unwrap(c))
|
||||
})
|
||||
}
|
||||
|
||||
// TestUnwrap tests extracting values from Const
|
||||
func TestUnwrap(t *testing.T) {
|
||||
t.Run("unwraps string value", func(t *testing.T) {
|
||||
c := Make[string, int]("world")
|
||||
value := Unwrap(c)
|
||||
assert.Equal(t, "world", value)
|
||||
})
|
||||
|
||||
t.Run("unwraps empty string", func(t *testing.T) {
|
||||
c := Make[string, int]("")
|
||||
value := Unwrap(c)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
|
||||
t.Run("unwraps zero value", func(t *testing.T) {
|
||||
c := Make[int, string](0)
|
||||
value := Unwrap(c)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOf tests the Of function
|
||||
func TestOf(t *testing.T) {
|
||||
assert.Equal(t, Make[string, int](""), Of[string, int](S.Monoid)(1))
|
||||
t.Run("creates Const with monoid empty value", func(t *testing.T) {
|
||||
of := Of[string, int](S.Monoid)
|
||||
c := of(42)
|
||||
assert.Equal(t, "", Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("ignores input value", func(t *testing.T) {
|
||||
of := Of[string, int](S.Monoid)
|
||||
c1 := of(1)
|
||||
c2 := of(100)
|
||||
assert.Equal(t, Unwrap(c1), Unwrap(c2))
|
||||
})
|
||||
|
||||
t.Run("works with int monoid", func(t *testing.T) {
|
||||
of := Of[int, string](N.MonoidSum[int]())
|
||||
c := of("ignored")
|
||||
assert.Equal(t, 0, Unwrap(c))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
fab := Make[string, int]("bar")
|
||||
assert.Equal(t, Make[string, int]("foobar"), Ap[string, int, int](S.Monoid)(fab)(Make[string, func(int) int]("foo")))
|
||||
// TestMap tests the Map function
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("preserves wrapped value", func(t *testing.T) {
|
||||
fa := Make[string, int]("foo")
|
||||
result := F.Pipe1(fa, Map[string](utils.Double))
|
||||
assert.Equal(t, "foo", Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("changes phantom type", func(t *testing.T) {
|
||||
fa := Make[string, int]("data")
|
||||
fb := Map[string, int, string](strconv.Itoa)(fa)
|
||||
// Value unchanged, but type changed from Const[string, int] to Const[string, string]
|
||||
assert.Equal(t, "data", Unwrap(fb))
|
||||
})
|
||||
|
||||
t.Run("function is never called", func(t *testing.T) {
|
||||
called := false
|
||||
fa := Make[string, int]("test")
|
||||
fb := Map[string, int, string](func(i int) string {
|
||||
called = true
|
||||
return strconv.Itoa(i)
|
||||
})(fa)
|
||||
assert.False(t, called, "Map function should not be called")
|
||||
assert.Equal(t, "test", Unwrap(fb))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadMap tests the MonadMap function
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("preserves wrapped value", func(t *testing.T) {
|
||||
fa := Make[string, int]("original")
|
||||
fb := MonadMap(fa, func(i int) string { return strconv.Itoa(i) })
|
||||
assert.Equal(t, "original", Unwrap(fb))
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
fa := Make[int, string](42)
|
||||
fb := MonadMap(fa, func(s string) bool { return len(s) > 0 })
|
||||
assert.Equal(t, 42, Unwrap(fb))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAp tests the Ap function
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("combines string values", func(t *testing.T) {
|
||||
fab := Make[string, int]("bar")
|
||||
fa := Make[string, func(int) int]("foo")
|
||||
result := Ap[string, int, int](S.Monoid)(fab)(fa)
|
||||
assert.Equal(t, "foobar", Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("combines int values with sum", func(t *testing.T) {
|
||||
fab := Make[int, string](10)
|
||||
fa := Make[int, func(string) string](5)
|
||||
result := Ap[int, string, string](N.SemigroupSum[int]())(fab)(fa)
|
||||
assert.Equal(t, 15, Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("combines int values with product", func(t *testing.T) {
|
||||
fab := Make[int, bool](3)
|
||||
fa := Make[int, func(bool) bool](4)
|
||||
result := Ap[int, bool, bool](N.SemigroupProduct[int]())(fab)(fa)
|
||||
assert.Equal(t, 12, Unwrap(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests the MonadAp function
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("combines values using semigroup", func(t *testing.T) {
|
||||
ap := MonadAp[string, int, int](S.Monoid)
|
||||
fab := Make[string, func(int) int]("hello")
|
||||
fa := Make[string, int]("world")
|
||||
result := ap(fab, fa)
|
||||
assert.Equal(t, "helloworld", Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("works with empty strings", func(t *testing.T) {
|
||||
ap := MonadAp[string, int, int](S.Monoid)
|
||||
fab := Make[string, func(int) int]("")
|
||||
fa := Make[string, int]("test")
|
||||
result := ap(fab, fa)
|
||||
assert.Equal(t, "test", Unwrap(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoid tests the Monoid function
|
||||
func TestMonoid(t *testing.T) {
|
||||
t.Run("always returns constant value", func(t *testing.T) {
|
||||
m := Monoid(42)
|
||||
assert.Equal(t, 42, m.Concat(1, 2))
|
||||
assert.Equal(t, 42, m.Concat(100, 200))
|
||||
assert.Equal(t, 42, m.Empty())
|
||||
})
|
||||
|
||||
t.Run("works with strings", func(t *testing.T) {
|
||||
m := Monoid("constant")
|
||||
assert.Equal(t, "constant", m.Concat("a", "b"))
|
||||
assert.Equal(t, "constant", m.Empty())
|
||||
})
|
||||
|
||||
t.Run("works with structs", func(t *testing.T) {
|
||||
type Point struct{ X, Y int }
|
||||
p := Point{X: 1, Y: 2}
|
||||
m := Monoid(p)
|
||||
assert.Equal(t, p, m.Concat(Point{X: 3, Y: 4}, Point{X: 5, Y: 6}))
|
||||
assert.Equal(t, p, m.Empty())
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := Monoid(10)
|
||||
|
||||
// Left identity: Concat(Empty(), x) = x (both return constant)
|
||||
assert.Equal(t, 10, m.Concat(m.Empty(), 5))
|
||||
|
||||
// Right identity: Concat(x, Empty()) = x (both return constant)
|
||||
assert.Equal(t, 10, m.Concat(5, m.Empty()))
|
||||
|
||||
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
left := m.Concat(m.Concat(1, 2), 3)
|
||||
right := m.Concat(1, m.Concat(2, 3))
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, 10, left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConstFunctorLaws tests functor laws for Const
|
||||
func TestConstFunctorLaws(t *testing.T) {
|
||||
t.Run("identity law", func(t *testing.T) {
|
||||
// map id = id
|
||||
fa := Make[string, int]("test")
|
||||
mapped := Map[string, int, int](F.Identity[int])(fa)
|
||||
assert.Equal(t, Unwrap(fa), Unwrap(mapped))
|
||||
})
|
||||
|
||||
t.Run("composition law", func(t *testing.T) {
|
||||
// map (g . f) = map g . map f
|
||||
fa := Make[string, int]("data")
|
||||
f := func(i int) string { return strconv.Itoa(i) }
|
||||
g := func(s string) bool { return len(s) > 0 }
|
||||
|
||||
// map (g . f)
|
||||
composed := Map[string, int, bool](func(i int) bool { return g(f(i)) })(fa)
|
||||
|
||||
// map g . map f
|
||||
intermediate := F.Pipe1(fa, Map[string, int, string](f))
|
||||
chained := Map[string, string, bool](g)(intermediate)
|
||||
|
||||
assert.Equal(t, Unwrap(composed), Unwrap(chained))
|
||||
})
|
||||
}
|
||||
|
||||
// TestConstApplicativeLaws tests applicative laws for Const
|
||||
func TestConstApplicativeLaws(t *testing.T) {
|
||||
t.Run("identity law", func(t *testing.T) {
|
||||
// For Const, ap combines the wrapped values using the semigroup
|
||||
// ap (of id) v combines empty (from of) with v's value
|
||||
v := Make[string, int]("value")
|
||||
ofId := Of[string, func(int) int](S.Monoid)(F.Identity[int])
|
||||
result := Ap[string, int, int](S.Monoid)(v)(ofId)
|
||||
// Result combines "" (from Of) with "value" using string monoid
|
||||
assert.Equal(t, "value", Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("homomorphism law", func(t *testing.T) {
|
||||
// ap (of f) (of x) = of (f x)
|
||||
f := func(i int) string { return strconv.Itoa(i) }
|
||||
x := 42
|
||||
|
||||
ofF := Of[string, func(int) string](S.Monoid)(f)
|
||||
ofX := Of[string, int](S.Monoid)(x)
|
||||
left := Ap[string, int, string](S.Monoid)(ofX)(ofF)
|
||||
|
||||
right := Of[string, string](S.Monoid)(f(x))
|
||||
|
||||
assert.Equal(t, Unwrap(left), Unwrap(right))
|
||||
})
|
||||
}
|
||||
|
||||
// TestConstEdgeCases tests edge cases
|
||||
func TestConstEdgeCases(t *testing.T) {
|
||||
t.Run("empty string values", func(t *testing.T) {
|
||||
c := Make[string, int]("")
|
||||
assert.Equal(t, "", Unwrap(c))
|
||||
|
||||
mapped := Map[string, int, string](strconv.Itoa)(c)
|
||||
assert.Equal(t, "", Unwrap(mapped))
|
||||
})
|
||||
|
||||
t.Run("zero values", func(t *testing.T) {
|
||||
c := Make[int, string](0)
|
||||
assert.Equal(t, 0, Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var ptr *int
|
||||
c := Make[*int, string](ptr)
|
||||
assert.Nil(t, Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("multiple map operations", func(t *testing.T) {
|
||||
c := Make[string, int]("original")
|
||||
// Chain multiple map operations
|
||||
step1 := Map[string, int, string](strconv.Itoa)(c)
|
||||
step2 := Map[string, string, bool](func(s string) bool { return len(s) > 0 })(step1)
|
||||
result := Map[string, bool, int](func(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})(step2)
|
||||
assert.Equal(t, "original", Unwrap(result))
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkMake benchmarks the Make constructor
|
||||
func BenchmarkMake(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = Make[string, int]("test")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkUnwrap benchmarks the Unwrap function
|
||||
func BenchmarkUnwrap(b *testing.B) {
|
||||
c := Make[string, int]("test")
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = Unwrap(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMap benchmarks the Map function
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
c := Make[string, int]("test")
|
||||
mapFn := Map[string, int, string](strconv.Itoa)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = mapFn(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkAp benchmarks the Ap function
|
||||
func BenchmarkAp(b *testing.B) {
|
||||
fab := Make[string, int]("hello")
|
||||
fa := Make[string, func(int) int]("world")
|
||||
apFn := Ap[string, int, int](S.Monoid)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = apFn(fab)(fa)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMonoid benchmarks the Monoid function
|
||||
func BenchmarkMonoid(b *testing.B) {
|
||||
m := Monoid(42)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = m.Concat(1, 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 constant
|
||||
|
||||
import (
|
||||
@@ -5,7 +20,47 @@ import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// Monoid returns a [M.Monoid] that returns a constant value in all operations
|
||||
// Monoid creates a monoid that always returns a constant value.
|
||||
//
|
||||
// This creates a trivial monoid where both the Concat operation and Empty
|
||||
// always return the same constant value, regardless of inputs. This is useful
|
||||
// for testing, placeholder implementations, or when you need a monoid instance
|
||||
// but the actual combining behavior doesn't matter.
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// The constant monoid satisfies all monoid laws trivially:
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always returns 'a'
|
||||
// - Left Identity: Concat(Empty(), x) = x - both return 'a'
|
||||
// - Right Identity: Concat(x, Empty()) = x - both return 'a'
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the constant value
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The constant value to return in all operations
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[A] that always returns the constant value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a monoid that always returns 42
|
||||
// m := Monoid(42)
|
||||
// result := m.Concat(1, 2) // 42
|
||||
// empty := m.Empty() // 42
|
||||
//
|
||||
// // Useful for testing or placeholder implementations
|
||||
// type Config struct {
|
||||
// Timeout int
|
||||
// }
|
||||
// defaultConfig := Monoid(Config{Timeout: 30})
|
||||
// config := defaultConfig.Concat(Config{Timeout: 10}, Config{Timeout: 20})
|
||||
// // config is Config{Timeout: 30}
|
||||
//
|
||||
// See also:
|
||||
// - function.Constant2: The underlying constant function
|
||||
// - M.MakeMonoid: The monoid constructor
|
||||
func Monoid[A any](a A) M.Monoid[A] {
|
||||
return M.MakeMonoid(function.Constant2[A, A](a), a)
|
||||
}
|
||||
|
||||
145
v2/optics/codec/bind.go
Normal file
145
v2/optics/codec/bind.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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/optics/codec/validate"
|
||||
"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),
|
||||
)
|
||||
}
|
||||
}
|
||||
415
v2/optics/codec/bind_test.go
Normal file
415
v2/optics/codec/bind_test.go
Normal file
@@ -0,0 +1,415 @@
|
||||
// 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"
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
@@ -10,6 +10,7 @@ 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/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
@@ -338,4 +339,6 @@ type (
|
||||
// - ApplicativeMonoid: Combines successful results using inner monoid
|
||||
// - AlternativeMonoid: Combines applicative and alternative behaviors
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user