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

Compare commits

..

5 Commits

Author SHA1 Message Date
Dr. Carsten Leue
4d67b1d254 fix: expose Empty for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 10:52:23 +01:00
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
Dr. Carsten Leue
b2d111e8ec fix: more doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 14:04:44 +01:00
38 changed files with 3243 additions and 226 deletions

View File

@@ -151,6 +151,11 @@ func TestFromReaderResult_Success(t *testing.T) {
- Don't manually handle `(value, error)` tuples when helpers exist
- Don't use `either.MonadFold` in tests unless necessary
4. **Use Void Type for Unit Values**
- Use `function.Void` (or `F.Void`) instead of `struct{}`
- Use `function.VOID` (or `F.VOID`) instead of `struct{}{}`
- Example: `Empty[F.Void, F.Void, any](lazy.Of(pair.MakePair(F.VOID, F.VOID)))`
### Error Handling
1. **In Production Code**

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)

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -55,5 +55,7 @@ type (
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
// Pair represents a tuple of two values of types L and R.
// It's commonly used to return multiple values from functions or to group related data.
Pair[L, R any] = pair.Pair[L, R]
)

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,816 @@
// 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")
})
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
@@ -747,3 +748,114 @@ func FromRefinement[A, B any](refinement Refinement[A, B]) Type[B, A, A] {
refinement.ReverseGet,
)
}
// Empty creates a Type codec that ignores input during decoding and uses a default value,
// and ignores the value during encoding, using a default output.
//
// This codec is useful for:
// - Providing default values for optional fields
// - Creating placeholder codecs in generic contexts
// - Implementing constant codecs that always produce the same value
// - Building codecs for phantom types or unit-like types
//
// The codec uses a lazily-evaluated Pair[O, A] to provide both the default output
// for encoding and the default value for decoding. The lazy evaluation ensures that
// the defaults are only computed when needed.
//
// # Type Parameters
//
// - A: The target type (what we decode to and encode from)
// - O: The output type (what we encode to)
// - I: The input type (what we decode from, but is ignored)
//
// # Parameters
//
// - e: A Lazy[Pair[O, A]] that provides the default values:
// - pair.Head(e()): The default output value O used during encoding
// - pair.Tail(e()): The default decoded value A used during decoding
//
// # Returns
//
// - A Type[A, O, I] that:
// - Decode: Always succeeds and returns the default value A, ignoring input I
// - Encode: Always returns the default output O, ignoring the input value A
// - Is: Checks if a value is of type A (standard type checking)
// - Name: Returns "Empty"
//
// # Behavior
//
// Decoding:
// - Ignores the input value completely
// - Always succeeds with validation.Success
// - Returns the default value from pair.Tail(e())
//
// Encoding:
// - Ignores the input value completely
// - Always returns the default output from pair.Head(e())
//
// # Example Usage
//
// Creating a codec with default values:
//
// // Create a codec that always decodes to 42 and encodes to "default"
// defaultCodec := codec.Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
//
// // Decode always returns 42, regardless of input
// result := defaultCodec.Decode("anything") // Success: Right(42)
// result = defaultCodec.Decode(123) // Success: Right(42)
// result = defaultCodec.Decode(nil) // Success: Right(42)
//
// // Encode always returns "default", regardless of input
// encoded := defaultCodec.Encode(100) // Returns: "default"
// encoded = defaultCodec.Encode(0) // Returns: "default"
//
// Using with struct fields for default values:
//
// type Config struct {
// Timeout int
// Retries int
// }
//
// // Codec that provides default retries value
// defaultRetries := codec.Empty[int, int, any](lazy.Of(pair.MakePair(3, 3)))
//
// configCodec := F.Pipe2(
// codec.Struct[Config]("Config"),
// codec.ApSL(S.Monoid, timeoutLens, codec.Int()),
// codec.ApSL(S.Monoid, retriesLens, defaultRetries),
// )
//
// Creating a unit-like codec:
//
// // Codec for a unit type that always produces Void
// unitCodec := codec.Empty[function.Void, function.Void, any](
// lazy.Of(pair.MakePair(function.VOID, function.VOID)),
// )
//
// # Use Cases
//
// - Default values: Provide fallback values when decoding optional fields
// - Constant codecs: Always produce the same value regardless of input
// - Placeholder codecs: Use in generic contexts where a codec is required but not used
// - Unit types: Encode/decode unit-like types that carry no information
// - Testing: Create simple codecs for testing codec composition
//
// # Notes
//
// - The lazy evaluation of the Pair ensures defaults are only computed when needed
// - Both encoding and decoding always succeed (no validation errors)
// - The input values are completely ignored in both directions
// - The Is method still performs standard type checking for type A
// - This codec is useful in applicative composition where some fields have defaults
//
// See also:
// - Id: For identity codecs that preserve values
// - MakeType: For creating custom codecs with validation logic
func Empty[A, O, I any](e Lazy[Pair[O, A]]) Type[A, O, I] {
return MakeType(
"Empty",
Is[A](),
validate.OfLazy[I](F.Pipe1(e, lazy.Map(pair.Tail[O, A]))),
reader.OfLazy[A](F.Pipe1(e, lazy.Map(pair.Head[O, A]))),
)
}

View File

@@ -7,9 +7,11 @@ import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -19,12 +21,7 @@ func TestString(t *testing.T) {
stringType := String()
result := stringType.Decode("hello")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "hello", value)
assert.Equal(t, validation.Of("hello"), result)
})
t.Run("fails to decode non-string", func(t *testing.T) {
@@ -57,12 +54,7 @@ func TestString(t *testing.T) {
stringType := String()
result := stringType.Decode("")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "error" },
F.Identity[string],
)
assert.Equal(t, "", value)
assert.Equal(t, validation.Of(""), result)
})
}
@@ -71,12 +63,7 @@ func TestInt(t *testing.T) {
intType := Int()
result := intType.Decode(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, validation.Of(42), result)
})
t.Run("fails to decode string as int", func(t *testing.T) {
@@ -109,24 +96,14 @@ func TestInt(t *testing.T) {
intType := Int()
result := intType.Decode(-42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, -42, value)
assert.Equal(t, validation.Of(-42), result)
})
t.Run("decodes zero", func(t *testing.T) {
intType := Int()
result := intType.Decode(0)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
assert.Equal(t, validation.Of(0), result)
})
}
@@ -135,24 +112,14 @@ func TestBool(t *testing.T) {
boolType := Bool()
result := boolType.Decode(true)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) bool { return false },
F.Identity[bool],
)
assert.Equal(t, true, value)
assert.Equal(t, validation.Of(true), result)
})
t.Run("decodes false", func(t *testing.T) {
boolType := Bool()
result := boolType.Decode(false)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) bool { return true },
F.Identity[bool],
)
assert.Equal(t, false, value)
assert.Equal(t, validation.Of(false), result)
})
t.Run("fails to decode int as bool", func(t *testing.T) {
@@ -189,36 +156,21 @@ func TestArray(t *testing.T) {
intArray := Array(Int())
result := intArray.Decode([]int{1, 2, 3})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3}, value)
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
})
t.Run("decodes valid string array", func(t *testing.T) {
stringArray := Array(String())
result := stringArray.Decode([]string{"a", "b", "c"})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []string { return nil },
F.Identity[[]string],
)
assert.Equal(t, []string{"a", "b", "c"}, value)
assert.Equal(t, validation.Of([]string{"a", "b", "c"}), result)
})
t.Run("decodes empty array", func(t *testing.T) {
intArray := Array(Int())
result := intArray.Decode([]int{})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{}, value)
assert.Equal(t, validation.Of([]int{}), result)
})
t.Run("fails when array contains invalid element", func(t *testing.T) {
@@ -256,12 +208,7 @@ func TestArray(t *testing.T) {
nestedArray := Array(Array(Int()))
result := nestedArray.Decode([][]int{{1, 2}, {3, 4}})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) [][]int { return nil },
F.Identity[[][]int],
)
assert.Equal(t, [][]int{{1, 2}, {3, 4}}, value)
assert.Equal(t, validation.Of([][]int{{1, 2}, {3, 4}}), result)
})
t.Run("fails to decode non-iterable", func(t *testing.T) {
@@ -275,12 +222,7 @@ func TestArray(t *testing.T) {
boolArray := Array(Bool())
result := boolArray.Decode([]bool{true, false, true})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []bool { return nil },
F.Identity[[]bool],
)
assert.Equal(t, []bool{true, false, true}, value)
assert.Equal(t, validation.Of([]bool{true, false, true}), result)
})
t.Run("collects multiple validation errors", func(t *testing.T) {
@@ -360,24 +302,14 @@ func TestTranscodeArray(t *testing.T) {
intTranscode := TranscodeArray(Int())
result := intTranscode.Decode([]any{1, 2, 3})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3}, value)
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
})
t.Run("decodes valid string array from string slice", func(t *testing.T) {
stringTranscode := TranscodeArray(String())
result := stringTranscode.Decode([]any{"a", "b", "c"})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []string { return nil },
F.Identity[[]string],
)
assert.Equal(t, []string{"a", "b", "c"}, value)
assert.Equal(t, validation.Of([]string{"a", "b", "c"}), result)
})
t.Run("decodes empty array", func(t *testing.T) {
@@ -411,24 +343,14 @@ func TestTranscodeArray(t *testing.T) {
nestedTranscode := TranscodeArray(TranscodeArray(Int()))
result := nestedTranscode.Decode([][]any{{1, 2}, {3, 4}})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) [][]int { return nil },
F.Identity[[][]int],
)
assert.Equal(t, [][]int{{1, 2}, {3, 4}}, value)
assert.Equal(t, validation.Of([][]int{{1, 2}, {3, 4}}), result)
})
t.Run("decodes array of bools", func(t *testing.T) {
boolTranscode := TranscodeArray(Bool())
result := boolTranscode.Decode([]any{true, false, true})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []bool { return nil },
F.Identity[[]bool],
)
assert.Equal(t, []bool{true, false, true}, value)
assert.Equal(t, validation.Of([]bool{true, false, true}), result)
})
t.Run("encodes empty array", func(t *testing.T) {
@@ -481,12 +403,7 @@ func TestTranscodeArrayWithTransformation(t *testing.T) {
arrayTranscode := TranscodeArray(stringToInt)
result := arrayTranscode.Decode([]string{"a", "bb", "ccc"})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3}, value)
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
})
t.Run("encodes int slice to string slice", func(t *testing.T) {
@@ -1358,24 +1275,14 @@ func TestId(t *testing.T) {
idCodec := Id[string]()
result := idCodec.Decode("hello")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "hello", value)
assert.Equal(t, validation.Of("hello"), result)
})
t.Run("decodes int successfully", func(t *testing.T) {
idCodec := Id[int]()
result := idCodec.Decode(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, validation.Of(42), result)
})
t.Run("encodes with identity function", func(t *testing.T) {
@@ -1431,13 +1338,7 @@ func TestId(t *testing.T) {
person := Person{Name: "Alice", Age: 30}
result := idCodec.Decode(person)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Person { return Person{} },
F.Identity[Person],
)
assert.Equal(t, person, value)
assert.Equal(t, validation.Of(person), result)
encoded := idCodec.Encode(person)
assert.Equal(t, person, encoded)
@@ -1450,13 +1351,7 @@ func TestIdWithTranscodeArray(t *testing.T) {
arrayCodec := TranscodeArray(intId)
result := arrayCodec.Decode([]int{1, 2, 3, 4, 5})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3, 4, 5}, value)
assert.Equal(t, validation.Of([]int{1, 2, 3, 4, 5}), result)
})
t.Run("Id codec encodes array with identity", func(t *testing.T) {
@@ -1473,13 +1368,7 @@ func TestIdWithTranscodeArray(t *testing.T) {
input := [][]int{{1, 2}, {3, 4}, {5}}
result := nestedCodec.Decode(input)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) [][]int { return nil },
F.Identity[[][]int],
)
assert.Equal(t, input, value)
assert.Equal(t, validation.Of(input), result)
})
}
@@ -1748,7 +1637,7 @@ func TestFromRefinementComposition(t *testing.T) {
positiveCodec := FromRefinement(positiveIntPrism)
// Compose with Int codec using Pipe
composed := Pipe[int, any, int, int](positiveCodec)(Int())
composed := Pipe[int, any](positiveCodec)(Int())
t.Run("ComposedDecodeValid", func(t *testing.T) {
result := composed.Decode(42)
@@ -1849,3 +1738,416 @@ func TestFromRefinementValidationContext(t *testing.T) {
assert.Equal(t, -5, err.Value)
})
}
// TestEmpty_Success tests that Empty always succeeds during decoding
func TestEmpty_Success(t *testing.T) {
t.Run("decodes any input to default value", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
// Test with various input types
testCases := []struct {
name string
input any
}{
{"string input", "anything"},
{"int input", 123},
{"nil input", nil},
{"bool input", true},
{"struct input", struct{ X int }{X: 10}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := defaultCodec.Decode(tc.input)
assert.Equal(t, validation.Of(42), result)
})
}
})
t.Run("always returns same default value", func(t *testing.T) {
defaultCodec := Empty[string, string, any](lazy.Of(pair.MakePair("output", "default")))
result1 := defaultCodec.Decode(123)
result2 := defaultCodec.Decode("different")
result3 := defaultCodec.Decode(nil)
assert.True(t, either.IsRight(result1))
assert.True(t, either.IsRight(result2))
assert.True(t, either.IsRight(result3))
value1 := either.MonadFold(result1, func(validation.Errors) string { return "" }, F.Identity[string])
value2 := either.MonadFold(result2, func(validation.Errors) string { return "" }, F.Identity[string])
value3 := either.MonadFold(result3, func(validation.Errors) string { return "" }, F.Identity[string])
assert.Equal(t, "default", value1)
assert.Equal(t, "default", value2)
assert.Equal(t, "default", value3)
})
}
// TestEmpty_Encoding tests that Empty always uses default output during encoding
func TestEmpty_Encoding(t *testing.T) {
t.Run("encodes any value to default output", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
// Test with various input values
testCases := []struct {
name string
input int
}{
{"zero value", 0},
{"positive value", 100},
{"negative value", -50},
{"default value", 42},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
encoded := defaultCodec.Encode(tc.input)
assert.Equal(t, "default", encoded)
})
}
})
t.Run("always returns same default output", func(t *testing.T) {
defaultCodec := Empty[string, int, any](lazy.Of(pair.MakePair(999, "ignored")))
encoded1 := defaultCodec.Encode("value1")
encoded2 := defaultCodec.Encode("value2")
encoded3 := defaultCodec.Encode("")
assert.Equal(t, 999, encoded1)
assert.Equal(t, 999, encoded2)
assert.Equal(t, 999, encoded3)
})
}
// TestEmpty_Name tests that Empty has correct name
func TestEmpty_Name(t *testing.T) {
t.Run("has name 'Empty'", func(t *testing.T) {
defaultCodec := Empty[int, int, any](lazy.Of(pair.MakePair(0, 0)))
assert.Equal(t, "Empty", defaultCodec.Name())
})
}
// TestEmpty_TypeChecking tests that Empty performs standard type checking
func TestEmpty_TypeChecking(t *testing.T) {
t.Run("Is checks for correct type", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
// Should succeed for int
result := defaultCodec.Is(100)
assert.True(t, either.IsRight(result))
// Should fail for non-int
result = defaultCodec.Is("not an int")
assert.True(t, either.IsLeft(result))
})
t.Run("Is checks for string type", func(t *testing.T) {
defaultCodec := Empty[string, string, any](lazy.Of(pair.MakePair("out", "in")))
// Should succeed for string
result := defaultCodec.Is("hello")
assert.True(t, either.IsRight(result))
// Should fail for non-string
result = defaultCodec.Is(123)
assert.True(t, either.IsLeft(result))
})
}
// TestEmpty_LazyEvaluation tests that the Pair parameter allows dynamic values
func TestEmpty_LazyEvaluation(t *testing.T) {
t.Run("lazy pair allows dynamic values", func(t *testing.T) {
counter := 0
lazyPair := func() pair.Pair[int, int] {
counter++
return pair.MakePair(counter, counter*10)
}
defaultCodec := Empty[int, int, any](lazyPair)
// Each decode can get a different value if the lazy function is dynamic
result1 := defaultCodec.Decode("input1")
value1 := either.MonadFold(result1,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
result2 := defaultCodec.Decode("input2")
value2 := either.MonadFold(result2,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
// Values can be different if lazy function produces different results
assert.True(t, value1 > 0)
assert.True(t, value2 > 0)
})
}
// TestEmpty_WithStructs tests Empty with struct types
func TestEmpty_WithStructs(t *testing.T) {
type Config struct {
Timeout int
Retries int
}
t.Run("provides default struct value", func(t *testing.T) {
defaultConfig := Config{Timeout: 30, Retries: 3}
defaultCodec := Empty[Config, Config, any](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Config { return Config{} },
F.Identity[Config],
)
assert.Equal(t, 30, value.Timeout)
assert.Equal(t, 3, value.Retries)
})
t.Run("encodes to default struct", func(t *testing.T) {
defaultConfig := Config{Timeout: 30, Retries: 3}
inputConfig := Config{Timeout: 60, Retries: 5}
defaultCodec := Empty[Config, Config, any](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
encoded := defaultCodec.Encode(inputConfig)
assert.Equal(t, 30, encoded.Timeout)
assert.Equal(t, 3, encoded.Retries)
})
}
// TestEmpty_WithPointers tests Empty with pointer types
func TestEmpty_WithPointers(t *testing.T) {
t.Run("provides default pointer value", func(t *testing.T) {
defaultValue := 42
defaultCodec := Empty[*int, *int, any](lazy.Of(pair.MakePair(&defaultValue, &defaultValue)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) *int { return nil },
F.Identity[*int],
)
require.NotNil(t, value)
assert.Equal(t, 42, *value)
})
t.Run("provides nil pointer as default", func(t *testing.T) {
var nilPtr *int
defaultCodec := Empty[*int, *int, any](lazy.Of(pair.MakePair(nilPtr, nilPtr)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) *int { return new(int) },
F.Identity[*int],
)
assert.Nil(t, value)
})
}
// TestEmpty_WithSlices tests Empty with slice types
func TestEmpty_WithSlices(t *testing.T) {
t.Run("provides default slice value", func(t *testing.T) {
defaultSlice := []int{1, 2, 3}
defaultCodec := Empty[[]int, []int, any](lazy.Of(pair.MakePair(defaultSlice, defaultSlice)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3}, value)
})
t.Run("provides empty slice as default", func(t *testing.T) {
emptySlice := []int{}
defaultCodec := Empty[[]int, []int, any](lazy.Of(pair.MakePair(emptySlice, emptySlice)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{}, value)
})
}
// TestEmpty_DifferentInputOutput tests Empty with different input and output types
func TestEmpty_DifferentInputOutput(t *testing.T) {
t.Run("decodes to int, encodes to string", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("default-output", 42)))
// Decode always returns 42
result := defaultCodec.Decode("any input")
assert.Equal(t, validation.Of(42), result)
// Encode always returns "default-output"
encoded := defaultCodec.Encode(100)
assert.Equal(t, "default-output", encoded)
})
t.Run("decodes to string, encodes to int", func(t *testing.T) {
defaultCodec := Empty[string, int, any](lazy.Of(pair.MakePair(999, "default-value")))
// Decode always returns "default-value"
result := defaultCodec.Decode(123)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "default-value", value)
// Encode always returns 999
encoded := defaultCodec.Encode("any string")
assert.Equal(t, 999, encoded)
})
}
// TestEmpty_EdgeCases tests edge cases for Empty
func TestEmpty_EdgeCases(t *testing.T) {
t.Run("with zero values", func(t *testing.T) {
defaultCodec := Empty[int, int, any](lazy.Of(pair.MakePair(0, 0)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
encoded := defaultCodec.Encode(100)
assert.Equal(t, 0, encoded)
})
t.Run("with empty string", func(t *testing.T) {
defaultCodec := Empty[string, string, any](lazy.Of(pair.MakePair("", "")))
result := defaultCodec.Decode("non-empty")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "error" },
F.Identity[string],
)
assert.Equal(t, "", value)
encoded := defaultCodec.Encode("non-empty")
assert.Equal(t, "", encoded)
})
t.Run("with false boolean", func(t *testing.T) {
defaultCodec := Empty[bool, bool, any](lazy.Of(pair.MakePair(false, false)))
result := defaultCodec.Decode(true)
assert.Equal(t, validation.Of(false), result)
encoded := defaultCodec.Encode(true)
assert.Equal(t, false, encoded)
})
}
// TestEmpty_Integration tests Empty in composition scenarios
func TestEmpty_Integration(t *testing.T) {
t.Run("composes with other codecs using Pipe", func(t *testing.T) {
// Create a codec that always provides a default int
defaultIntCodec := Empty[int, int, any](lazy.Of(pair.MakePair(42, 42)))
// Create a refinement that only accepts positive integers
positiveIntPrism := prism.MakePrismWithName(
func(n int) option.Option[int] {
if n > 0 {
return option.Some(n)
}
return option.None[int]()
},
func(n int) int { return n },
"PositiveInt",
)
positiveCodec := FromRefinement(positiveIntPrism)
// Compose: always decode to 42, then validate it's positive
composed := Pipe[int, any](positiveCodec)(defaultIntCodec)
// Should succeed because 42 is positive
result := composed.Decode("anything")
assert.Equal(t, validation.Of(42), result)
})
t.Run("used as placeholder in generic contexts", func(t *testing.T) {
// Empty can be used where a codec is required but not actually used
unitCodec := Empty[Void, Void, any](
lazy.Of(pair.MakePair(F.VOID, F.VOID)),
)
result := unitCodec.Decode("ignored")
assert.Equal(t, validation.Of(F.VOID), result)
encoded := unitCodec.Encode(F.VOID)
assert.Equal(t, F.VOID, encoded)
})
}
// TestEmpty_RoundTrip tests that Empty maintains consistency
func TestEmpty_RoundTrip(t *testing.T) {
t.Run("decode then encode returns default output", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("output", 42)))
// Decode
result := defaultCodec.Decode("input")
require.True(t, either.IsRight(result))
decoded := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
// Encode
encoded := defaultCodec.Encode(decoded)
// Should get default output, not related to decoded value
assert.Equal(t, "output", encoded)
})
t.Run("multiple round trips are consistent", func(t *testing.T) {
defaultCodec := Empty[int, int, any](lazy.Of(pair.MakePair(100, 50)))
// First round trip
result1 := defaultCodec.Decode("input1")
decoded1 := either.MonadFold(result1,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
encoded1 := defaultCodec.Encode(decoded1)
// Second round trip
result2 := defaultCodec.Decode("input2")
decoded2 := either.MonadFold(result2,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
encoded2 := defaultCodec.Encode(decoded2)
// All decoded values should be the same
assert.Equal(t, 50, decoded1)
assert.Equal(t, 50, decoded2)
// All encoded values should be the same
assert.Equal(t, 100, encoded1)
assert.Equal(t, 100, encoded2)
})
}

View File

@@ -19,12 +19,7 @@ func TestDo(t *testing.T) {
decoder := Do[string](State{})
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{}, value)
assert.Equal(t, validation.Of(State{}), result)
})
t.Run("creates decoder with initialized state", func(t *testing.T) {
@@ -79,12 +74,7 @@ func TestBind(t *testing.T) {
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 42, y: 10}, value)
assert.Equal(t, validation.Of(State{x: 42, y: 10}), result)
})
t.Run("propagates failure", func(t *testing.T) {
@@ -216,12 +206,7 @@ func TestLet(t *testing.T) {
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
assert.Equal(t, validation.Of(State{x: 60, y: 10, z: 20}), result)
})
}

View File

@@ -18,6 +18,47 @@ func Of[I, A any](a A) Decode[I, A] {
return readereither.Of[I, Errors](a)
}
// OfLazy converts a lazy computation into a Decode that ignores its input.
// The resulting Decode will evaluate the lazy computation when executed and wrap
// the result in a successful validation, regardless of the input provided.
//
// This function is intended solely for deferring the computation of a value, NOT for
// representing side effects. The lazy computation should be a pure function that
// produces the same result each time it's called (referential transparency). For
// operations with side effects, use appropriate effect types like IO or IOResult.
//
// This is useful for lifting deferred computations into the Decode context without
// requiring access to the input, while maintaining the validation wrapper for consistency.
//
// Type Parameters:
// - I: The input type (ignored by the resulting Decode)
// - A: The result type produced by the lazy computation
//
// Parameters:
// - fa: A lazy computation that produces a value of type A (must be pure, no side effects)
//
// Returns:
// - A Decode that ignores its input, evaluates the lazy computation, and wraps the result in Validation[A]
//
// Example:
//
// lazyValue := func() int { return 42 }
// decoder := decode.OfLazy[string](lazyValue)
// result := decoder("any input") // validation.Success(42)
//
// Example - Deferring expensive computation:
//
// expensiveCalc := func() Config {
// // Expensive but pure computation here
// return computeDefaultConfig()
// }
// decoder := decode.OfLazy[map[string]any](expensiveCalc)
// // Computation is deferred until the Decode is executed
// result := decoder(inputData) // validation.Success(config)
func OfLazy[I, A any](fa Lazy[A]) Decode[I, A] {
return readereither.OfLazy[I, Errors](fa)
}
// Left creates a Decode that always fails with the given validation errors.
// This is the dual of Of - while Of lifts a success value, Left lifts failure errors
// into the Decode context.

View File

@@ -51,6 +51,108 @@ func TestOf(t *testing.T) {
})
}
// TestOfLazy tests the OfLazy function
func TestOfLazy(t *testing.T) {
t.Run("evaluates lazy computation ignoring input", func(t *testing.T) {
lazyValue := func() int { return 42 }
decoder := OfLazy[string](lazyValue)
res := decoder("any input")
assert.Equal(t, validation.Of(42), res)
})
t.Run("defers computation until Decode is executed", func(t *testing.T) {
executed := false
lazyComputation := func() string {
executed = true
return "computed"
}
decoder := OfLazy[string](lazyComputation)
// Computation should not be executed yet
assert.False(t, executed, "lazy computation should not be executed during Decode creation")
// Execute the Decode
res := decoder("input")
// Now computation should be executed
assert.True(t, executed, "lazy computation should be executed when Decode runs")
assert.Equal(t, validation.Of("computed"), res)
})
t.Run("evaluates lazy computation each time Decode is called", func(t *testing.T) {
counter := 0
lazyCounter := func() int {
counter++
return counter
}
decoder := OfLazy[string](lazyCounter)
// First execution
res1 := decoder("input")
assert.Equal(t, validation.Of(1), res1)
// Second execution
res2 := decoder("input")
assert.Equal(t, validation.Of(2), res2)
// Third execution
res3 := decoder("input")
assert.Equal(t, validation.Of(3), res3)
})
t.Run("works with different types", func(t *testing.T) {
lazyString := func() string { return "hello" }
decoder1 := OfLazy[int](lazyString)
assert.Equal(t, validation.Of("hello"), decoder1(123))
lazySlice := func() []int { return []int{1, 2, 3} }
decoder2 := OfLazy[string](lazySlice)
assert.Equal(t, validation.Of([]int{1, 2, 3}), decoder2("input"))
type Person struct {
Name string
Age int
}
lazyStruct := func() Person { return Person{Name: "Alice", Age: 30} }
decoder3 := OfLazy[map[string]any](lazyStruct)
assert.Equal(t, validation.Of(Person{Name: "Alice", Age: 30}), decoder3(map[string]any{}))
})
t.Run("can be composed with other Decode operations", func(t *testing.T) {
lazyValue := func() int { return 10 }
decoder := MonadMap(
OfLazy[string](lazyValue),
func(x int) int { return x * 2 },
)
res := decoder("input")
assert.Equal(t, validation.Of(20), res)
})
t.Run("ignores input completely", func(t *testing.T) {
lazyValue := func() string { return "constant" }
decoder := OfLazy[string](lazyValue)
// Different inputs should produce same result
res1 := decoder("input1")
res2 := decoder("input2")
assert.Equal(t, validation.Of("constant"), res1)
assert.Equal(t, validation.Of("constant"), res2)
assert.Equal(t, res1, res2)
})
t.Run("always wraps result in success validation", func(t *testing.T) {
lazyValue := func() int { return 42 }
decoder := OfLazy[string](lazyValue)
res := decoder("input")
// Verify it's a successful validation
assert.True(t, either.IsRight(res))
assert.Equal(t, validation.Of(42), res)
})
}
// TestLeft tests the Left function
func TestLeft(t *testing.T) {
t.Run("creates decoder that always fails", func(t *testing.T) {

View File

@@ -2,6 +2,7 @@ package codec
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/formatting"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
@@ -10,12 +11,15 @@ 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"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerresult"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/semigroup"
)
type (
@@ -338,4 +342,156 @@ 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]
// Semigroup represents an algebraic structure with an associative binary operation.
//
// A Semigroup[A] provides:
// - Concat(A, A): Combines two values associatively
//
// Semigroup law:
// - Associativity: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
//
// Unlike Monoid, Semigroup does not require an identity element (Empty).
// This makes it more general but less powerful for certain operations.
//
// In the codec context, semigroups are used to:
// - Combine validation errors
// - Merge partial results
// - Aggregate codec outputs
//
// Example semigroups:
// - String concatenation (without empty string)
// - Array concatenation (without empty array)
// - Error accumulation
//
// Note: Every Monoid is also a Semigroup, but not every Semigroup is a Monoid.
Semigroup[A any] = semigroup.Semigroup[A]
// Void represents a unit type with a single value.
//
// Void is used instead of struct{} to represent:
// - Unit values in functional programming
// - Placeholder types where no meaningful value is needed
// - Return types for functions that produce no useful result
//
// The single value of type Void is VOID (function.VOID).
//
// Usage:
// - Use function.Void (or F.Void) as the type
// - Use function.VOID (or F.VOID) as the value
//
// Example:
// unitCodec := codec.Empty[F.Void, F.Void, any](
// lazy.Of(pair.MakePair(F.VOID, F.VOID)),
// )
//
// Benefits over struct{}:
// - More explicit intent (unit type vs empty struct)
// - Consistent with functional programming conventions
// - Better semantic meaning in type signatures
//
// See also:
// - function.VOID: The single value of type Void
// - Empty: Codec function that uses Void for unit types
Void = function.Void
)

View File

@@ -170,12 +170,7 @@ func TestLet(t *testing.T) {
)
result := validator("input")(nil)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 5, computed: 10}, value)
assert.Equal(t, validation.Of(State{x: 5, computed: 10}), result)
})
t.Run("preserves failure", func(t *testing.T) {
@@ -218,12 +213,7 @@ func TestLet(t *testing.T) {
)
result := validator("input")(nil)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
assert.Equal(t, validation.Of(State{x: 60, y: 10, z: 20}), result)
})
}

View File

@@ -160,6 +160,109 @@ func Of[I, A any](a A) Validate[I, A] {
return reader.Of[I](decode.Of[Context](a))
}
// OfLazy creates a Validate that defers the computation of a value until needed.
//
// This function lifts a lazy computation into the validation context. The computation
// is deferred until the validator is actually executed, allowing for efficient handling
// of expensive operations or values that may not always be needed.
//
// **IMPORTANT**: The lazy function MUST be pure (referentially transparent). It should
// always return the same value when called and must not perform side effects. For
// computations with side effects, use IO or IOEither types instead.
//
// # Type Parameters
//
// - I: The input type (not used, but required for type consistency)
// - A: The type of the value produced by the lazy computation
//
// # Parameters
//
// - fa: A lazy computation that produces a value of type A. This function is called
// each time the validator is executed.
//
// # Returns
//
// A Validate[I, A] that ignores its input and returns a successful validation containing
// the lazily computed value.
//
// # Purity Requirements
//
// The lazy function MUST be pure:
// - Always returns the same result for the same (lack of) input
// - No side effects (no I/O, no mutation, no randomness)
// - Deterministic and referentially transparent
//
// For side effects, use:
// - IO types for effectful computations
// - IOEither for effectful computations that may fail
//
// # Example: Deferring Expensive Computation
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// )
//
// // Expensive computation deferred until needed
// expensiveValue := validate.OfLazy[string, int](func() int {
// // This computation only runs when the validator is executed
// return computeExpensiveValue()
// })
//
// result := expensiveValue("any input")(nil)
// // result is validation.Success(computed value)
//
// # Example: Lazy Default Value
//
// // Provide a default value that's only computed if needed
// withDefault := validate.OfLazy[Config, Config](func() Config {
// return loadDefaultConfig()
// })
//
// // Use in a validation pipeline
// validator := F.Pipe1(
// validateFromFile,
// validate.Alt(func() validate.Validate[string, Config] {
// return withDefault
// }),
// )
// // Default config only loaded if file validation fails
//
// # Example: Composition with Other Validators
//
// // Combine lazy value with validation logic
// lazyValidator := F.Pipe1(
// validate.OfLazy[string, int](func() int { return 42 }),
// validate.Chain(func(n int) validate.Validate[string, string] {
// return func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if len(input) > n {
// return validation.FailureWithMessage[string](input, "too long")(ctx)
// }
// return validation.Success(input)
// }
// }
// }),
// )
//
// # Notes
//
// - The lazy function is evaluated each time the validator is executed
// - The input value I is ignored; the validator succeeds regardless of input
// - The result is always wrapped in a successful validation
// - This is useful for deferring expensive computations or providing lazy defaults
// - The lazy function must be pure - no side effects allowed
// - For side effects, use IO or IOEither types instead
//
// # See Also
//
// - Of: For non-lazy values
// - decode.OfLazy: The underlying decode operation
// - reader.Of: The reader lifting operation
func OfLazy[I, A any](fa Lazy[A]) Validate[I, A] {
return reader.Of[I](decode.OfLazy[Context](fa))
}
// MonadMap applies a function to the successful result of a validation.
//
// This is the functor map operation for Validate. It transforms the success value

View File

@@ -1274,3 +1274,139 @@ func TestOrElse(t *testing.T) {
}
})
}
// TestOfLazy tests the OfLazy function
func TestOfLazy(t *testing.T) {
t.Run("evaluates lazy computation", func(t *testing.T) {
// Create a validator with a lazy value
validator := OfLazy[string, int](func() int {
return 42
})
result := validator("any input")(nil)
assert.Equal(t, validation.Success(42), result)
})
t.Run("defers execution until called", func(t *testing.T) {
executed := false
validator := OfLazy[string, int](func() int {
executed = true
return 100
})
// Lazy function not executed yet
assert.False(t, executed)
// Execute the validator
result := validator("input")(nil)
// Now it should be executed
assert.True(t, executed)
assert.Equal(t, validation.Success(100), result)
})
t.Run("evaluates on each call", func(t *testing.T) {
callCount := 0
validator := OfLazy[string, int](func() int {
callCount++
return callCount
})
// First call
result1 := validator("input")(nil)
assert.Equal(t, validation.Success(1), result1)
// Second call - evaluates again
result2 := validator("input")(nil)
assert.Equal(t, validation.Success(2), result2)
// Third call
result3 := validator("input")(nil)
assert.Equal(t, validation.Success(3), result3)
})
t.Run("works with different types", func(t *testing.T) {
// String type
stringValidator := OfLazy[int, string](func() string {
return "hello"
})
result := stringValidator(42)(nil)
assert.Equal(t, validation.Success("hello"), result)
// Struct type
type Config struct {
Host string
Port int
}
configValidator := OfLazy[string, Config](func() Config {
return Config{Host: "localhost", Port: 8080}
})
result2 := configValidator("input")(nil)
assert.Equal(t, validation.Success(Config{Host: "localhost", Port: 8080}), result2)
// Slice type
sliceValidator := OfLazy[string, []int](func() []int {
return []int{1, 2, 3}
})
result3 := sliceValidator("input")(nil)
assert.Equal(t, validation.Success([]int{1, 2, 3}), result3)
})
t.Run("composes with other validators", func(t *testing.T) {
// Create a lazy validator that produces a number
lazyValue := OfLazy[string, int](func() int {
return 42
})
// Map to transform the value
validator := MonadMap(lazyValue, func(n int) int {
return n * 2
})
result := validator("any input")(nil)
assert.Equal(t, validation.Success(84), result)
})
t.Run("ignores input value", func(t *testing.T) {
validator := OfLazy[string, int](func() int {
return 999
})
// Different inputs should produce the same result
result1 := validator("input1")(nil)
result2 := validator("input2")(nil)
result3 := validator("")(nil)
assert.Equal(t, validation.Success(999), result1)
assert.Equal(t, validation.Success(999), result2)
assert.Equal(t, validation.Success(999), result3)
})
t.Run("always wraps in success validation", func(t *testing.T) {
validator := OfLazy[string, int](func() int {
return 42
})
result := validator("input")(nil)
// Verify it's a Right (success)
assert.True(t, E.IsRight(result))
// Extract and verify the value
value, _ := E.Unwrap(result)
assert.Equal(t, 42, value)
})
t.Run("works with context", func(t *testing.T) {
validator := OfLazy[string, string](func() string {
return "validated"
})
ctx := validation.Context{
{Key: "field", Type: "string"},
}
result := validator("input")(ctx)
assert.Equal(t, validation.Success("validated"), result)
})
}

View File

@@ -227,6 +227,51 @@ func Of[R, A any](a A) Reader[R, A] {
return function.Constant1[R](a)
}
// OfLazy converts a lazy computation into a Reader that ignores its environment.
// The resulting Reader will evaluate the lazy computation when executed, regardless
// of the environment provided.
//
// This function is intended solely for deferring the computation of a value, NOT for
// representing side effects. The lazy computation should be a pure function that
// produces the same result each time it's called (referential transparency). For
// operations with side effects, use appropriate effect types like IO or IOEither.
//
// This is useful for lifting deferred computations into the Reader context without
// requiring access to the environment.
//
// Type Parameters:
// - R: The environment type (ignored by the resulting Reader)
// - A: The result type produced by the lazy computation
//
// Parameters:
// - fa: A lazy computation that produces a value of type A (must be pure, no side effects)
//
// Returns:
// - A Reader that ignores its environment and evaluates the lazy computation
//
// Example:
//
// type Config struct { Host string }
// lazyValue := func() int { return 42 }
// r := reader.OfLazy[Config](lazyValue)
// result := r(Config{Host: "localhost"}) // 42
//
// Example - Deferring expensive computation:
//
// type Env struct { Debug bool }
// expensiveCalc := func() string {
// // Expensive but pure computation here
// return "computed result"
// }
// r := reader.OfLazy[Env](expensiveCalc)
// // Computation is deferred until the Reader is executed
// result := r(Env{Debug: true}) // "computed result"
func OfLazy[R, A any](fa Lazy[A]) Reader[R, A] {
return func(_ R) A {
return fa()
}
}
// MonadChain sequences two Reader computations where the second depends on the result of the first.
// Both computations share the same environment.
// This is the monadic bind operation (flatMap).

View File

@@ -92,6 +92,91 @@ func TestOf(t *testing.T) {
assert.Equal(t, "constant", result)
}
func TestOfLazy(t *testing.T) {
t.Run("evaluates lazy computation ignoring environment", func(t *testing.T) {
lazyValue := func() int { return 42 }
r := OfLazy[Config](lazyValue)
result := r(Config{Host: "localhost", Port: 8080})
assert.Equal(t, 42, result)
})
t.Run("defers computation until Reader is executed", func(t *testing.T) {
executed := false
lazyComputation := func() string {
executed = true
return "computed"
}
r := OfLazy[Config](lazyComputation)
// Computation should not be executed yet
assert.False(t, executed, "lazy computation should not be executed during Reader creation")
// Execute the Reader
result := r(Config{Host: "localhost"})
// Now computation should be executed
assert.True(t, executed, "lazy computation should be executed when Reader runs")
assert.Equal(t, "computed", result)
})
t.Run("evaluates lazy computation each time Reader is called", func(t *testing.T) {
counter := 0
lazyCounter := func() int {
counter++
return counter
}
r := OfLazy[Config](lazyCounter)
// First execution
result1 := r(Config{Host: "localhost"})
assert.Equal(t, 1, result1)
// Second execution
result2 := r(Config{Host: "localhost"})
assert.Equal(t, 2, result2)
// Third execution
result3 := r(Config{Host: "localhost"})
assert.Equal(t, 3, result3)
})
t.Run("works with different types", func(t *testing.T) {
lazyString := func() string { return "hello" }
r1 := OfLazy[Config](lazyString)
assert.Equal(t, "hello", r1(Config{}))
lazySlice := func() []int { return []int{1, 2, 3} }
r2 := OfLazy[Config](lazySlice)
assert.Equal(t, []int{1, 2, 3}, r2(Config{}))
lazyStruct := func() Config { return Config{Host: "test", Port: 9000} }
r3 := OfLazy[string](lazyStruct)
assert.Equal(t, Config{Host: "test", Port: 9000}, r3("ignored"))
})
t.Run("can be composed with other Reader operations", func(t *testing.T) {
lazyValue := func() int { return 10 }
r := F.Pipe1(
OfLazy[Config](lazyValue),
Map[Config](func(x int) int { return x * 2 }),
)
result := r(Config{Host: "localhost"})
assert.Equal(t, 20, result)
})
t.Run("ignores environment completely", func(t *testing.T) {
lazyValue := func() string { return "constant" }
r := OfLazy[Config](lazyValue)
// Different environments should produce same result
config1 := Config{Host: "host1", Port: 8080}
config2 := Config{Host: "host2", Port: 9090}
assert.Equal(t, "constant", r(config1))
assert.Equal(t, "constant", r(config2))
})
}
func TestChain(t *testing.T) {
config := Config{Port: 8080}
getPort := Asks(func(c Config) int { return c.Port })

View File

@@ -103,4 +103,6 @@ type (
// Seq represents an iterator sequence over values of type T.
Seq[T any] = iter.Seq[T]
Lazy[A any] = func() A
)

View File

@@ -23,6 +23,7 @@ import (
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/reader"
)
@@ -46,6 +47,13 @@ func Right[E, L, A any](r A) ReaderEither[E, L, A] {
return eithert.Right(reader.Of[E, Either[L, A]], r)
}
func OfLazy[E, L, A any](r Lazy[A]) ReaderEither[E, L, A] {
return reader.OfLazy[E](function.Pipe1(
r,
lazy.Map(ET.Of[L, A]),
))
}
func FromReader[L, E, A any](r Reader[E, A]) ReaderEither[E, L, A] {
return RightReader[L](r)
}

View File

@@ -23,10 +23,14 @@ import (
)
type (
// Lazy represents a deferred computation that produces a value of type A.
Lazy[A any] = lazy.Lazy[A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
// Either represents a value of one of two possible types (a disjoint union).
// An instance of Either is either Left (representing an error) or Right (representing a success).
Either[E, A any] = either.Either[E, A]
// Reader represents a computation that depends on an environment R and produces a value A.
@@ -34,9 +38,9 @@ type (
// ReaderEither represents a computation that depends on an environment R and can fail
// with an error E or succeed with a value A.
// It combines Reader (dependency injection) with Either (error handling).
// It combines the Reader monad (for dependency injection) with the Either monad (for error handling).
ReaderEither[R, E, A any] = Reader[R, Either[E, A]]
// Kleisli represents a Kleisli arrow for the ReaderEither monad.
// It's a function from A to ReaderEither[R, E, B], used for composing operations that
// depend on an environment and may fail.
@@ -44,7 +48,6 @@ type (
// Operator represents a function that transforms one ReaderEither into another.
// It takes a ReaderEither[R, E, A] and produces a ReaderEither[R, E, B].
// This is commonly used for lifting functions into the ReaderEither context.
Operator[R, E, A, B any] = Kleisli[R, E, ReaderEither[R, E, A], B]
Lazy[A any] = lazy.Lazy[A]
)

View File

@@ -29,6 +29,7 @@ import (
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/result"
)
@@ -279,6 +280,49 @@ func Of[R, A any](a A) ReaderResult[R, A] {
return readert.MonadOf[ReaderResult[R, A]](ET.Of[error, A], a)
}
// OfLazy converts a lazy computation into a ReaderResult that ignores its environment.
// The resulting ReaderResult will evaluate the lazy computation when executed and wrap
// the result in a successful Result, regardless of the environment provided.
//
// This function is intended solely for deferring the computation of a value, NOT for
// representing side effects. The lazy computation should be a pure function that
// produces the same result each time it's called (referential transparency). For
// operations with side effects, use appropriate effect types like IO or IOResult.
//
// This is useful for lifting deferred computations into the ReaderResult context without
// requiring access to the environment, while maintaining the Result wrapper for consistency.
//
// Type Parameters:
// - R: The environment type (ignored by the resulting ReaderResult)
// - A: The result type produced by the lazy computation
//
// Parameters:
// - r: A lazy computation that produces a value of type A (must be pure, no side effects)
//
// Returns:
// - A ReaderResult that ignores its environment, evaluates the lazy computation, and wraps the result in Result[A]
//
// Example:
//
// type Config struct { Host string }
// lazyValue := func() int { return 42 }
// rr := readerresult.OfLazy[Config](lazyValue)
// result := rr(Config{Host: "localhost"}) // result.Of(42)
//
// Example - Deferring expensive computation:
//
// type Env struct { Debug bool }
// expensiveCalc := func() string {
// // Expensive but pure computation here
// return "computed result"
// }
// rr := readerresult.OfLazy[Env](expensiveCalc)
// // Computation is deferred until the ReaderResult is executed
// result := rr(Env{Debug: true}) // result.Of("computed result")
func OfLazy[R, A any](r Lazy[A]) ReaderResult[R, A] {
return readereither.OfLazy[R, error](r)
}
// MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.
// Both computations share the same environment. This is useful for combining independent
// computations that don't depend on each other's results.

View File

@@ -77,6 +77,101 @@ func TestOf(t *testing.T) {
assert.Equal(t, result.Of(42), rr(defaultContext))
}
func TestOfLazy(t *testing.T) {
t.Run("evaluates lazy computation ignoring environment", func(t *testing.T) {
lazyValue := func() int { return 42 }
rr := OfLazy[MyContext](lazyValue)
res := rr(defaultContext)
assert.Equal(t, result.Of(42), res)
})
t.Run("defers computation until ReaderResult is executed", func(t *testing.T) {
executed := false
lazyComputation := func() string {
executed = true
return "computed"
}
rr := OfLazy[MyContext](lazyComputation)
// Computation should not be executed yet
assert.False(t, executed, "lazy computation should not be executed during ReaderResult creation")
// Execute the ReaderResult
res := rr(defaultContext)
// Now computation should be executed
assert.True(t, executed, "lazy computation should be executed when ReaderResult runs")
assert.Equal(t, result.Of("computed"), res)
})
t.Run("evaluates lazy computation each time ReaderResult is called", func(t *testing.T) {
counter := 0
lazyCounter := func() int {
counter++
return counter
}
rr := OfLazy[MyContext](lazyCounter)
// First execution
res1 := rr(defaultContext)
assert.Equal(t, result.Of(1), res1)
// Second execution
res2 := rr(defaultContext)
assert.Equal(t, result.Of(2), res2)
// Third execution
res3 := rr(defaultContext)
assert.Equal(t, result.Of(3), res3)
})
t.Run("works with different types", func(t *testing.T) {
lazyString := func() string { return "hello" }
rr1 := OfLazy[MyContext](lazyString)
assert.Equal(t, result.Of("hello"), rr1(defaultContext))
lazySlice := func() []int { return []int{1, 2, 3} }
rr2 := OfLazy[MyContext](lazySlice)
assert.Equal(t, result.Of([]int{1, 2, 3}), rr2(defaultContext))
lazyStruct := func() MyContext { return "test" }
rr3 := OfLazy[string](lazyStruct)
assert.Equal(t, result.Of(MyContext("test")), rr3("ignored"))
})
t.Run("can be composed with other ReaderResult operations", func(t *testing.T) {
lazyValue := func() int { return 10 }
rr := F.Pipe1(
OfLazy[MyContext](lazyValue),
Map[MyContext](func(x int) int { return x * 2 }),
)
res := rr(defaultContext)
assert.Equal(t, result.Of(20), res)
})
t.Run("ignores environment completely", func(t *testing.T) {
lazyValue := func() string { return "constant" }
rr := OfLazy[MyContext](lazyValue)
// Different environments should produce same result
ctx1 := MyContext("context1")
ctx2 := MyContext("context2")
assert.Equal(t, result.Of("constant"), rr(ctx1))
assert.Equal(t, result.Of("constant"), rr(ctx2))
})
t.Run("always wraps result in success", func(t *testing.T) {
lazyValue := func() int { return 42 }
rr := OfLazy[MyContext](lazyValue)
res := rr(defaultContext)
// Verify it's a successful Result
assert.True(t, result.IsRight(res))
assert.Equal(t, result.Of(42), res)
})
}
func TestFromReader(t *testing.T) {
r := func(ctx MyContext) string { return string(ctx) }
rr := FromReader(r)

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)
}
}