1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-25 22:21:49 +02:00

fix: better tests and doc

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Dr. Carsten Leue
2025-11-12 16:24:12 +01:00
parent af271e7d10
commit ca813b673c
14 changed files with 2523 additions and 517 deletions

View File

@@ -15,14 +15,163 @@
package bytes
// Empty returns an empty byte slice.
//
// This function returns the identity element for the byte slice Monoid,
// which is an empty byte slice. It's useful as a starting point for
// building byte slices or as a default value.
//
// Returns:
// - An empty byte slice ([]byte{})
//
// Properties:
// - Empty() is the identity element for Monoid.Concat
// - Monoid.Concat(Empty(), x) == x
// - Monoid.Concat(x, Empty()) == x
//
// Example - Basic usage:
//
// empty := Empty()
// fmt.Println(len(empty)) // 0
//
// Example - As identity element:
//
// data := []byte("hello")
// result1 := Monoid.Concat(Empty(), data) // []byte("hello")
// result2 := Monoid.Concat(data, Empty()) // []byte("hello")
//
// Example - Building byte slices:
//
// // Start with empty and build up
// buffer := Empty()
// buffer = Monoid.Concat(buffer, []byte("Hello"))
// buffer = Monoid.Concat(buffer, []byte(" "))
// buffer = Monoid.Concat(buffer, []byte("World"))
// // buffer: []byte("Hello World")
//
// See also:
// - Monoid.Empty(): Alternative way to get empty byte slice
// - ConcatAll(): For concatenating multiple byte slices
func Empty() []byte {
return Monoid.Empty()
}
// ToString converts a byte slice to a string.
//
// This function performs a direct conversion from []byte to string.
// The conversion creates a new string with a copy of the byte data.
//
// Parameters:
// - a: The byte slice to convert
//
// Returns:
// - A string containing the same data as the byte slice
//
// Performance Note:
//
// This conversion allocates a new string. For performance-critical code
// that needs to avoid allocations, consider using unsafe.String (Go 1.20+)
// or working directly with byte slices.
//
// Example - Basic conversion:
//
// bytes := []byte("hello")
// str := ToString(bytes)
// fmt.Println(str) // "hello"
//
// Example - Converting binary data:
//
// // ASCII codes for "Hello"
// data := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
// str := ToString(data)
// fmt.Println(str) // "Hello"
//
// Example - Empty byte slice:
//
// empty := Empty()
// str := ToString(empty)
// fmt.Println(str == "") // true
//
// Example - UTF-8 encoded text:
//
// utf8Bytes := []byte("Hello, 世界")
// str := ToString(utf8Bytes)
// fmt.Println(str) // "Hello, 世界"
//
// Example - Round-trip conversion:
//
// original := "test string"
// bytes := []byte(original)
// result := ToString(bytes)
// fmt.Println(original == result) // true
//
// See also:
// - []byte(string): For converting string to byte slice
// - Size(): For getting the length of a byte slice
func ToString(a []byte) string {
return string(a)
}
// Size returns the number of bytes in a byte slice.
//
// This function returns the length of the byte slice, which is the number
// of bytes it contains. This is equivalent to len(as) but provided as a
// named function for use in functional composition.
//
// Parameters:
// - as: The byte slice to measure
//
// Returns:
// - The number of bytes in the slice
//
// Example - Basic usage:
//
// data := []byte("hello")
// size := Size(data)
// fmt.Println(size) // 5
//
// Example - Empty slice:
//
// empty := Empty()
// size := Size(empty)
// fmt.Println(size) // 0
//
// Example - Binary data:
//
// binary := []byte{0x01, 0x02, 0x03, 0x04}
// size := Size(binary)
// fmt.Println(size) // 4
//
// Example - UTF-8 encoded text:
//
// // Note: Size returns byte count, not character count
// utf8 := []byte("Hello, 世界")
// byteCount := Size(utf8)
// fmt.Println(byteCount) // 13 (not 9 characters)
//
// Example - Using in functional composition:
//
// import "github.com/IBM/fp-go/v2/array"
//
// slices := [][]byte{
// []byte("a"),
// []byte("bb"),
// []byte("ccc"),
// }
//
// // Map to get sizes
// sizes := array.Map(Size)(slices)
// // sizes: []int{1, 2, 3}
//
// Example - Checking if slice is empty:
//
// data := []byte("test")
// isEmpty := Size(data) == 0
// fmt.Println(isEmpty) // false
//
// See also:
// - len(): Built-in function for getting slice length
// - ToString(): For converting byte slice to string
func Size(as []byte) int {
return len(as)
}

View File

@@ -187,6 +187,299 @@ func TestOrd(t *testing.T) {
})
}
// TestOrdProperties tests mathematical properties of Ord
func TestOrdProperties(t *testing.T) {
t.Run("reflexivity: x == x", func(t *testing.T) {
testCases := [][]byte{
[]byte{},
[]byte("a"),
[]byte("test"),
[]byte{0x01, 0x02, 0x03},
}
for _, tc := range testCases {
assert.Equal(t, 0, Ord.Compare(tc, tc),
"Compare(%v, %v) should be 0", tc, tc)
assert.True(t, Ord.Equals(tc, tc),
"Equals(%v, %v) should be true", tc, tc)
}
})
t.Run("antisymmetry: if x <= y and y <= x then x == y", func(t *testing.T) {
testCases := []struct {
a, b []byte
}{
{[]byte("abc"), []byte("abc")},
{[]byte{}, []byte{}},
{[]byte{0x01}, []byte{0x01}},
}
for _, tc := range testCases {
cmp1 := Ord.Compare(tc.a, tc.b)
cmp2 := Ord.Compare(tc.b, tc.a)
if cmp1 <= 0 && cmp2 <= 0 {
assert.True(t, Ord.Equals(tc.a, tc.b),
"If %v <= %v and %v <= %v, they should be equal", tc.a, tc.b, tc.b, tc.a)
}
}
})
t.Run("transitivity: if x <= y and y <= z then x <= z", func(t *testing.T) {
x := []byte("a")
y := []byte("b")
z := []byte("c")
cmpXY := Ord.Compare(x, y)
cmpYZ := Ord.Compare(y, z)
cmpXZ := Ord.Compare(x, z)
if cmpXY <= 0 && cmpYZ <= 0 {
assert.True(t, cmpXZ <= 0,
"If %v <= %v and %v <= %v, then %v <= %v", x, y, y, z, x, z)
}
})
t.Run("totality: either x <= y or y <= x", func(t *testing.T) {
testCases := []struct {
a, b []byte
}{
{[]byte("abc"), []byte("abd")},
{[]byte("xyz"), []byte("abc")},
{[]byte{}, []byte("a")},
{[]byte{0x01}, []byte{0x02}},
}
for _, tc := range testCases {
cmp1 := Ord.Compare(tc.a, tc.b)
cmp2 := Ord.Compare(tc.b, tc.a)
assert.True(t, cmp1 <= 0 || cmp2 <= 0,
"Either %v <= %v or %v <= %v must be true", tc.a, tc.b, tc.b, tc.a)
}
})
}
// TestEdgeCases tests edge cases and boundary conditions
func TestEdgeCases(t *testing.T) {
t.Run("very large byte slices", func(t *testing.T) {
large := make([]byte, 1000000)
for i := range large {
large[i] = byte(i % 256)
}
size := Size(large)
assert.Equal(t, 1000000, size)
str := ToString(large)
assert.Equal(t, 1000000, len(str))
})
t.Run("concatenating many slices", func(t *testing.T) {
slices := make([][]byte, 100)
for i := range slices {
slices[i] = []byte{byte(i)}
}
result := ConcatAll(slices...)
assert.Equal(t, 100, Size(result))
})
t.Run("null bytes in slice", func(t *testing.T) {
data := []byte{0x00, 0x01, 0x00, 0x02}
size := Size(data)
assert.Equal(t, 4, size)
str := ToString(data)
assert.Equal(t, 4, len(str))
})
t.Run("comparing slices with null bytes", func(t *testing.T) {
a := []byte{0x00, 0x01}
b := []byte{0x00, 0x02}
assert.Equal(t, -1, Ord.Compare(a, b))
})
}
// TestMonoidConcatPerformance tests concatenation performance characteristics
func TestMonoidConcatPerformance(t *testing.T) {
t.Run("ConcatAll vs repeated Concat", func(t *testing.T) {
slices := [][]byte{
[]byte("a"),
[]byte("b"),
[]byte("c"),
[]byte("d"),
[]byte("e"),
}
// Using ConcatAll
result1 := ConcatAll(slices...)
// Using repeated Concat
result2 := Monoid.Empty()
for _, s := range slices {
result2 = Monoid.Concat(result2, s)
}
assert.Equal(t, result1, result2)
assert.Equal(t, []byte("abcde"), result1)
})
}
// TestRoundTrip tests round-trip conversions
func TestRoundTrip(t *testing.T) {
t.Run("string to bytes to string", func(t *testing.T) {
original := "Hello, World! 世界"
bytes := []byte(original)
result := ToString(bytes)
assert.Equal(t, original, result)
})
t.Run("bytes to string to bytes", func(t *testing.T) {
original := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
str := ToString(original)
result := []byte(str)
assert.Equal(t, original, result)
})
}
// TestConcatAllVariadic tests ConcatAll with various argument counts
func TestConcatAllVariadic(t *testing.T) {
t.Run("zero arguments", func(t *testing.T) {
result := ConcatAll()
assert.Equal(t, []byte{}, result)
})
t.Run("one argument", func(t *testing.T) {
result := ConcatAll([]byte("test"))
assert.Equal(t, []byte("test"), result)
})
t.Run("two arguments", func(t *testing.T) {
result := ConcatAll([]byte("hello"), []byte("world"))
assert.Equal(t, []byte("helloworld"), result)
})
t.Run("many arguments", func(t *testing.T) {
result := ConcatAll(
[]byte("a"),
[]byte("b"),
[]byte("c"),
[]byte("d"),
[]byte("e"),
[]byte("f"),
[]byte("g"),
[]byte("h"),
[]byte("i"),
[]byte("j"),
)
assert.Equal(t, []byte("abcdefghij"), result)
})
}
// Benchmark tests
func BenchmarkToString(b *testing.B) {
data := []byte("Hello, World!")
b.Run("small", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ToString(data)
}
})
b.Run("large", func(b *testing.B) {
large := make([]byte, 10000)
for i := range large {
large[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToString(large)
}
})
}
func BenchmarkSize(b *testing.B) {
data := []byte("Hello, World!")
for i := 0; i < b.N; i++ {
_ = Size(data)
}
}
func BenchmarkMonoidConcat(b *testing.B) {
a := []byte("Hello")
c := []byte(" World")
b.Run("small slices", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Monoid.Concat(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Monoid.Concat(large1, large2)
}
})
}
func BenchmarkConcatAll(b *testing.B) {
slices := [][]byte{
[]byte("Hello"),
[]byte(" "),
[]byte("World"),
[]byte("!"),
}
b.Run("few slices", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ConcatAll(slices...)
}
})
b.Run("many slices", func(b *testing.B) {
many := make([][]byte, 100)
for i := range many {
many[i] = []byte{byte(i)}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ConcatAll(many...)
}
})
}
func BenchmarkOrdCompare(b *testing.B) {
a := []byte("abc")
c := []byte("abd")
b.Run("equal", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Ord.Compare(a, a)
}
})
b.Run("different", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Ord.Compare(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
large2[9999] = 1
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Ord.Compare(large1, large2)
}
})
}
// Example tests
func ExampleEmpty() {
empty := Empty()
@@ -219,3 +512,17 @@ func ExampleConcatAll() {
// Output:
}
func ExampleMonoid_concat() {
result := Monoid.Concat([]byte("Hello"), []byte(" World"))
println(string(result)) // Hello World
// Output:
}
func ExampleOrd_compare() {
cmp := Ord.Compare([]byte("abc"), []byte("abd"))
println(cmp) // -1 (abc < abd)
// Output:
}

4
v2/bytes/coverage.out Normal file
View File

@@ -0,0 +1,4 @@
mode: set
github.com/IBM/fp-go/v2/bytes/bytes.go:55.21,57.2 1 1
github.com/IBM/fp-go/v2/bytes/bytes.go:111.32,113.2 1 1
github.com/IBM/fp-go/v2/bytes/bytes.go:175.26,177.2 1 1

View File

@@ -23,12 +23,219 @@ import (
)
var (
// monoid for byte arrays
// Monoid is the Monoid instance for byte slices.
//
// This Monoid combines byte slices through concatenation, with an empty
// byte slice as the identity element. It satisfies the monoid laws:
//
// Identity laws:
// - Monoid.Concat(Monoid.Empty(), x) == x (left identity)
// - Monoid.Concat(x, Monoid.Empty()) == x (right identity)
//
// Associativity law:
// - Monoid.Concat(Monoid.Concat(a, b), c) == Monoid.Concat(a, Monoid.Concat(b, c))
//
// Operations:
// - Empty(): Returns an empty byte slice []byte{}
// - Concat(a, b []byte): Concatenates two byte slices
//
// Example - Basic concatenation:
//
// result := Monoid.Concat([]byte("Hello"), []byte(" World"))
// // result: []byte("Hello World")
//
// Example - Identity element:
//
// empty := Monoid.Empty()
// data := []byte("test")
// result1 := Monoid.Concat(empty, data) // []byte("test")
// result2 := Monoid.Concat(data, empty) // []byte("test")
//
// Example - Building byte buffers:
//
// buffer := Monoid.Empty()
// buffer = Monoid.Concat(buffer, []byte("Line 1\n"))
// buffer = Monoid.Concat(buffer, []byte("Line 2\n"))
// buffer = Monoid.Concat(buffer, []byte("Line 3\n"))
//
// Example - Associativity:
//
// a := []byte("a")
// b := []byte("b")
// c := []byte("c")
// left := Monoid.Concat(Monoid.Concat(a, b), c) // []byte("abc")
// right := Monoid.Concat(a, Monoid.Concat(b, c)) // []byte("abc")
// // left == right
//
// See also:
// - ConcatAll: For concatenating multiple byte slices at once
// - Empty(): Convenience function for getting empty byte slice
Monoid = A.Monoid[byte]()
// ConcatAll concatenates all bytes
// ConcatAll efficiently concatenates multiple byte slices into a single slice.
//
// This function takes a variadic number of byte slices and combines them
// into a single byte slice. It pre-allocates the exact amount of memory
// needed, making it more efficient than repeated concatenation.
//
// Parameters:
// - slices: Zero or more byte slices to concatenate
//
// Returns:
// - A new byte slice containing all input slices concatenated in order
//
// Performance:
//
// ConcatAll is more efficient than using Monoid.Concat repeatedly because
// it calculates the total size upfront and allocates memory once, avoiding
// multiple allocations and copies.
//
// Example - Basic usage:
//
// result := ConcatAll(
// []byte("Hello"),
// []byte(" "),
// []byte("World"),
// )
// // result: []byte("Hello World")
//
// Example - Empty input:
//
// result := ConcatAll()
// // result: []byte{}
//
// Example - Single slice:
//
// result := ConcatAll([]byte("test"))
// // result: []byte("test")
//
// Example - Building protocol messages:
//
// import "encoding/binary"
//
// header := []byte{0x01, 0x02}
// length := make([]byte, 4)
// binary.BigEndian.PutUint32(length, 100)
// payload := []byte("data")
// footer := []byte{0xFF}
//
// message := ConcatAll(header, length, payload, footer)
//
// Example - With empty slices:
//
// result := ConcatAll(
// []byte("a"),
// []byte{},
// []byte("b"),
// []byte{},
// []byte("c"),
// )
// // result: []byte("abc")
//
// Example - Building CSV line:
//
// fields := [][]byte{
// []byte("John"),
// []byte("Doe"),
// []byte("30"),
// }
// separator := []byte(",")
//
// // Interleave fields with separators
// parts := [][]byte{
// fields[0], separator,
// fields[1], separator,
// fields[2],
// }
// line := ConcatAll(parts...)
// // line: []byte("John,Doe,30")
//
// See also:
// - Monoid.Concat: For concatenating exactly two byte slices
// - bytes.Join: Standard library function for joining with separator
ConcatAll = A.ArrayConcatAll[byte]
// Ord implements the default ordering on bytes
// Ord is the Ord instance for byte slices providing lexicographic ordering.
//
// This Ord instance compares byte slices lexicographically (dictionary order),
// comparing bytes from left to right until a difference is found or one slice
// ends. It uses the standard library's bytes.Compare and bytes.Equal functions.
//
// Comparison rules:
// - Compares byte-by-byte from left to right
// - First differing byte determines the order
// - Shorter slice is less than longer slice if all bytes match
// - Empty slice is less than any non-empty slice
//
// Operations:
// - Compare(a, b []byte) int: Returns -1 if a < b, 0 if a == b, 1 if a > b
// - Equals(a, b []byte) bool: Returns true if slices are equal
//
// Example - Basic comparison:
//
// cmp := Ord.Compare([]byte("abc"), []byte("abd"))
// // cmp: -1 (abc < abd)
//
// cmp = Ord.Compare([]byte("xyz"), []byte("abc"))
// // cmp: 1 (xyz > abc)
//
// cmp = Ord.Compare([]byte("test"), []byte("test"))
// // cmp: 0 (equal)
//
// Example - Length differences:
//
// cmp := Ord.Compare([]byte("ab"), []byte("abc"))
// // cmp: -1 (shorter is less)
//
// cmp = Ord.Compare([]byte("abc"), []byte("ab"))
// // cmp: 1 (longer is greater)
//
// Example - Empty slices:
//
// cmp := Ord.Compare([]byte{}, []byte("a"))
// // cmp: -1 (empty is less)
//
// cmp = Ord.Compare([]byte{}, []byte{})
// // cmp: 0 (both empty)
//
// Example - Equality check:
//
// equal := Ord.Equals([]byte("test"), []byte("test"))
// // equal: true
//
// equal = Ord.Equals([]byte("test"), []byte("Test"))
// // equal: false (case-sensitive)
//
// Example - Sorting byte slices:
//
// import "github.com/IBM/fp-go/v2/array"
//
// data := [][]byte{
// []byte("zebra"),
// []byte("apple"),
// []byte("mango"),
// }
//
// sorted := array.Sort(Ord)(data)
// // sorted: [[]byte("apple"), []byte("mango"), []byte("zebra")]
//
// Example - Binary data comparison:
//
// cmp := Ord.Compare([]byte{0x01, 0x02}, []byte{0x01, 0x03})
// // cmp: -1 (0x02 < 0x03)
//
// Example - Finding minimum:
//
// import O "github.com/IBM/fp-go/v2/ord"
//
// a := []byte("xyz")
// b := []byte("abc")
// min := O.Min(Ord)(a, b)
// // min: []byte("abc")
//
// See also:
// - bytes.Compare: Standard library comparison function
// - bytes.Equal: Standard library equality function
// - array.Sort: For sorting slices using an Ord instance
Ord = O.MakeOrd(bytes.Compare, bytes.Equal)
)

View File

@@ -15,7 +15,84 @@
package eq
// Contramap implements an Equals predicate based on a mapping
// Contramap creates an Eq[B] from an Eq[A] by providing a function that maps B to A.
// This is a contravariant functor operation that allows you to transform equality predicates
// by mapping the input type. It's particularly useful for comparing complex types by
// extracting comparable fields.
//
// The name "contramap" comes from category theory, where it represents a contravariant
// functor. Unlike regular map (covariant), which transforms the output, contramap
// transforms the input in the opposite direction.
//
// Type Parameters:
// - A: The type that has an existing Eq instance
// - B: The type for which we want to create an Eq instance
//
// Parameters:
// - f: A function that extracts or converts a value of type B to type A
//
// Returns:
// - A function that takes an Eq[A] and returns an Eq[B]
//
// The resulting Eq[B] compares two B values by:
// 1. Applying f to both values to get A values
// 2. Using the original Eq[A] to compare those A values
//
// Example - Compare structs by a single field:
//
// type Person struct {
// ID int
// Name string
// Age int
// }
//
// // Compare persons by ID only
// personEqByID := eq.Contramap(func(p Person) int {
// return p.ID
// })(eq.FromStrictEquals[int]())
//
// p1 := Person{ID: 1, Name: "Alice", Age: 30}
// p2 := Person{ID: 1, Name: "Bob", Age: 25}
// assert.True(t, personEqByID.Equals(p1, p2)) // Same ID, different names
//
// Example - Case-insensitive string comparison:
//
// type User struct {
// Username string
// Email string
// }
//
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
//
// userEqByUsername := eq.Contramap(func(u User) string {
// return u.Username
// })(caseInsensitiveEq)
//
// u1 := User{Username: "Alice", Email: "alice@example.com"}
// u2 := User{Username: "ALICE", Email: "different@example.com"}
// assert.True(t, userEqByUsername.Equals(u1, u2)) // Case-insensitive match
//
// Example - Nested field access:
//
// type Address struct {
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Compare persons by city
// personEqByCity := eq.Contramap(func(p Person) string {
// return p.Address.City
// })(eq.FromStrictEquals[string]())
//
// Contramap Law:
// Contramap must satisfy: Contramap(f)(Contramap(g)(eq)) = Contramap(g ∘ f)(eq)
// This means contramapping twice is the same as contramapping with the composed function.
func Contramap[A, B any](f func(b B) A) func(Eq[A]) Eq[B] {
return func(fa Eq[A]) Eq[B] {
equals := fa.Equals

View File

@@ -19,38 +19,188 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// Eq represents an equality type class for type T.
// It provides a way to define custom equality semantics for any type,
// not just those that are comparable with Go's == operator.
//
// Type Parameters:
// - T: The type for which equality is defined
//
// Methods:
// - Equals(x, y T) bool: Returns true if x and y are considered equal
//
// Laws:
// An Eq instance must satisfy the equivalence relation laws:
// 1. Reflexivity: Equals(x, x) = true for all x
// 2. Symmetry: Equals(x, y) = Equals(y, x) for all x, y
// 3. Transitivity: If Equals(x, y) and Equals(y, z), then Equals(x, z)
//
// Example:
//
// // Create an equality predicate for integers
// intEq := eq.FromStrictEquals[int]()
// assert.True(t, intEq.Equals(42, 42))
// assert.False(t, intEq.Equals(42, 43))
//
// // Create a custom equality predicate
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
// assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
type Eq[T any] interface {
// Equals returns true if x and y are considered equal according to this equality predicate.
//
// Parameters:
// - x: The first value to compare
// - y: The second value to compare
//
// Returns:
// - true if x and y are equal, false otherwise
Equals(x, y T) bool
}
// eq is the internal implementation of the Eq interface.
// It wraps a comparison function to provide the Eq interface.
type eq[T any] struct {
c func(x, y T) bool
}
// Equals implements the Eq interface by delegating to the wrapped comparison function.
func (e eq[T]) Equals(x, y T) bool {
return e.c(x, y)
}
// strictEq is a helper function that uses Go's built-in == operator for comparison.
// It can only be used with comparable types.
func strictEq[A comparable](a, b A) bool {
return a == b
}
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
// FromStrictEquals constructs an Eq instance using Go's built-in == operator.
// This is the most common way to create an Eq for types that support ==.
//
// Type Parameters:
// - T: Must be a comparable type (supports ==)
//
// Returns:
// - An Eq[T] that uses == for equality comparison
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// assert.True(t, intEq.Equals(42, 42))
// assert.False(t, intEq.Equals(42, 43))
//
// stringEq := eq.FromStrictEquals[string]()
// assert.True(t, stringEq.Equals("hello", "hello"))
// assert.False(t, stringEq.Equals("hello", "world"))
//
// Note: For types that are not comparable or require custom equality logic,
// use FromEquals instead.
func FromStrictEquals[T comparable]() Eq[T] {
return FromEquals(strictEq[T])
}
// FromEquals constructs an [EQ.Eq] from the comparison function
// FromEquals constructs an Eq instance from a custom comparison function.
// This allows defining equality for any type, including non-comparable types
// or types that need custom equality semantics.
//
// Type Parameters:
// - T: The type for which equality is being defined (can be any type)
//
// Parameters:
// - c: A function that takes two values of type T and returns true if they are equal
//
// Returns:
// - An Eq[T] that uses the provided function for equality comparison
//
// Example:
//
// // Case-insensitive string equality
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
// assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
//
// // Approximate float equality
// approxEq := eq.FromEquals(func(a, b float64) bool {
// return math.Abs(a-b) < 0.0001
// })
// assert.True(t, approxEq.Equals(1.0, 1.00009))
//
// // Custom struct equality (compare by specific fields)
// type Person struct { ID int; Name string }
// personEq := eq.FromEquals(func(a, b Person) bool {
// return a.ID == b.ID // Compare only by ID
// })
//
// Note: The provided function should satisfy the equivalence relation laws
// (reflexivity, symmetry, transitivity) for correct behavior.
func FromEquals[T any](c func(x, y T) bool) Eq[T] {
return eq[T]{c: c}
}
// Empty returns the equals predicate that is always true
// Empty returns an Eq instance that always returns true for any comparison.
// This is the identity element for the Eq Monoid and is useful when you need
// an equality predicate that accepts everything.
//
// Type Parameters:
// - T: The type for which the always-true equality is defined
//
// Returns:
// - An Eq[T] where Equals(x, y) always returns true
//
// Example:
//
// alwaysTrue := eq.Empty[int]()
// assert.True(t, alwaysTrue.Equals(1, 2))
// assert.True(t, alwaysTrue.Equals(42, 100))
//
// // Useful as identity in monoid operations
// monoid := eq.Monoid[string]()
// combined := monoid.Concat(eq.FromStrictEquals[string](), monoid.Empty())
// // combined behaves the same as FromStrictEquals
//
// Use cases:
// - As the identity element in Monoid operations
// - When you need a placeholder equality that accepts everything
// - In generic code that requires an Eq but doesn't need actual comparison
func Empty[T any]() Eq[T] {
return FromEquals(F.Constant2[T, T](true))
}
// Equals returns a predicate to test if one value equals the other under an equals predicate
// Equals returns a curried equality checking function.
// This is useful for partial application and functional composition.
//
// Type Parameters:
// - T: The type being compared
//
// Parameters:
// - eq: The Eq instance to use for comparison
//
// Returns:
// - A function that takes a value and returns another function that checks equality with that value
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// equals42 := eq.Equals(intEq)(42)
//
// assert.True(t, equals42(42))
// assert.False(t, equals42(43))
//
// // Use in higher-order functions
// numbers := []int{40, 41, 42, 43, 44}
// filtered := array.Filter(equals42)(numbers)
// // filtered = [42]
//
// // Partial application
// equalsFunc := eq.Equals(intEq)
// equals10 := equalsFunc(10)
// equals20 := equalsFunc(20)
//
// This is particularly useful when working with functional programming patterns
// like map, filter, and other higher-order functions.
func Equals[T any](eq Eq[T]) func(T) func(T) bool {
return func(other T) func(T) bool {
return F.Bind2nd(eq.Equals, other)

View File

@@ -20,6 +20,65 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Semigroup returns a Semigroup instance for Eq[A].
// A Semigroup provides a way to combine two values of the same type.
// For Eq, the combination uses logical AND - two values are equal only if
// they are equal according to BOTH equality predicates.
//
// Type Parameters:
// - A: The type for which equality predicates are being combined
//
// Returns:
// - A Semigroup[Eq[A]] that combines equality predicates with logical AND
//
// The Concat operation satisfies:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Example - Combine multiple equality checks:
//
// type User struct {
// Username string
// Email string
// }
//
// usernameEq := eq.Contramap(func(u User) string {
// return u.Username
// })(eq.FromStrictEquals[string]())
//
// emailEq := eq.Contramap(func(u User) string {
// return u.Email
// })(eq.FromStrictEquals[string]())
//
// // Users are equal only if BOTH username AND email match
// userEq := eq.Semigroup[User]().Concat(usernameEq, emailEq)
//
// u1 := User{Username: "alice", Email: "alice@example.com"}
// u2 := User{Username: "alice", Email: "alice@example.com"}
// u3 := User{Username: "alice", Email: "different@example.com"}
//
// assert.True(t, userEq.Equals(u1, u2)) // Both match
// assert.False(t, userEq.Equals(u1, u3)) // Email differs
//
// Example - Combine multiple field checks:
//
// type Product struct {
// ID int
// Name string
// Price float64
// }
//
// idEq := eq.Contramap(func(p Product) int { return p.ID })(eq.FromStrictEquals[int]())
// nameEq := eq.Contramap(func(p Product) string { return p.Name })(eq.FromStrictEquals[string]())
// priceEq := eq.Contramap(func(p Product) float64 { return p.Price })(eq.FromStrictEquals[float64]())
//
// sg := eq.Semigroup[Product]()
// // All three fields must match
// productEq := sg.Concat(sg.Concat(idEq, nameEq), priceEq)
//
// Use cases:
// - Combining multiple field comparisons for struct equality
// - Building complex equality predicates from simpler ones
// - Ensuring all conditions are met (logical AND of predicates)
func Semigroup[A any]() S.Semigroup[Eq[A]] {
return S.MakeSemigroup(func(x, y Eq[A]) Eq[A] {
return FromEquals(func(a, b A) bool {
@@ -28,6 +87,67 @@ func Semigroup[A any]() S.Semigroup[Eq[A]] {
})
}
// Monoid returns a Monoid instance for Eq[A].
// A Monoid extends Semigroup with an identity element (Empty).
// For Eq, the identity is an equality predicate that always returns true.
//
// Type Parameters:
// - A: The type for which the equality monoid is defined
//
// Returns:
// - A Monoid[Eq[A]] with:
// - Concat: Combines equality predicates with logical AND (from Semigroup)
// - Empty: An equality predicate that always returns true (identity element)
//
// Monoid Laws:
// 1. Left Identity: Concat(Empty(), x) = x
// 2. Right Identity: Concat(x, Empty()) = x
// 3. Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Example - Using the identity element:
//
// monoid := eq.Monoid[int]()
// intEq := eq.FromStrictEquals[int]()
//
// // Empty is the identity - combining with it doesn't change behavior
// leftIdentity := monoid.Concat(monoid.Empty(), intEq)
// rightIdentity := monoid.Concat(intEq, monoid.Empty())
//
// assert.True(t, leftIdentity.Equals(42, 42))
// assert.False(t, leftIdentity.Equals(42, 43))
// assert.True(t, rightIdentity.Equals(42, 42))
// assert.False(t, rightIdentity.Equals(42, 43))
//
// Example - Empty always returns true:
//
// monoid := eq.Monoid[string]()
// alwaysTrue := monoid.Empty()
//
// assert.True(t, alwaysTrue.Equals("hello", "world"))
// assert.True(t, alwaysTrue.Equals("same", "same"))
// assert.True(t, alwaysTrue.Equals("", "anything"))
//
// Example - Building complex equality with fold:
//
// type Person struct {
// FirstName string
// LastName string
// Age int
// }
//
// firstNameEq := eq.Contramap(func(p Person) string { return p.FirstName })(eq.FromStrictEquals[string]())
// lastNameEq := eq.Contramap(func(p Person) string { return p.LastName })(eq.FromStrictEquals[string]())
// ageEq := eq.Contramap(func(p Person) int { return p.Age })(eq.FromStrictEquals[int]())
//
// monoid := eq.Monoid[Person]()
// // Combine all predicates - all fields must match
// personEq := monoid.Concat(monoid.Concat(firstNameEq, lastNameEq), ageEq)
//
// Use cases:
// - Providing a neutral element for equality combinations
// - Generic algorithms that require a Monoid instance
// - Folding multiple equality predicates into one
// - Default "accept everything" equality predicate
func Monoid[A any]() M.Monoid[Eq[A]] {
return M.MakeMonoid(Semigroup[A]().Concat, Empty[A]())
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,105 @@
package function
// Flip reverses the order of parameters of a curried function
// Flip reverses the order of parameters of a curried function.
//
// Given a curried function f that takes T1 then T2 and returns R,
// Flip returns a new curried function that takes T2 then T1 and returns R.
// This is useful when you have a curried function but need to apply its
// arguments in a different order.
//
// Mathematical notation:
// - Given: f: T1 → T2 → R
// - Returns: g: T2 → T1 → R where g(t2)(t1) = f(t1)(t2)
//
// Type Parameters:
// - T1: The type of the first parameter (becomes second after flip)
// - T2: The type of the second parameter (becomes first after flip)
// - R: The return type
//
// Parameters:
// - f: A curried function taking T1 then T2 and returning R
//
// Returns:
// - A new curried function taking T2 then T1 and returning R
//
// Relationship to Swap:
//
// Flip is the curried version of Swap. While Swap works with binary functions,
// Flip works with curried functions:
// - Swap: func(T1, T2) R → func(T2, T1) R
// - Flip: func(T1) func(T2) R → func(T2) func(T1) R
//
// Example - Basic usage:
//
// // Create a curried division function
// divide := Curry2(func(a, b float64) float64 { return a / b })
// // divide(10)(2) = 5.0 (10 / 2)
//
// // Flip the parameter order
// divideFlipped := Flip(divide)
// // divideFlipped(10)(2) = 0.2 (2 / 10)
//
// Example - String formatting:
//
// // Curried string formatter: format(template)(value)
// format := Curry2(func(template, value string) string {
// return fmt.Sprintf(template, value)
// })
//
// // Normal order: template first, then value
// result1 := format("Hello, %s!")("World") // "Hello, World!"
//
// // Flipped order: value first, then template
// formatFlipped := Flip(format)
// result2 := formatFlipped("Hello, %s!")("World") // "Hello, World!"
//
// // Useful for partial application in different order
// greetWorld := format("Hello, %s!")
// greetWorld("Alice") // "Hello, Alice!"
//
// formatAlice := formatFlipped("Alice")
// formatAlice("Hello, %s!") // "Hello, Alice!"
//
// Example - Practical use case with map operations:
//
// // Curried map lookup: getFrom(map)(key)
// getFrom := Curry2(func(m map[string]int, key string) int {
// return m[key]
// })
//
// data := map[string]int{"a": 1, "b": 2, "c": 3}
//
// // Create a getter for this specific map
// getValue := getFrom(data)
// getValue("a") // 1
//
// // Flip to create key-first version: get(key)(map)
// get := Flip(getFrom)
// getA := get("a")
// getA(data) // 1
//
// Example - Combining with other functional patterns:
//
// // Curried append: append(slice)(element)
// appendTo := Curry2(func(slice []int, elem int) []int {
// return append(slice, elem)
// })
//
// // Flip to get: prepend(element)(slice)
// prepend := Flip(appendTo)
//
// nums := []int{1, 2, 3}
// add4 := appendTo(nums)
// result1 := add4(4) // [1, 2, 3, 4]
//
// prependZero := prepend(0)
// result2 := prependZero(nums) // [1, 2, 3, 0]
//
// See also:
// - Swap: For flipping parameters of non-curried binary functions
// - Curry2: For converting binary functions to curried form
// - Uncurry2: For converting curried functions back to binary form
func Flip[T1, T2, R any](f func(T1) func(T2) R) func(T2) func(T1) R {
return func(t2 T2) func(T1) R {
return func(t1 T1) R {

View File

@@ -22,15 +22,265 @@ import (
"github.com/stretchr/testify/assert"
)
// TestFlip tests the Flip function with various scenarios
func TestFlip(t *testing.T) {
t.Run("flips string concatenation", func(t *testing.T) {
// Create a curried function that formats strings
format := Curry2(func(a, b string) string {
return fmt.Sprintf("%s:%s", a, b)
})
x := Curry2(func(a, b string) string {
return fmt.Sprintf("%s:%s", a, b)
// Original order: a then b
assert.Equal(t, "a:b", format("a")("b"))
assert.Equal(t, "hello:world", format("hello")("world"))
// Flipped order: b then a
flipped := Flip(format)
assert.Equal(t, "b:a", flipped("a")("b"))
assert.Equal(t, "world:hello", flipped("hello")("world"))
})
assert.Equal(t, "a:b", x("a")("b"))
t.Run("flips numeric operations", func(t *testing.T) {
// Curried subtraction: subtract(a)(b) = a - b
subtract := Curry2(func(a, b int) int {
return a - b
})
y := Flip(x)
// Original: 10 - 3 = 7
assert.Equal(t, 7, subtract(10)(3))
assert.Equal(t, "b:a", y("a")("b"))
// Flipped: 3 - 10 = -7
flipped := Flip(subtract)
assert.Equal(t, -7, flipped(10)(3))
})
t.Run("flips division", func(t *testing.T) {
// Curried division: divide(a)(b) = a / b
divide := Curry2(func(a, b float64) float64 {
return a / b
})
// Original: 10 / 2 = 5.0
assert.Equal(t, 5.0, divide(10)(2))
// Flipped: 2 / 10 = 0.2
flipped := Flip(divide)
assert.Equal(t, 0.2, flipped(10)(2))
})
t.Run("flips with partial application", func(t *testing.T) {
// Curried append-like operation
prepend := Curry2(func(prefix, text string) string {
return prefix + text
})
// Create specialized functions with original order
addHello := prepend("Hello, ")
assert.Equal(t, "Hello, World", addHello("World"))
assert.Equal(t, "Hello, Go", addHello("Go"))
// Flip and create specialized functions with reversed order
flipped := Flip(prepend)
addToWorld := flipped("World")
assert.Equal(t, "Hello, World", addToWorld("Hello, "))
assert.Equal(t, "Goodbye, World", addToWorld("Goodbye, "))
})
t.Run("flips with different types", func(t *testing.T) {
// Curried function with different input types
repeat := Curry2(func(s string, n int) string {
result := ""
for i := 0; i < n; i++ {
result += s
}
return result
})
// Original: repeat("x")(3) = "xxx"
assert.Equal(t, "xxx", repeat("x")(3))
assert.Equal(t, "abab", repeat("ab")(2))
// Flipped: repeat(3)("x") = "xxx"
flipped := Flip(repeat)
assert.Equal(t, "xxx", flipped(3)("x"))
assert.Equal(t, "abab", flipped(2)("ab"))
})
t.Run("double flip returns to original", func(t *testing.T) {
// Flipping twice should return to original behavior
original := Curry2(func(a, b string) string {
return a + "-" + b
})
flipped := Flip(original)
doubleFlipped := Flip(flipped)
// Original and double-flipped should behave the same
assert.Equal(t, original("a")("b"), doubleFlipped("a")("b"))
assert.Equal(t, "a-b", doubleFlipped("a")("b"))
})
t.Run("flips with complex types", func(t *testing.T) {
type Person struct {
Name string
Age int
}
// Curried function creating a person
makePerson := Curry2(func(name string, age int) Person {
return Person{Name: name, Age: age}
})
// Original order: name then age
alice := makePerson("Alice")(30)
assert.Equal(t, "Alice", alice.Name)
assert.Equal(t, 30, alice.Age)
// Flipped order: age then name
flipped := Flip(makePerson)
bob := flipped(25)("Bob")
assert.Equal(t, "Bob", bob.Name)
assert.Equal(t, 25, bob.Age)
})
t.Run("flips map operations", func(t *testing.T) {
// Curried map getter: get(map)(key)
get := Curry2(func(m map[string]int, key string) int {
return m[key]
})
data := map[string]int{"a": 1, "b": 2, "c": 3}
// Original: provide map first, then key
getValue := get(data)
assert.Equal(t, 1, getValue("a"))
assert.Equal(t, 2, getValue("b"))
// Flipped: provide key first, then map
flipped := Flip(get)
getA := flipped("a")
assert.Equal(t, 1, getA(data))
data2 := map[string]int{"a": 10, "b": 20}
assert.Equal(t, 10, getA(data2))
})
t.Run("flips boolean operations", func(t *testing.T) {
// Curried logical operation
implies := Curry2(func(a, b bool) bool {
return !a || b
})
// Test truth table for implication
assert.True(t, implies(true)(true)) // T → T = T
assert.False(t, implies(true)(false)) // T → F = F
assert.True(t, implies(false)(true)) // F → T = T
assert.True(t, implies(false)(false)) // F → F = T
// Flipped version (reverse implication)
flipped := Flip(implies)
assert.True(t, flipped(true)(true)) // T ← T = T
assert.True(t, flipped(true)(false)) // T ← F = T
assert.False(t, flipped(false)(true)) // F ← T = F
assert.True(t, flipped(false)(false)) // F ← F = T
})
t.Run("flips with slice operations", func(t *testing.T) {
// Curried slice append
appendTo := Curry2(func(slice []int, elem int) []int {
return append(slice, elem)
})
nums := []int{1, 2, 3}
// Original: provide slice first, then element
add4 := appendTo(nums)
result1 := add4(4)
assert.Equal(t, []int{1, 2, 3, 4}, result1)
// Flipped: provide element first, then slice
flipped := Flip(appendTo)
appendFive := flipped(5)
result2 := appendFive(nums)
assert.Equal(t, []int{1, 2, 3, 5}, result2)
})
}
// TestFlipProperties tests mathematical properties of Flip
func TestFlipProperties(t *testing.T) {
t.Run("flip is involutive (flip . flip = id)", func(t *testing.T) {
// Flipping twice should give back the original function behavior
original := Curry2(func(a, b int) int {
return a*10 + b
})
flipped := Flip(original)
doubleFlipped := Flip(flipped)
// Test with multiple inputs
testCases := []struct{ a, b int }{
{1, 2},
{5, 7},
{0, 0},
{-1, 3},
}
for _, tc := range testCases {
assert.Equal(t,
original(tc.a)(tc.b),
doubleFlipped(tc.a)(tc.b),
"flip(flip(f)) should equal f for inputs (%d, %d)", tc.a, tc.b)
}
})
t.Run("flip preserves function composition", func(t *testing.T) {
// If we have f: A → B → C and g: C → D
// then g ∘ f(a)(b) = g(f(a)(b))
// and g ∘ flip(f)(b)(a) = g(flip(f)(b)(a))
f := Curry2(func(a, b int) int {
return a + b
})
g := func(n int) int {
return n * 2
}
flippedF := Flip(f)
// Compose g with f
composed1 := func(a, b int) int {
return g(f(a)(b))
}
// Compose g with flipped f
composed2 := func(a, b int) int {
return g(flippedF(b)(a))
}
// Both should give the same result
assert.Equal(t, composed1(3, 5), composed2(3, 5))
assert.Equal(t, 16, composed1(3, 5)) // (3 + 5) * 2 = 16
})
}
// BenchmarkFlip benchmarks the Flip function
func BenchmarkFlip(b *testing.B) {
add := Curry2(func(a, b int) int {
return a + b
})
flipped := Flip(add)
b.Run("original", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = add(i)(i + 1)
}
})
b.Run("flipped", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = flipped(i)(i + 1)
}
})
}

233
v2/optics/iso/lens/doc.go Normal file
View File

@@ -0,0 +1,233 @@
// 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 lens provides conversions from isomorphisms to lenses.
# Overview
This package bridges the gap between isomorphisms (bidirectional transformations)
and lenses (focused accessors). Since every isomorphism can be viewed as a lens,
this package provides functions to perform that conversion.
An isomorphism Iso[S, A] represents a lossless bidirectional transformation between
types S and A. A lens Lens[S, A] provides focused access to a part A within a
structure S. Since an isomorphism can transform the entire structure S to A and back,
it naturally forms a lens that focuses on the "whole as a part".
# Mathematical Foundation
Given an Iso[S, A] with:
- Get: S → A (forward transformation)
- ReverseGet: A → S (reverse transformation)
We can construct a Lens[S, A] with:
- Get: S → A (same as iso's Get)
- Set: A → S → S (implemented as: a => s => ReverseGet(a))
The lens laws are automatically satisfied because the isomorphism laws guarantee:
1. GetSet: Set(Get(s))(s) == s (from iso's round-trip law)
2. SetGet: Get(Set(a)(s)) == a (from iso's inverse law)
3. SetSet: Set(a2)(Set(a1)(s)) == Set(a2)(s) (trivially true)
# Basic Usage
Converting an isomorphism to a lens:
type Celsius float64
type Kelvin float64
// Create an isomorphism between Celsius and Kelvin
celsiusKelvinIso := iso.MakeIso(
func(c Celsius) Kelvin { return Kelvin(c + 273.15) },
func(k Kelvin) Celsius { return Celsius(k - 273.15) },
)
// Convert to a lens
celsiusKelvinLens := lens.IsoAsLens(celsiusKelvinIso)
// Use as a lens
celsius := Celsius(20.0)
kelvin := celsiusKelvinLens.Get(celsius) // 293.15 K
updated := celsiusKelvinLens.Set(Kelvin(300))(celsius) // 26.85°C
# Working with Pointers
For pointer-based structures, use IsoAsLensRef:
type UserId int
type User struct {
id UserId
name string
}
// Isomorphism between User pointer and UserId
userIdIso := iso.MakeIso(
func(u *User) UserId { return u.id },
func(id UserId) *User { return &User{id: id, name: "Unknown"} },
)
// Convert to a reference lens
userIdLens := lens.IsoAsLensRef(userIdIso)
user := &User{id: 42, name: "Alice"}
id := userIdLens.Get(user) // 42
updated := userIdLens.Set(UserId(100))(user) // New user with id 100
# Use Cases
1. Type Wrappers: Convert between newtype wrappers and their underlying types
type Email string
type ValidatedEmail struct{ value Email }
emailIso := iso.MakeIso(
func(ve ValidatedEmail) Email { return ve.value },
func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
)
emailLens := lens.IsoAsLens(emailIso)
2. Unit Conversions: Work with different units of measurement
type Meters float64
type Feet float64
metersFeetIso := iso.MakeIso(
func(m Meters) Feet { return Feet(m * 3.28084) },
func(f Feet) Meters { return Meters(f / 3.28084) },
)
distanceLens := lens.IsoAsLens(metersFeetIso)
3. Encoding/Decoding: Transform between different representations
type JSON string
type Config struct {
Host string
Port int
}
// Assuming encode/decode functions exist
configIso := iso.MakeIso(encode, decode)
configLens := lens.IsoAsLens(configIso)
# Composition
Lenses created from isomorphisms can be composed with other lenses:
type Temperature struct {
celsius Celsius
}
// Lens to access celsius field
celsiusFieldLens := L.MakeLens(
func(t Temperature) Celsius { return t.celsius },
func(t Temperature, c Celsius) Temperature {
t.celsius = c
return t
},
)
// Compose with iso-based lens to work with Kelvin
tempKelvinLens := F.Pipe1(
celsiusFieldLens,
L.Compose[Temperature](celsiusKelvinLens),
)
temp := Temperature{celsius: 20}
kelvin := tempKelvinLens.Get(temp) // 293.15 K
updated := tempKelvinLens.Set(Kelvin(300))(temp) // 26.85°C
# Comparison with Direct Lenses
While you can create a lens directly, using an isomorphism provides benefits:
1. Reusability: The isomorphism can be used in multiple contexts
2. Bidirectionality: The inverse transformation is explicitly available
3. Type Safety: Isomorphism laws ensure correctness
4. Composability: Isomorphisms compose naturally
Direct lens approach requires defining both get and set operations separately,
while the isomorphism approach defines the bidirectional transformation once
and converts it to a lens when needed.
# Performance Considerations
Converting an isomorphism to a lens has minimal overhead. The resulting lens
simply delegates to the isomorphism's Get and ReverseGet functions. However,
keep in mind:
1. Each Set operation performs a full transformation via ReverseGet
2. For pointer types, use IsoAsLensRef to ensure proper copying
3. The lens ignores the original structure in Set, using only the new value
# Function Reference
Conversion Functions:
- IsoAsLens: Convert Iso[S, A] to Lens[S, A] for value types
- IsoAsLensRef: Convert Iso[*S, A] to Lens[*S, A] for pointer types
# Related Packages
- github.com/IBM/fp-go/v2/optics/iso: Isomorphisms (bidirectional transformations)
- github.com/IBM/fp-go/v2/optics/lens: Lenses (focused accessors)
- github.com/IBM/fp-go/v2/optics/lens/iso: Convert lenses to isomorphisms (inverse operation)
- github.com/IBM/fp-go/v2/endomorphism: Endomorphisms (A → A functions)
- github.com/IBM/fp-go/v2/function: Function composition utilities
# Examples
Complete example with type wrappers:
type UserId int
type Username string
type User struct {
id UserId
name Username
}
// Isomorphism for UserId
userIdIso := iso.MakeIso(
func(u User) UserId { return u.id },
func(id UserId) User { return User{id: id, name: "Unknown"} },
)
// Isomorphism for Username
usernameIso := iso.MakeIso(
func(u User) Username { return u.name },
func(name Username) User { return User{id: 0, name: name} },
)
// Convert to lenses
idLens := lens.IsoAsLens(userIdIso)
nameLens := lens.IsoAsLens(usernameIso)
user := User{id: 42, name: "Alice"}
// Access and modify through lenses
id := idLens.Get(user) // 42
name := nameLens.Get(user) // "Alice"
renamed := nameLens.Set("Bob")(user) // User{id: 0, name: "Bob"}
reidentified := idLens.Set(UserId(100))(user) // User{id: 100, name: "Unknown"}
Note: When using Set with iso-based lenses, the entire structure is replaced
via ReverseGet, so other fields may be reset to default values. For partial
updates, use regular lenses instead.
*/
package lens
// Made with Bob

View File

@@ -18,16 +18,15 @@ package lens
import (
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// IsoAsLens converts an `Iso` to a `Lens`
func IsoAsLens[S, A any](sa I.Iso[S, A]) L.Lens[S, A] {
func IsoAsLens[S, A any](sa Iso[S, A]) Lens[S, A] {
return L.MakeLensCurried(sa.Get, F.Flow2(sa.ReverseGet, F.Flow2(F.Constant1[S, S], EM.Of[func(S) S])))
}
// IsoAsLensRef converts an `Iso` to a `Lens`
func IsoAsLensRef[S, A any](sa I.Iso[*S, A]) L.Lens[*S, A] {
func IsoAsLensRef[S, A any](sa Iso[*S, A]) Lens[*S, A] {
return L.MakeLensRefCurried(sa.Get, F.Flow2(sa.ReverseGet, F.Flow2(F.Constant1[*S, *S], EM.Of[func(*S) *S])))
}

View File

@@ -0,0 +1,401 @@
// 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 lens
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
ISO "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
// Test types
type Celsius float64
type Fahrenheit float64
type UserId int
type User struct {
id UserId
name string
}
type Meters float64
type Feet float64
// TestIsoAsLensBasic tests basic functionality of IsoAsLens
func TestIsoAsLensBasic(t *testing.T) {
// Create an isomorphism between Celsius and Fahrenheit
celsiusToFahrenheit := func(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
fahrenheitToCelsius := func(f Fahrenheit) Celsius {
return Celsius((f - 32) * 5 / 9)
}
tempIso := ISO.MakeIso(celsiusToFahrenheit, fahrenheitToCelsius)
tempLens := IsoAsLens(tempIso)
t.Run("Get", func(t *testing.T) {
celsius := Celsius(20.0)
fahrenheit := tempLens.Get(celsius)
assert.InDelta(t, 68.0, float64(fahrenheit), 0.001)
})
t.Run("Set", func(t *testing.T) {
celsius := Celsius(20.0)
newFahrenheit := Fahrenheit(86.0)
updated := tempLens.Set(newFahrenheit)(celsius)
assert.InDelta(t, 30.0, float64(updated), 0.001)
})
t.Run("SetPreservesOriginal", func(t *testing.T) {
original := Celsius(20.0)
newFahrenheit := Fahrenheit(86.0)
_ = tempLens.Set(newFahrenheit)(original)
// Original should be unchanged
assert.Equal(t, Celsius(20.0), original)
})
}
// TestIsoAsLensRefBasic tests basic functionality of IsoAsLensRef
func TestIsoAsLensRefBasic(t *testing.T) {
// Create an isomorphism for User pointer and UserId
userToId := func(u *User) UserId {
return u.id
}
idToUser := func(id UserId) *User {
return &User{id: id, name: "Unknown"}
}
userIdIso := ISO.MakeIso(userToId, idToUser)
userIdLens := IsoAsLensRef(userIdIso)
t.Run("Get", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
id := userIdLens.Get(user)
assert.Equal(t, UserId(42), id)
})
t.Run("Set", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
newId := UserId(100)
updated := userIdLens.Set(newId)(user)
assert.Equal(t, UserId(100), updated.id)
assert.Equal(t, "Unknown", updated.name) // ReverseGet creates new user
})
t.Run("SetCreatesNewPointer", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
newId := UserId(100)
updated := userIdLens.Set(newId)(user)
// Should be different pointers
assert.NotSame(t, user, updated)
// Original should be unchanged
assert.Equal(t, UserId(42), user.id)
assert.Equal(t, "Alice", user.name)
})
}
// TestIsoAsLensLaws verifies that IsoAsLens satisfies lens laws
func TestIsoAsLensLaws(t *testing.T) {
// Create a simple isomorphism
type Wrapper struct{ value int }
wrapperIso := ISO.MakeIso(
func(w Wrapper) int { return w.value },
func(i int) Wrapper { return Wrapper{value: i} },
)
lens := IsoAsLens(wrapperIso)
wrapper := Wrapper{value: 42}
newValue := 100
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := lens.Set(lens.Get(wrapper))(wrapper)
assert.Equal(t, wrapper, result)
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := lens.Get(lens.Set(newValue)(wrapper))
assert.Equal(t, newValue, result)
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
result1 := lens.Set(200)(lens.Set(newValue)(wrapper))
result2 := lens.Set(200)(wrapper)
assert.Equal(t, result2, result1)
})
}
// TestIsoAsLensRefLaws verifies that IsoAsLensRef satisfies lens laws
func TestIsoAsLensRefLaws(t *testing.T) {
type Wrapper struct{ value int }
wrapperIso := ISO.MakeIso(
func(w *Wrapper) int { return w.value },
func(i int) *Wrapper { return &Wrapper{value: i} },
)
lens := IsoAsLensRef(wrapperIso)
wrapper := &Wrapper{value: 42}
newValue := 100
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := lens.Set(lens.Get(wrapper))(wrapper)
assert.Equal(t, wrapper.value, result.value)
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := lens.Get(lens.Set(newValue)(wrapper))
assert.Equal(t, newValue, result)
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
result1 := lens.Set(200)(lens.Set(newValue)(wrapper))
result2 := lens.Set(200)(wrapper)
assert.Equal(t, result2.value, result1.value)
})
}
// TestIsoAsLensComposition tests composing iso-based lenses with other lenses
func TestIsoAsLensComposition(t *testing.T) {
type Temperature struct {
celsius Celsius
}
// Lens to access celsius field
celsiusFieldLens := L.MakeLens(
func(t Temperature) Celsius { return t.celsius },
func(t Temperature, c Celsius) Temperature {
t.celsius = c
return t
},
)
// Isomorphism between Celsius and Fahrenheit
celsiusToFahrenheit := func(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
fahrenheitToCelsius := func(f Fahrenheit) Celsius {
return Celsius((f - 32) * 5 / 9)
}
tempIso := ISO.MakeIso(celsiusToFahrenheit, fahrenheitToCelsius)
tempLens := IsoAsLens(tempIso)
// Compose to work with Fahrenheit directly from Temperature
composedLens := F.Pipe1(
celsiusFieldLens,
L.Compose[Temperature](tempLens),
)
temp := Temperature{celsius: 20}
t.Run("ComposedGet", func(t *testing.T) {
fahrenheit := composedLens.Get(temp)
assert.InDelta(t, 68.0, float64(fahrenheit), 0.001)
})
t.Run("ComposedSet", func(t *testing.T) {
newFahrenheit := Fahrenheit(86.0)
updated := composedLens.Set(newFahrenheit)(temp)
assert.InDelta(t, 30.0, float64(updated.celsius), 0.001)
})
}
// TestIsoAsLensModify tests using Modify with iso-based lenses
func TestIsoAsLensModify(t *testing.T) {
// Isomorphism between Meters and Feet
metersToFeet := func(m Meters) Feet {
return Feet(m * 3.28084)
}
feetToMeters := func(f Feet) Meters {
return Meters(f / 3.28084)
}
distanceIso := ISO.MakeIso(metersToFeet, feetToMeters)
distanceLens := IsoAsLens(distanceIso)
meters := Meters(10.0)
t.Run("ModifyDouble", func(t *testing.T) {
// Double the distance in feet, result in meters
doubleFeet := func(f Feet) Feet { return f * 2 }
modified := L.Modify[Meters](doubleFeet)(distanceLens)(meters)
assert.InDelta(t, 20.0, float64(modified), 0.001)
})
t.Run("ModifyIdentity", func(t *testing.T) {
// Identity modification should return same value
identity := func(f Feet) Feet { return f }
modified := L.Modify[Meters](identity)(distanceLens)(meters)
assert.InDelta(t, float64(meters), float64(modified), 0.001)
})
}
// TestIsoAsLensWithIdentityIso tests that identity iso creates identity lens
func TestIsoAsLensWithIdentityIso(t *testing.T) {
type Value int
idIso := ISO.Id[Value]()
idLens := IsoAsLens(idIso)
value := Value(42)
t.Run("IdentityGet", func(t *testing.T) {
result := idLens.Get(value)
assert.Equal(t, value, result)
})
t.Run("IdentitySet", func(t *testing.T) {
newValue := Value(100)
result := idLens.Set(newValue)(value)
assert.Equal(t, newValue, result)
})
}
// TestIsoAsLensRefWithIdentityIso tests identity iso with references
func TestIsoAsLensRefWithIdentityIso(t *testing.T) {
type Value struct{ n int }
idIso := ISO.Id[*Value]()
idLens := IsoAsLensRef(idIso)
value := &Value{n: 42}
t.Run("IdentityGet", func(t *testing.T) {
result := idLens.Get(value)
assert.Equal(t, value, result)
})
t.Run("IdentitySet", func(t *testing.T) {
newValue := &Value{n: 100}
result := idLens.Set(newValue)(value)
assert.Equal(t, newValue, result)
})
}
// TestIsoAsLensRoundTrip tests round-trip conversions
func TestIsoAsLensRoundTrip(t *testing.T) {
type Email string
type ValidatedEmail struct{ value Email }
emailIso := ISO.MakeIso(
func(ve ValidatedEmail) Email { return ve.value },
func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
)
emailLens := IsoAsLens(emailIso)
validated := ValidatedEmail{value: "user@example.com"}
t.Run("RoundTripThroughGet", func(t *testing.T) {
// Get the email, then Set it back
email := emailLens.Get(validated)
restored := emailLens.Set(email)(validated)
assert.Equal(t, validated, restored)
})
t.Run("RoundTripThroughSet", func(t *testing.T) {
// Set a new email, then Get it
newEmail := Email("admin@example.com")
updated := emailLens.Set(newEmail)(validated)
retrieved := emailLens.Get(updated)
assert.Equal(t, newEmail, retrieved)
})
}
// TestIsoAsLensWithComplexTypes tests with more complex type transformations
func TestIsoAsLensWithComplexTypes(t *testing.T) {
type Point struct {
x, y float64
}
type PolarCoord struct {
r, theta float64
}
// Isomorphism between Cartesian and Polar coordinates (simplified for testing)
cartesianToPolar := func(p Point) PolarCoord {
r := p.x*p.x + p.y*p.y
theta := 0.0 // Simplified
return PolarCoord{r: r, theta: theta}
}
polarToCartesian := func(pc PolarCoord) Point {
return Point{x: pc.r, y: pc.theta} // Simplified
}
coordIso := ISO.MakeIso(cartesianToPolar, polarToCartesian)
coordLens := IsoAsLens(coordIso)
point := Point{x: 3.0, y: 4.0}
t.Run("ComplexGet", func(t *testing.T) {
polar := coordLens.Get(point)
assert.NotNil(t, polar)
})
t.Run("ComplexSet", func(t *testing.T) {
newPolar := PolarCoord{r: 5.0, theta: 0.927}
updated := coordLens.Set(newPolar)(point)
assert.NotNil(t, updated)
})
}
// TestIsoAsLensTypeConversion tests type conversion scenarios
func TestIsoAsLensTypeConversion(t *testing.T) {
type StringWrapper string
type IntWrapper int
// Isomorphism that converts string length to int
strLenIso := ISO.MakeIso(
func(s StringWrapper) IntWrapper { return IntWrapper(len(s)) },
func(i IntWrapper) StringWrapper {
// Create a string of given length (simplified)
result := ""
for j := 0; j < int(i); j++ {
result += "x"
}
return StringWrapper(result)
},
)
strLenLens := IsoAsLens(strLenIso)
t.Run("StringToLength", func(t *testing.T) {
str := StringWrapper("hello")
length := strLenLens.Get(str)
assert.Equal(t, IntWrapper(5), length)
})
t.Run("LengthToString", func(t *testing.T) {
str := StringWrapper("hello")
newLength := IntWrapper(3)
updated := strLenLens.Set(newLength)(str)
assert.Equal(t, 3, len(updated))
})
}
// Made with Bob

View File

@@ -0,0 +1,11 @@
package lens
import (
"github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
)
type (
Lens[S, A any] = L.Lens[S, A]
Iso[S, A any] = iso.Iso[S, A]
)