mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-23 10:11:43 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f7a6c0589 | ||
|
|
e7f78e1a33 |
@@ -190,6 +190,11 @@ func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
|
||||
return G.MonadReduce(fa, f, initial)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadReduceWithIndex[A, B any](fa []A, f func(int, B, A) B, initial B) B {
|
||||
return G.MonadReduceWithIndex(fa, f, initial)
|
||||
}
|
||||
|
||||
// Reduce folds an array from left to right, applying a function to accumulate a result.
|
||||
//
|
||||
// Example:
|
||||
|
||||
@@ -764,14 +764,14 @@ func TestFoldMap(t *testing.T) {
|
||||
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := From(1, 2, 3, 4)
|
||||
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
|
||||
result := FoldMap[int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
|
||||
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation", func(t *testing.T) {
|
||||
concatSemigroup := STR.Semigroup
|
||||
arr := From(1, 2, 3)
|
||||
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
|
||||
result := FoldMap[int](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
|
||||
assert.Equal(t, "123", result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,3 +177,255 @@ func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose is an alias for Local that emphasizes the composition aspect of consumer transformation.
|
||||
// It composes a preprocessing function with a consumer, creating a new consumer that applies
|
||||
// the function before consuming the value.
|
||||
//
|
||||
// This function is semantically identical to Local but uses terminology that may be more familiar
|
||||
// to developers coming from functional programming backgrounds where "compose" is a common operation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// The name "Compose" highlights that we're composing two operations:
|
||||
// 1. The transformation function f: R2 -> R1
|
||||
// 2. The consumer c: R1 -> ()
|
||||
//
|
||||
// Result: A composed consumer: R2 -> ()
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic composition:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Compose with a string-to-int parser
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Compose(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Composing multiple transformations:
|
||||
//
|
||||
// type Data struct {
|
||||
// Value string
|
||||
// }
|
||||
//
|
||||
// type Wrapper struct {
|
||||
// Data Data
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs strings
|
||||
// logString := func(s string) {
|
||||
// fmt.Println(s)
|
||||
// }
|
||||
//
|
||||
// // Compose transformations step by step
|
||||
// extractData := func(w Wrapper) Data { return w.Data }
|
||||
// extractValue := func(d Data) string { return d.Value }
|
||||
//
|
||||
// logData := consumer.Compose(extractValue)(logString)
|
||||
// logWrapper := consumer.Compose(extractData)(logData)
|
||||
//
|
||||
// logWrapper(Wrapper{Data: Data{Value: "Hello"}}) // Logs: "Hello"
|
||||
//
|
||||
// Example - Function composition style:
|
||||
//
|
||||
// // Compose is particularly useful when thinking in terms of function composition
|
||||
// type Request struct {
|
||||
// Body []byte
|
||||
// }
|
||||
//
|
||||
// // Consumer that processes strings
|
||||
// processString := func(s string) {
|
||||
// fmt.Printf("Processing: %s\n", s)
|
||||
// }
|
||||
//
|
||||
// // Compose byte-to-string conversion with processing
|
||||
// bytesToString := func(b []byte) string {
|
||||
// return string(b)
|
||||
// }
|
||||
// extractBody := func(r Request) []byte {
|
||||
// return r.Body
|
||||
// }
|
||||
//
|
||||
// // Chain compositions
|
||||
// processBytes := consumer.Compose(bytesToString)(processString)
|
||||
// processRequest := consumer.Compose(extractBody)(processBytes)
|
||||
//
|
||||
// processRequest(Request{Body: []byte("test")}) // Logs: "Processing: test"
|
||||
//
|
||||
// Relationship to Local:
|
||||
// - Compose and Local are identical in implementation
|
||||
// - Compose emphasizes the functional composition aspect
|
||||
// - Local emphasizes the environment/context transformation aspect
|
||||
// - Use Compose when thinking about function composition
|
||||
// - Use Local when thinking about adapting to different contexts
|
||||
//
|
||||
// Use Cases:
|
||||
// - Building processing pipelines with clear composition semantics
|
||||
// - Adapting consumers in a functional programming style
|
||||
// - Creating reusable consumer transformations
|
||||
// - Chaining multiple preprocessing steps
|
||||
func Compose[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return Local(f)
|
||||
}
|
||||
|
||||
// Contramap is the categorical name for the contravariant functor operation on Consumers.
|
||||
// It transforms a Consumer by preprocessing its input, making it the dual of the covariant
|
||||
// functor's map operation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#contravariant
|
||||
//
|
||||
// In category theory, a contravariant functor reverses the direction of morphisms.
|
||||
// While a covariant functor maps f: A -> B to map(f): F[A] -> F[B],
|
||||
// a contravariant functor maps f: A -> B to contramap(f): F[B] -> F[A].
|
||||
//
|
||||
// For Consumers:
|
||||
// - Consumer[A] is contravariant in A
|
||||
// - Given f: R2 -> R1, contramap(f) transforms Consumer[R1] to Consumer[R2]
|
||||
// - The direction is reversed: we go from Consumer[R1] to Consumer[R2]
|
||||
//
|
||||
// This is semantically identical to Local and Compose, but uses the standard
|
||||
// categorical terminology that emphasizes the contravariant nature of the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic contravariant mapping:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Contramap with a string-to-int parser
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Contramap(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Demonstrating contravariance:
|
||||
//
|
||||
// // In covariant functors (like Option, Array), map goes "forward":
|
||||
// // map: (A -> B) -> F[A] -> F[B]
|
||||
// //
|
||||
// // In contravariant functors (like Consumer), contramap goes "backward":
|
||||
// // contramap: (B -> A) -> F[A] -> F[B]
|
||||
//
|
||||
// type Animal struct{ Name string }
|
||||
// type Dog struct{ Animal Animal; Breed string }
|
||||
//
|
||||
// // Consumer for animals
|
||||
// consumeAnimal := func(a Animal) {
|
||||
// fmt.Printf("Animal: %s\n", a.Name)
|
||||
// }
|
||||
//
|
||||
// // Function from Dog to Animal (B -> A)
|
||||
// dogToAnimal := func(d Dog) Animal {
|
||||
// return d.Animal
|
||||
// }
|
||||
//
|
||||
// // Contramap creates Consumer[Dog] from Consumer[Animal]
|
||||
// // Direction is reversed: Consumer[Animal] -> Consumer[Dog]
|
||||
// consumeDog := consumer.Contramap(dogToAnimal)(consumeAnimal)
|
||||
//
|
||||
// consumeDog(Dog{
|
||||
// Animal: Animal{Name: "Buddy"},
|
||||
// Breed: "Golden Retriever",
|
||||
// }) // Logs: "Animal: Buddy"
|
||||
//
|
||||
// Example - Contravariant functor laws:
|
||||
//
|
||||
// // Law 1: Identity
|
||||
// // contramap(identity) = identity
|
||||
// identity := func(x int) int { return x }
|
||||
// consumer1 := consumer.Contramap(identity)(consumeInt)
|
||||
// // consumer1 behaves identically to consumeInt
|
||||
//
|
||||
// // Law 2: Composition
|
||||
// // contramap(f . g) = contramap(g) . contramap(f)
|
||||
// // Note: composition order is reversed compared to covariant map
|
||||
// f := func(s string) int { n, _ := strconv.Atoi(s); return n }
|
||||
// g := func(b bool) string { if b { return "1" } else { return "0" } }
|
||||
//
|
||||
// // These two are equivalent:
|
||||
// consumer2 := consumer.Contramap(func(b bool) int { return f(g(b)) })(consumeInt)
|
||||
// consumer3 := consumer.Contramap(g)(consumer.Contramap(f)(consumeInt))
|
||||
//
|
||||
// Example - Practical use with type hierarchies:
|
||||
//
|
||||
// type Logger interface {
|
||||
// Log(string)
|
||||
// }
|
||||
//
|
||||
// type Message struct {
|
||||
// Text string
|
||||
// Timestamp time.Time
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs strings
|
||||
// logString := func(s string) {
|
||||
// fmt.Println(s)
|
||||
// }
|
||||
//
|
||||
// // Contramap to handle Message types
|
||||
// extractText := func(m Message) string {
|
||||
// return fmt.Sprintf("[%s] %s", m.Timestamp.Format(time.RFC3339), m.Text)
|
||||
// }
|
||||
//
|
||||
// logMessage := consumer.Contramap(extractText)(logString)
|
||||
// logMessage(Message{
|
||||
// Text: "Hello",
|
||||
// Timestamp: time.Now(),
|
||||
// }) // Logs: "[2024-01-20T10:00:00Z] Hello"
|
||||
//
|
||||
// Relationship to Local and Compose:
|
||||
// - Contramap, Local, and Compose are identical in implementation
|
||||
// - Contramap emphasizes the categorical/theoretical aspect
|
||||
// - Local emphasizes the context transformation aspect
|
||||
// - Compose emphasizes the function composition aspect
|
||||
// - Use Contramap when working with category theory concepts
|
||||
// - Use Local when adapting to different contexts
|
||||
// - Use Compose when building functional pipelines
|
||||
//
|
||||
// Category Theory Background:
|
||||
// - Consumer[A] forms a contravariant functor
|
||||
// - The contravariant functor laws must hold:
|
||||
// 1. contramap(id) = id
|
||||
// 2. contramap(f ∘ g) = contramap(g) ∘ contramap(f)
|
||||
// - This is dual to the covariant functor (map) operation
|
||||
// - Consumers are contravariant because they consume rather than produce values
|
||||
//
|
||||
// Use Cases:
|
||||
// - Working with contravariant functors in a categorical style
|
||||
// - Adapting consumers to work with more specific types
|
||||
// - Building type-safe consumer transformations
|
||||
// - Implementing profunctor patterns (Consumer is a profunctor)
|
||||
func Contramap[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return Local(f)
|
||||
}
|
||||
|
||||
@@ -381,3 +381,513 @@ func TestLocal(t *testing.T) {
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContramap(t *testing.T) {
|
||||
t.Run("basic contravariant mapping", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
parseToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Contramap(parseToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("contravariant identity law", func(t *testing.T) {
|
||||
// contramap(identity) = identity
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
identity := function.Identity[int]
|
||||
consumeIdentity := Contramap(identity)(consumeInt)
|
||||
|
||||
consumeIdentity(42)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
// Should behave identically to original consumer
|
||||
consumeInt(100)
|
||||
capturedDirect := captured
|
||||
consumeIdentity(100)
|
||||
capturedMapped := captured
|
||||
|
||||
assert.Equal(t, capturedDirect, capturedMapped)
|
||||
})
|
||||
|
||||
t.Run("contravariant composition law", func(t *testing.T) {
|
||||
// contramap(f . g) = contramap(g) . contramap(f)
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
f := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
g := func(b bool) string {
|
||||
if b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
// Compose f and g manually
|
||||
fg := func(b bool) int {
|
||||
return f(g(b))
|
||||
}
|
||||
|
||||
// Method 1: contramap(f . g)
|
||||
consumer1 := Contramap(fg)(consumeInt)
|
||||
consumer1(true)
|
||||
result1 := captured
|
||||
|
||||
// Method 2: contramap(g) . contramap(f)
|
||||
consumer2 := Contramap(g)(Contramap(f)(consumeInt))
|
||||
consumer2(true)
|
||||
result2 := captured
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, 1, result1)
|
||||
})
|
||||
|
||||
t.Run("type hierarchy adaptation", func(t *testing.T) {
|
||||
type Animal struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type Dog struct {
|
||||
Animal Animal
|
||||
Breed string
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeAnimal := func(a Animal) {
|
||||
capturedName = a.Name
|
||||
}
|
||||
|
||||
dogToAnimal := func(d Dog) Animal {
|
||||
return d.Animal
|
||||
}
|
||||
|
||||
consumeDog := Contramap(dogToAnimal)(consumeAnimal)
|
||||
consumeDog(Dog{
|
||||
Animal: Animal{Name: "Buddy"},
|
||||
Breed: "Golden Retriever",
|
||||
})
|
||||
|
||||
assert.Equal(t, "Buddy", capturedName)
|
||||
})
|
||||
|
||||
t.Run("field extraction with contramap", func(t *testing.T) {
|
||||
type Message struct {
|
||||
Text string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
var capturedText string
|
||||
consumeString := func(s string) {
|
||||
capturedText = s
|
||||
}
|
||||
|
||||
extractText := func(m Message) string {
|
||||
return m.Text
|
||||
}
|
||||
|
||||
consumeMessage := Contramap(extractText)(consumeString)
|
||||
consumeMessage(Message{
|
||||
Text: "Hello",
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
assert.Equal(t, "Hello", capturedText)
|
||||
})
|
||||
|
||||
t.Run("multiple contramap applications", func(t *testing.T) {
|
||||
type Level3 struct{ Value int }
|
||||
type Level2 struct{ L3 Level3 }
|
||||
type Level1 struct{ L2 Level2 }
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extract3 := func(l3 Level3) int { return l3.Value }
|
||||
extract2 := func(l2 Level2) Level3 { return l2.L3 }
|
||||
extract1 := func(l1 Level1) Level2 { return l1.L2 }
|
||||
|
||||
// Chain contramap operations
|
||||
consumeLevel3 := Contramap(extract3)(consumeInt)
|
||||
consumeLevel2 := Contramap(extract2)(consumeLevel3)
|
||||
consumeLevel1 := Contramap(extract1)(consumeLevel2)
|
||||
|
||||
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("contramap with calculation", func(t *testing.T) {
|
||||
type Rectangle struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
var capturedArea int
|
||||
consumeArea := func(area int) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(r Rectangle) int {
|
||||
return r.Width * r.Height
|
||||
}
|
||||
|
||||
consumeRectangle := Contramap(calculateArea)(consumeArea)
|
||||
consumeRectangle(Rectangle{Width: 5, Height: 10})
|
||||
|
||||
assert.Equal(t, 50, capturedArea)
|
||||
})
|
||||
|
||||
t.Run("contramap preserves side effects", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
contramappedConsumer := Contramap(transform)(consumer)
|
||||
|
||||
contramappedConsumer("1")
|
||||
contramappedConsumer("2")
|
||||
contramappedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("contramap with pointer types", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
dereference := func(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
consumePointer := Contramap(dereference)(consumeInt)
|
||||
|
||||
value := 42
|
||||
consumePointer(&value)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumePointer(nil)
|
||||
assert.Equal(t, 0, captured)
|
||||
})
|
||||
|
||||
t.Run("contramap equivalence with Local", func(t *testing.T) {
|
||||
var capturedLocal, capturedContramap int
|
||||
|
||||
consumeIntLocal := func(x int) {
|
||||
capturedLocal = x
|
||||
}
|
||||
|
||||
consumeIntContramap := func(x int) {
|
||||
capturedContramap = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// Both should produce identical results
|
||||
consumerLocal := Local(transform)(consumeIntLocal)
|
||||
consumerContramap := Contramap(transform)(consumeIntContramap)
|
||||
|
||||
consumerLocal("42")
|
||||
consumerContramap("42")
|
||||
|
||||
assert.Equal(t, capturedLocal, capturedContramap)
|
||||
assert.Equal(t, 42, capturedLocal)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose(t *testing.T) {
|
||||
t.Run("basic composition", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
parseToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Compose(parseToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("composing multiple transformations", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type Wrapper struct {
|
||||
Data Data
|
||||
}
|
||||
|
||||
var captured string
|
||||
consumeString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
extractData := func(w Wrapper) Data { return w.Data }
|
||||
extractValue := func(d Data) string { return d.Value }
|
||||
|
||||
// Compose step by step
|
||||
consumeData := Compose(extractValue)(consumeString)
|
||||
consumeWrapper := Compose(extractData)(consumeData)
|
||||
|
||||
consumeWrapper(Wrapper{Data: Data{Value: "Hello"}})
|
||||
|
||||
assert.Equal(t, "Hello", captured)
|
||||
})
|
||||
|
||||
t.Run("function composition style", func(t *testing.T) {
|
||||
type Request struct {
|
||||
Body []byte
|
||||
}
|
||||
|
||||
var captured string
|
||||
processString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
bytesToString := func(b []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
extractBody := func(r Request) []byte {
|
||||
return r.Body
|
||||
}
|
||||
|
||||
// Chain compositions
|
||||
processBytes := Compose(bytesToString)(processString)
|
||||
processRequest := Compose(extractBody)(processBytes)
|
||||
|
||||
processRequest(Request{Body: []byte("test")})
|
||||
|
||||
assert.Equal(t, "test", captured)
|
||||
})
|
||||
|
||||
t.Run("compose with identity", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
identity := function.Identity[int]
|
||||
composedConsumer := Compose(identity)(consumeInt)
|
||||
|
||||
composedConsumer(42)
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("compose with field extraction", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeName := func(name string) {
|
||||
capturedName = name
|
||||
}
|
||||
|
||||
extractName := func(u User) string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
consumeUser := Compose(extractName)(consumeName)
|
||||
consumeUser(User{Name: "Alice", Email: "alice@example.com", Age: 30})
|
||||
|
||||
assert.Equal(t, "Alice", capturedName)
|
||||
})
|
||||
|
||||
t.Run("compose with calculation", func(t *testing.T) {
|
||||
type Circle struct {
|
||||
Radius float64
|
||||
}
|
||||
|
||||
var capturedArea float64
|
||||
consumeArea := func(area float64) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(c Circle) float64 {
|
||||
return 3.14159 * c.Radius * c.Radius
|
||||
}
|
||||
|
||||
consumeCircle := Compose(calculateArea)(consumeArea)
|
||||
consumeCircle(Circle{Radius: 5.0})
|
||||
|
||||
assert.InDelta(t, 78.53975, capturedArea, 0.00001)
|
||||
})
|
||||
|
||||
t.Run("compose with slice operations", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeLength := func(n int) {
|
||||
captured = n
|
||||
}
|
||||
|
||||
getLength := func(s []string) int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
consumeSlice := Compose(getLength)(consumeLength)
|
||||
consumeSlice([]string{"a", "b", "c", "d"})
|
||||
|
||||
assert.Equal(t, 4, captured)
|
||||
})
|
||||
|
||||
t.Run("compose with map operations", func(t *testing.T) {
|
||||
var captured bool
|
||||
consumeHasKey := func(has bool) {
|
||||
captured = has
|
||||
}
|
||||
|
||||
hasKey := func(m map[string]int) bool {
|
||||
_, exists := m["key"]
|
||||
return exists
|
||||
}
|
||||
|
||||
consumeMap := Compose(hasKey)(consumeHasKey)
|
||||
|
||||
consumeMap(map[string]int{"key": 42})
|
||||
assert.True(t, captured)
|
||||
|
||||
consumeMap(map[string]int{"other": 42})
|
||||
assert.False(t, captured)
|
||||
})
|
||||
|
||||
t.Run("compose preserves consumer behavior", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
composedConsumer := Compose(transform)(consumer)
|
||||
|
||||
composedConsumer("1")
|
||||
composedConsumer("2")
|
||||
composedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("compose with error handling", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Error error
|
||||
}
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extractValue := func(r Result) int {
|
||||
if r.Error != nil {
|
||||
return -1
|
||||
}
|
||||
return r.Value
|
||||
}
|
||||
|
||||
consumeResult := Compose(extractValue)(consumeInt)
|
||||
|
||||
consumeResult(Result{Value: 42, Error: nil})
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumeResult(Result{Value: 100, Error: assert.AnError})
|
||||
assert.Equal(t, -1, captured)
|
||||
})
|
||||
|
||||
t.Run("compose equivalence with Local", func(t *testing.T) {
|
||||
var capturedLocal, capturedCompose int
|
||||
|
||||
consumeIntLocal := func(x int) {
|
||||
capturedLocal = x
|
||||
}
|
||||
|
||||
consumeIntCompose := func(x int) {
|
||||
capturedCompose = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// Both should produce identical results
|
||||
consumerLocal := Local(transform)(consumeIntLocal)
|
||||
consumerCompose := Compose(transform)(consumeIntCompose)
|
||||
|
||||
consumerLocal("42")
|
||||
consumerCompose("42")
|
||||
|
||||
assert.Equal(t, capturedLocal, capturedCompose)
|
||||
assert.Equal(t, 42, capturedLocal)
|
||||
})
|
||||
|
||||
t.Run("compose equivalence with Contramap", func(t *testing.T) {
|
||||
var capturedCompose, capturedContramap int
|
||||
|
||||
consumeIntCompose := func(x int) {
|
||||
capturedCompose = x
|
||||
}
|
||||
|
||||
consumeIntContramap := func(x int) {
|
||||
capturedContramap = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// All three should produce identical results
|
||||
consumerCompose := Compose(transform)(consumeIntCompose)
|
||||
consumerContramap := Contramap(transform)(consumeIntContramap)
|
||||
|
||||
consumerCompose("42")
|
||||
consumerContramap("42")
|
||||
|
||||
assert.Equal(t, capturedCompose, capturedContramap)
|
||||
assert.Equal(t, 42, capturedCompose)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ func TestContextPropagationThroughMonadTransforms(t *testing.T) {
|
||||
var capturedCtx context.Context
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Chain[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
@@ -405,7 +405,7 @@ func TestContextCancellationBetweenSteps(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
Chain[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestSequence(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := Sequence[Config1, Config2, int](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -88,7 +88,7 @@ func TestSequence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence[Config1, Config2, int](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -123,7 +123,7 @@ func TestSequence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence[Config1, Config2, string](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
// Test with valid inputs
|
||||
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
|
||||
@@ -155,7 +155,7 @@ func TestSequence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence[Config1, Config2, int](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
@@ -178,7 +178,7 @@ func TestSequence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence[Config1, Config2, int](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 3}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -207,7 +207,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := SequenceReader[Config1, Config2, int](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -239,7 +239,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[Config1, Config2, int](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(ctx)()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
@@ -261,7 +261,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[Config1, Config2, string](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with valid inputs
|
||||
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
|
||||
@@ -285,7 +285,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[Config1, Config2, int](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
@@ -304,7 +304,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[Config1, Config2, int](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
cfg1 := Config1{value1: 3}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -333,7 +333,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := SequenceReaderIO[Config1, Config2, int](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -365,7 +365,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO[Config1, Config2, int](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(ctx)()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
@@ -391,7 +391,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO[Config1, Config2, string](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
// Test with valid inputs
|
||||
sideEffect = 0
|
||||
@@ -419,7 +419,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO[Config1, Config2, int](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
@@ -442,7 +442,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO[Config1, Config2, int](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -478,7 +478,7 @@ func TestTraverse(t *testing.T) {
|
||||
}
|
||||
|
||||
// Apply traverse to swap order and transform
|
||||
traversed := Traverse[Config2, Config1, int, string](transform)(original)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
cfg1 := Config1{value1: 100}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -496,7 +496,7 @@ func TestTraverse(t *testing.T) {
|
||||
return Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
traversed := Traverse[Config2, Config1, int, string](transform)(original)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
@@ -516,12 +516,12 @@ func TestTraverse(t *testing.T) {
|
||||
|
||||
// Test with negative value
|
||||
originalNeg := Of[Config2](-1)
|
||||
traversedNeg := Traverse[Config2, Config1, int, string](transform)(originalNeg)
|
||||
traversedNeg := Traverse[Config2](transform)(originalNeg)
|
||||
resultNeg := traversedNeg(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), resultNeg)
|
||||
|
||||
// Test with positive value
|
||||
traversedPos := Traverse[Config2, Config1, int, string](transform)(original)
|
||||
traversedPos := Traverse[Config2](transform)(original)
|
||||
resultPos := traversedPos(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("42"), resultPos)
|
||||
})
|
||||
@@ -540,7 +540,7 @@ func TestTraverse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Config2, Config1, int, int](transform)(original)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 5})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(50), outcome)
|
||||
@@ -556,7 +556,7 @@ func TestTraverse(t *testing.T) {
|
||||
|
||||
outcome := F.Pipe2(
|
||||
original,
|
||||
Traverse[Config2, Config1, int, int](transform),
|
||||
Traverse[Config2](transform),
|
||||
func(k Kleisli[Config2, Config1, int]) ReaderReaderIOResult[Config2, int] {
|
||||
return k(Config1{value1: 5})
|
||||
},
|
||||
@@ -582,7 +582,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
}
|
||||
|
||||
// Apply traverse to introduce Config1 and swap order
|
||||
traversed := TraverseReader[Config2, Config1, int, string](formatWithConfig)(original)
|
||||
traversed := TraverseReader[Config2](formatWithConfig)(original)
|
||||
|
||||
cfg1 := Config1{value1: 5}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -600,7 +600,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
return reader.Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2, Config1, int, string](transform)(original)
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 5})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
@@ -617,7 +617,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2, Config1, int, int](double)(original)
|
||||
traversed := TraverseReader[Config2](double)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 3})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(126), outcome)
|
||||
@@ -633,7 +633,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2, Config1, int, int](transform)(original)
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
@@ -649,7 +649,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2, Config1, int, int](transform)(original)
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
cfg1 := Config1{value1: 5}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -673,7 +673,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
outcome := F.Pipe2(
|
||||
original,
|
||||
TraverseReader[Config2, Config1, int, int](multiply),
|
||||
TraverseReader[Config2](multiply),
|
||||
func(k Kleisli[Config2, Config1, int]) ReaderReaderIOResult[Config2, int] {
|
||||
return k(Config1{value1: 3})
|
||||
},
|
||||
@@ -698,7 +698,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sequence it
|
||||
sequenced := Sequence[Config1, Config2, int](nested)
|
||||
sequenced := Sequence(nested)
|
||||
|
||||
// Then traverse with a transformation
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
@@ -715,7 +715,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
|
||||
// Then apply traverse on a new computation
|
||||
original := Of[Config2](5)
|
||||
traversed := Traverse[Config2, Config1, int, string](transform)(original)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of("length=5"), outcome)
|
||||
})
|
||||
@@ -734,7 +734,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
seqResult := Sequence[Config1, Config2, int](seqErr)(cfg1)(cfg2)(ctx)()
|
||||
seqResult := Sequence(seqErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqResult))
|
||||
|
||||
// Test SequenceReader with error
|
||||
@@ -745,7 +745,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
seqReaderResult := SequenceReader[Config1, Config2, int](seqReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
seqReaderResult := SequenceReader(seqReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqReaderResult))
|
||||
|
||||
// Test SequenceReaderIO with error
|
||||
@@ -756,7 +756,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
seqReaderIOResult := SequenceReaderIO[Config1, Config2, int](seqReaderIOErr)(cfg1)(cfg2)(ctx)()
|
||||
seqReaderIOResult := SequenceReaderIO(seqReaderIOErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqReaderIOResult))
|
||||
|
||||
// Test Traverse with error
|
||||
@@ -764,7 +764,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
travTransform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
travResult := Traverse[Config2, Config1, int, string](travTransform)(travErr)(cfg1)(cfg2)(ctx)()
|
||||
travResult := Traverse[Config2](travTransform)(travErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(travResult))
|
||||
|
||||
// Test TraverseReader with error
|
||||
@@ -772,7 +772,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
travReaderTransform := func(n int) reader.Reader[Config1, string] {
|
||||
return reader.Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
travReaderResult := TraverseReader[Config2, Config1, int, string](travReaderTransform)(travReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
travReaderResult := TraverseReader[Config2](travReaderTransform)(travReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(travReaderResult))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderIOResult[R, A any](ma ReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReaderIOEither[context.Context, error](ma)
|
||||
return RRIOE.FromReaderIOEither[context.Context](ma)
|
||||
}
|
||||
|
||||
// FromReaderIO lifts a ReaderIO into a ReaderReaderIOResult.
|
||||
@@ -734,7 +734,7 @@ func FromIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
|
||||
//
|
||||
//go:inline
|
||||
func FromIOEither[R, A any](ma IOEither[error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromIOEither[R, context.Context, error](ma)
|
||||
return RRIOE.FromIOEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// FromIOResult lifts an IOResult into a ReaderReaderIOResult.
|
||||
@@ -742,14 +742,14 @@ func FromIOEither[R, A any](ma IOEither[error, A]) ReaderReaderIOResult[R, A] {
|
||||
//
|
||||
//go:inline
|
||||
func FromIOResult[R, A any](ma IOResult[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromIOEither[R, context.Context, error](ma)
|
||||
return RRIOE.FromIOEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// FromReaderEither lifts a ReaderEither into a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderEither[R, A any](ma RE.ReaderEither[R, error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReaderEither[R, context.Context, error](ma)
|
||||
return RRIOE.FromReaderEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// Ask retrieves the outer environment R.
|
||||
@@ -782,7 +782,7 @@ func FromOption[R, A any](onNone Lazy[error]) func(Option[A]) ReaderReaderIOResu
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) error) Kleisli[R, A, A] {
|
||||
return RRIOE.FromPredicate[R, context.Context, error](pred, onFalse)
|
||||
return RRIOE.FromPredicate[R, context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
// MonadAlt provides alternative/fallback behavior.
|
||||
@@ -825,7 +825,7 @@ func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadMapLeft[R, context.Context](fa, f)
|
||||
return RRIOE.MonadMapLeft(fa, f)
|
||||
}
|
||||
|
||||
// MapLeft transforms the error value if the computation fails.
|
||||
@@ -864,7 +864,7 @@ func ReadIOEither[A, R any](rio IOEither[error, R]) func(ReaderReaderIOResult[R,
|
||||
//
|
||||
//go:inline
|
||||
func ReadIO[A, R any](rio IO[R]) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
|
||||
return RRIOE.ReadIO[context.Context, error, A, R](rio)
|
||||
return RRIOE.ReadIO[context.Context, error, A](rio)
|
||||
}
|
||||
|
||||
// MonadChainLeft handles errors by chaining a recovery computation.
|
||||
@@ -873,7 +873,7 @@ func ReadIO[A, R any](rio IO[R]) func(ReaderReaderIOResult[R, A]) ReaderIOResult
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[R, A any](fa ReaderReaderIOResult[R, A], f Kleisli[R, error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadChainLeft[R, context.Context, error, error, A](fa, f)
|
||||
return RRIOE.MonadChainLeft(fa, f)
|
||||
}
|
||||
|
||||
// ChainLeft handles errors by chaining a recovery computation.
|
||||
@@ -882,7 +882,7 @@ func MonadChainLeft[R, A any](fa ReaderReaderIOResult[R, A], f Kleisli[R, error,
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.ChainLeft[R, context.Context, error, error, A](f)
|
||||
return RRIOE.ChainLeft(f)
|
||||
}
|
||||
|
||||
// Delay adds a time delay before executing the computation.
|
||||
|
||||
@@ -101,7 +101,7 @@ func TestMonadChain(t *testing.T) {
|
||||
func TestChain(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
Chain[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](n * 2)
|
||||
}),
|
||||
)
|
||||
@@ -127,7 +127,7 @@ func TestChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
ChainFirst[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
ChainFirst(func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
sideEffect = n
|
||||
return Of[AppConfig]("ignored")
|
||||
}),
|
||||
@@ -141,7 +141,7 @@ func TestTap(t *testing.T) {
|
||||
sideEffect := 0
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Tap[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
Tap(func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
sideEffect = n
|
||||
return Of[AppConfig]("ignored")
|
||||
}),
|
||||
@@ -167,7 +167,7 @@ func TestFromEither(t *testing.T) {
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromEither[AppConfig, int](either.Left[int](err))
|
||||
computation := FromEither[AppConfig](either.Left[int](err))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
@@ -189,7 +189,7 @@ func TestFromResult(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
computation := FromReader[AppConfig](func(cfg AppConfig) int {
|
||||
computation := FromReader(func(cfg AppConfig) int {
|
||||
return len(cfg.DatabaseURL)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -197,7 +197,7 @@ func TestFromReader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRightReader(t *testing.T) {
|
||||
computation := RightReader[AppConfig](func(cfg AppConfig) int {
|
||||
computation := RightReader(func(cfg AppConfig) int {
|
||||
return len(cfg.LogLevel)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -241,7 +241,7 @@ func TestFromIOEither(t *testing.T) {
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromIOEither[AppConfig, int](ioeither.Left[int](err))
|
||||
computation := FromIOEither[AppConfig](ioeither.Left[int](err))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
@@ -267,7 +267,7 @@ func TestFromIOResult(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromReaderIO(t *testing.T) {
|
||||
computation := FromReaderIO[AppConfig](func(cfg AppConfig) io.IO[int] {
|
||||
computation := FromReaderIO(func(cfg AppConfig) io.IO[int] {
|
||||
return func() int { return len(cfg.DatabaseURL) }
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -275,7 +275,7 @@ func TestFromReaderIO(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRightReaderIO(t *testing.T) {
|
||||
computation := RightReaderIO[AppConfig](func(cfg AppConfig) io.IO[int] {
|
||||
computation := RightReaderIO(func(cfg AppConfig) io.IO[int] {
|
||||
return func() int { return len(cfg.LogLevel) }
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -293,7 +293,7 @@ func TestLeftReaderIO(t *testing.T) {
|
||||
|
||||
func TestFromReaderEither(t *testing.T) {
|
||||
t.Run("right", func(t *testing.T) {
|
||||
computation := FromReaderEither[AppConfig](func(cfg AppConfig) either.Either[error, int] {
|
||||
computation := FromReaderEither(func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Right[error](len(cfg.DatabaseURL))
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -302,7 +302,7 @@ func TestFromReaderEither(t *testing.T) {
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromReaderEither[AppConfig, int](func(cfg AppConfig) either.Either[error, int] {
|
||||
computation := FromReaderEither(func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Left[int](err)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -396,7 +396,7 @@ func TestAlt(t *testing.T) {
|
||||
|
||||
computation := F.Pipe1(
|
||||
Left[AppConfig, int](err),
|
||||
Alt[AppConfig](func() ReaderReaderIOResult[AppConfig, int] {
|
||||
Alt(func() ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
}),
|
||||
)
|
||||
@@ -461,7 +461,7 @@ func TestLocal(t *testing.T) {
|
||||
Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
}),
|
||||
Local[string, AppConfig, OtherConfig](func(other OtherConfig) AppConfig {
|
||||
Local[string](func(other OtherConfig) AppConfig {
|
||||
return AppConfig{DatabaseURL: other.URL, LogLevel: "debug"}
|
||||
}),
|
||||
)
|
||||
@@ -518,7 +518,7 @@ func TestChainLeft(t *testing.T) {
|
||||
err := errors.New("original error")
|
||||
computation := F.Pipe1(
|
||||
Left[AppConfig, int](err),
|
||||
ChainLeft[AppConfig](func(e error) ReaderReaderIOResult[AppConfig, int] {
|
||||
ChainLeft(func(e error) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
}),
|
||||
)
|
||||
@@ -553,7 +553,7 @@ func TestChainEitherK(t *testing.T) {
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderK[AppConfig](func(n int) reader.Reader[AppConfig, int] {
|
||||
ChainReaderK(func(n int) reader.Reader[AppConfig, int] {
|
||||
return func(cfg AppConfig) int {
|
||||
return n + len(cfg.LogLevel)
|
||||
}
|
||||
@@ -566,7 +566,7 @@ func TestChainReaderK(t *testing.T) {
|
||||
func TestChainReaderIOK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderIOK[AppConfig](func(n int) readerio.ReaderIO[AppConfig, int] {
|
||||
ChainReaderIOK(func(n int) readerio.ReaderIO[AppConfig, int] {
|
||||
return func(cfg AppConfig) io.IO[int] {
|
||||
return func() int {
|
||||
return n + len(cfg.DatabaseURL)
|
||||
@@ -581,7 +581,7 @@ func TestChainReaderIOK(t *testing.T) {
|
||||
func TestChainReaderEitherK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderEitherK[AppConfig](func(n int) RE.ReaderEither[AppConfig, error, int] {
|
||||
ChainReaderEitherK(func(n int) RE.ReaderEither[AppConfig, error, int] {
|
||||
return func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Right[error](n + len(cfg.LogLevel))
|
||||
}
|
||||
@@ -670,7 +670,7 @@ func TestChainOptionK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromReaderIOResult(t *testing.T) {
|
||||
computation := FromReaderIOResult[AppConfig](func(cfg AppConfig) ioresult.IOResult[int] {
|
||||
computation := FromReaderIOResult(func(cfg AppConfig) ioresult.IOResult[int] {
|
||||
return func() result.Result[int] {
|
||||
return result.Of(len(cfg.DatabaseURL))
|
||||
}
|
||||
@@ -711,7 +711,7 @@ func TestAp(t *testing.T) {
|
||||
fa := Of[AppConfig](21)
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](N.Mul(2)),
|
||||
Ap[int, AppConfig](fa),
|
||||
Ap[int](fa),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
|
||||
@@ -253,7 +253,7 @@ func Second[T1, T2 any](_ T1, t2 T2) T2 {
|
||||
}
|
||||
|
||||
// Zero returns the zero value of the given type.
|
||||
func Zero[A comparable]() A {
|
||||
func Zero[A any]() A {
|
||||
var zero A
|
||||
return zero
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ func BenchmarkMonadChain_Left(b *testing.B) {
|
||||
|
||||
func BenchmarkChain_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := Chain[benchConfig](func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
chainer := Chain(func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -214,7 +214,7 @@ func BenchmarkChain_Right(b *testing.B) {
|
||||
|
||||
func BenchmarkChain_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := Chain[benchConfig](func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
chainer := Chain(func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -224,7 +224,7 @@ func BenchmarkChain_Left(b *testing.B) {
|
||||
|
||||
func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := ChainFirst[benchConfig](func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
chainer := ChainFirst(func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -234,7 +234,7 @@ func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
|
||||
func BenchmarkChainFirst_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := ChainFirst[benchConfig](func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
chainer := ChainFirst(func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -443,7 +443,7 @@ func BenchmarkPipeline_Chain_Right(b *testing.B) {
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -455,7 +455,7 @@ func BenchmarkPipeline_Chain_Left(b *testing.B) {
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func BenchmarkPipeline_Complex_Right(b *testing.B) {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
@@ -482,7 +482,7 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
@@ -492,7 +492,7 @@ func BenchmarkExecutePipeline_Complex_Right(b *testing.B) {
|
||||
rioe := F.Pipe3(
|
||||
Right[benchConfig](10),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
b.ResetTimer()
|
||||
|
||||
70
v2/internal/readert/monoid.go
Normal file
70
v2/internal/readert/monoid.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package readert
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// ApplySemigroup lifts a Semigroup[A] into a Semigroup[Reader[R, A]].
|
||||
// This allows you to combine two Readers that produce semigroup values by combining
|
||||
// their results using the semigroup's concat operation.
|
||||
//
|
||||
// The _map and _ap parameters are the Map and Ap operations for the Reader type,
|
||||
// typically obtained from the reader package.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// // Using the additive semigroup for integers
|
||||
// intSemigroup := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
// readerSemigroup := reader.ApplySemigroup(
|
||||
// reader.MonadMap[Config, int, func(int) int],
|
||||
// reader.MonadAp[int, Config, int],
|
||||
// intSemigroup,
|
||||
// )
|
||||
//
|
||||
// r1 := reader.Of[Config](5)
|
||||
// r2 := reader.Of[Config](3)
|
||||
// combined := readerSemigroup.Concat(r1, r2)
|
||||
// result := combined(Config{Multiplier: 1}) // 8
|
||||
func ApplySemigroup[R, A any](
|
||||
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
|
||||
_ap func(func(R, func(A) A), func(R) A) func(R) A,
|
||||
|
||||
s S.Semigroup[A],
|
||||
) S.Semigroup[func(R) A] {
|
||||
return S.ApplySemigroup(_map, _ap, s)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid lifts a Monoid[A] into a Monoid[Reader[R, A]].
|
||||
// This allows you to combine Readers that produce monoid values, with an empty/identity Reader.
|
||||
//
|
||||
// The _of parameter is the Of operation (pure/return) for the Reader type.
|
||||
// The _map and _ap parameters are the Map and Ap operations for the Reader type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Prefix string }
|
||||
// // Using the string concatenation monoid
|
||||
// stringMonoid := monoid.MakeMonoid("", func(a, b string) string { return a + b })
|
||||
// readerMonoid := reader.ApplicativeMonoid(
|
||||
// reader.Of[Config, string],
|
||||
// reader.MonadMap[Config, string, func(string) string],
|
||||
// reader.MonadAp[string, Config, string],
|
||||
// stringMonoid,
|
||||
// )
|
||||
//
|
||||
// r1 := reader.Asks(func(c Config) string { return c.Prefix })
|
||||
// r2 := reader.Of[Config]("hello")
|
||||
// combined := readerMonoid.Concat(r1, r2)
|
||||
// result := combined(Config{Prefix: ">> "}) // ">> hello"
|
||||
// empty := readerMonoid.Empty()(Config{Prefix: "any"}) // ""
|
||||
func ApplicativeMonoid[R, A any](
|
||||
_of func(A) func(R) A,
|
||||
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
|
||||
_ap func(func(R, func(A) A), func(R) A) func(R) A,
|
||||
|
||||
m M.Monoid[A],
|
||||
) M.Monoid[func(R) A] {
|
||||
return M.ApplicativeMonoid(_of, _map, _ap, m)
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
//
|
||||
// safeOperation := io.WithLock(lock)(dangerousOperation)
|
||||
// result := safeOperation()
|
||||
func WithLock[A any](lock IO[context.CancelFunc]) func(fa IO[A]) IO[A] {
|
||||
func WithLock[A any](lock IO[context.CancelFunc]) Operator[A, A] {
|
||||
return func(fa IO[A]) IO[A] {
|
||||
return func() A {
|
||||
defer lock()()
|
||||
|
||||
31
v2/optics/builder/builder.go
Normal file
31
v2/optics/builder/builder.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
func MakeBuilder[S, A any](get func(S) Option[A], set func(A) Endomorphism[S], name string) Builder[S, A] {
|
||||
return Builder[S, A]{
|
||||
GetOption: get,
|
||||
Set: set,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func ComposeLensPrism[S, A, B any](r Prism[A, B]) func(Lens[S, A]) Builder[S, B] {
|
||||
return func(l Lens[S, A]) Builder[S, B] {
|
||||
return MakeBuilder(
|
||||
F.Flow2(
|
||||
l.Get,
|
||||
r.GetOption,
|
||||
),
|
||||
F.Flow2(
|
||||
r.ReverseGet,
|
||||
l.Set,
|
||||
),
|
||||
fmt.Sprintf("Compose[%s -> %s]", l, r),
|
||||
)
|
||||
}
|
||||
}
|
||||
27
v2/optics/builder/types.go
Normal file
27
v2/optics/builder/types.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Builder[S, A any] struct {
|
||||
GetOption func(S) Option[A]
|
||||
|
||||
Set func(A) Endomorphism[S]
|
||||
|
||||
name string
|
||||
}
|
||||
|
||||
Kleisli[S, A, B any] = func(A) Builder[S, B]
|
||||
Operator[S, A, B any] = Kleisli[S, Builder[S, A], B]
|
||||
)
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"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/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -27,6 +28,8 @@ type typeImpl[A, O, I any] struct {
|
||||
encode Encode[A, O]
|
||||
}
|
||||
|
||||
var emptyContext = A.Empty[validation.ContextEntry]()
|
||||
|
||||
// MakeType creates a new Type with the given name, type checker, validator, and encoder.
|
||||
//
|
||||
// Parameters:
|
||||
@@ -138,16 +141,16 @@ func isTypedNil[A any](x any) Result[*A] {
|
||||
return result.Left[*A](errors.New("expecting nil"))
|
||||
}
|
||||
|
||||
func validateFromIs[A any](
|
||||
is ReaderResult[any, A],
|
||||
func validateFromIs[A, I any](
|
||||
is ReaderResult[I, A],
|
||||
msg string,
|
||||
) Reader[any, Reader[Context, Validation[A]]] {
|
||||
return func(u any) Reader[Context, Validation[A]] {
|
||||
) Validate[I, A] {
|
||||
return func(i I) Reader[Context, Validation[A]] {
|
||||
return F.Pipe2(
|
||||
u,
|
||||
i,
|
||||
is,
|
||||
result.Fold(
|
||||
validation.FailureWithError[A](u, msg),
|
||||
validation.FailureWithError[A](F.ToAny(i), msg),
|
||||
F.Flow2(
|
||||
validation.Success[A],
|
||||
reader.Of[Context],
|
||||
@@ -157,6 +160,17 @@ func validateFromIs[A any](
|
||||
}
|
||||
}
|
||||
|
||||
func isFromValidate[T, I any](val Validate[I, T]) ReaderResult[any, T] {
|
||||
invalidType := result.Left[T](errors.New("invalid input type"))
|
||||
return func(u any) Result[T] {
|
||||
i, ok := u.(I)
|
||||
if !ok {
|
||||
return invalidType
|
||||
}
|
||||
return validation.ToResult(val(i)(emptyContext))
|
||||
}
|
||||
}
|
||||
|
||||
// MakeNilType creates a Type that validates nil values.
|
||||
// It accepts any input and validates that it is nil, returning a typed nil pointer.
|
||||
//
|
||||
@@ -178,8 +192,7 @@ func Nil[A any]() Type[*A, *A, any] {
|
||||
}
|
||||
|
||||
func MakeSimpleType[A any]() Type[A, A, any] {
|
||||
var zero A
|
||||
name := fmt.Sprintf("%T", zero)
|
||||
name := fmt.Sprintf("%T", *new(A))
|
||||
is := Is[A]()
|
||||
|
||||
return MakeType(
|
||||
@@ -190,14 +203,53 @@ func MakeSimpleType[A any]() Type[A, A, any] {
|
||||
)
|
||||
}
|
||||
|
||||
// String creates a Type for string values.
|
||||
// It validates that input is a string type and provides identity encoding/decoding.
|
||||
// This is a simple type that accepts any input and validates it's a string.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[string, string, any] that can validate, decode, and encode string values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// stringType := codec.String()
|
||||
// result := stringType.Decode("hello") // Success: Right("hello")
|
||||
// result := stringType.Decode(123) // Failure: Left(validation errors)
|
||||
// encoded := stringType.Encode("world") // Returns: "world"
|
||||
func String() Type[string, string, any] {
|
||||
return MakeSimpleType[string]()
|
||||
}
|
||||
|
||||
// Int creates a Type for int values.
|
||||
// It validates that input is an int type and provides identity encoding/decoding.
|
||||
// This is a simple type that accepts any input and validates it's an int.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[int, int, any] that can validate, decode, and encode int values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intType := codec.Int()
|
||||
// result := intType.Decode(42) // Success: Right(42)
|
||||
// result := intType.Decode("42") // Failure: Left(validation errors)
|
||||
// encoded := intType.Encode(100) // Returns: 100
|
||||
func Int() Type[int, int, any] {
|
||||
return MakeSimpleType[int]()
|
||||
}
|
||||
|
||||
// Bool creates a Type for bool values.
|
||||
// It validates that input is a bool type and provides identity encoding/decoding.
|
||||
// This is a simple type that accepts any input and validates it's a bool.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[bool, bool, any] that can validate, decode, and encode bool values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// boolType := codec.Bool()
|
||||
// result := boolType.Decode(true) // Success: Right(true)
|
||||
// result := boolType.Decode(1) // Failure: Left(validation errors)
|
||||
// encoded := boolType.Encode(false) // Returns: false
|
||||
func Bool() Type[bool, bool, any] {
|
||||
return MakeSimpleType[bool]()
|
||||
}
|
||||
@@ -216,7 +268,7 @@ func pairToValidation[T any](p validationPair[T]) Validation[T] {
|
||||
return either.Of[validation.Errors](value)
|
||||
}
|
||||
|
||||
func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Validation[[]T]] {
|
||||
func validateArrayFromArray[T, O, I any](item Type[T, O, I]) Validate[[]I, []T] {
|
||||
|
||||
appendErrors := F.Flow2(
|
||||
A.Concat,
|
||||
@@ -232,8 +284,48 @@ func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Vali
|
||||
|
||||
zero := pair.Zero[validation.Errors, []T]()
|
||||
|
||||
return func(u any) Reader[Context, Validation[[]T]] {
|
||||
val := reflect.ValueOf(u)
|
||||
return func(is []I) Reader[Context, Validation[[]T]] {
|
||||
|
||||
return func(c Context) Validation[[]T] {
|
||||
|
||||
return F.Pipe1(
|
||||
A.MonadReduceWithIndex(is, func(i int, p validationPair[[]T], v I) validationPair[[]T] {
|
||||
return either.MonadFold(
|
||||
item.Validate(v)(appendContext(strconv.Itoa(i), itemName, v)(c)),
|
||||
appendErrors,
|
||||
appendValues,
|
||||
)(p)
|
||||
}, zero),
|
||||
pairToValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateArray[T, O any](item Type[T, O, any]) Validate[any, []T] {
|
||||
|
||||
appendErrors := F.Flow2(
|
||||
A.Concat,
|
||||
pair.MapHead[[]T, validation.Errors],
|
||||
)
|
||||
|
||||
appendValues := F.Flow2(
|
||||
A.Push,
|
||||
pair.MapTail[validation.Errors, []T],
|
||||
)
|
||||
|
||||
itemName := item.Name()
|
||||
|
||||
zero := pair.Zero[validation.Errors, []T]()
|
||||
|
||||
return func(i any) Reader[Context, Validation[[]T]] {
|
||||
|
||||
res, ok := i.([]T)
|
||||
if ok {
|
||||
return reader.Of[Context](validation.Success(res))
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(i)
|
||||
if !val.IsValid() {
|
||||
return validation.FailureWithMessage[[]T](val, "invalid value")
|
||||
}
|
||||
@@ -246,8 +338,9 @@ func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Vali
|
||||
|
||||
return F.Pipe1(
|
||||
R.MonadReduceWithIndex(val, func(i int, p validationPair[[]T], v reflect.Value) validationPair[[]T] {
|
||||
vIface := v.Interface()
|
||||
return either.MonadFold(
|
||||
item.Validate(v)(appendContext(strconv.Itoa(i), itemName, v)(c)),
|
||||
item.Validate(vIface)(appendContext(strconv.Itoa(i), itemName, vIface)(c)),
|
||||
appendErrors,
|
||||
appendValues,
|
||||
)(p)
|
||||
@@ -260,3 +353,397 @@ func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Vali
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Array creates a Type for array/slice values with elements of type T.
|
||||
// It validates that input is an array, slice, or string, and validates each element
|
||||
// using the provided item Type. During encoding, it maps the encode function over all elements.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the decoded array
|
||||
// - O: The type of elements in the encoded array
|
||||
//
|
||||
// Parameters:
|
||||
// - item: A Type[T, O, any] that defines how to validate/encode individual elements
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[[]T, []O, any] that can validate, decode, and encode array values
|
||||
//
|
||||
// The function handles:
|
||||
// - Native Go slices of type []T (passed through directly)
|
||||
// - reflect.Array, reflect.Slice, reflect.String (validated element by element)
|
||||
// - Collects all validation errors from individual elements
|
||||
// - Provides detailed context for each element's position in error messages
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intArray := codec.Array(codec.Int())
|
||||
// result := intArray.Decode([]int{1, 2, 3}) // Success: Right([1, 2, 3])
|
||||
// result := intArray.Decode([]any{1, "2", 3}) // Failure: validation error at index 1
|
||||
// encoded := intArray.Encode([]int{1, 2, 3}) // Returns: []int{1, 2, 3}
|
||||
//
|
||||
// stringArray := codec.Array(codec.String())
|
||||
// result := stringArray.Decode([]string{"a", "b"}) // Success: Right(["a", "b"])
|
||||
// result := stringArray.Decode("hello") // Success: Right(["h", "e", "l", "l", "o"])
|
||||
func Array[T, O any](item Type[T, O, any]) Type[[]T, []O, any] {
|
||||
|
||||
validate := validateArray(item)
|
||||
is := isFromValidate(validate)
|
||||
name := fmt.Sprintf("Array[%s]", item.Name())
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
is,
|
||||
validate,
|
||||
A.Map(item.Encode),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// TranscodeArray creates a Type for array/slice values with strongly-typed input.
|
||||
// Unlike Array which accepts any input type, TranscodeArray requires the input to be
|
||||
// a slice of type []I, providing type safety at the input level.
|
||||
//
|
||||
// This function validates each element of the input slice using the provided item Type,
|
||||
// transforming []I -> []T during decoding and []T -> []O during encoding.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the decoded array
|
||||
// - O: The type of elements in the encoded array
|
||||
// - I: The type of elements in the input array (must be a slice)
|
||||
//
|
||||
// Parameters:
|
||||
// - item: A Type[T, O, I] that defines how to validate/encode individual elements
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[[]T, []O, []I] that can validate, decode, and encode array values
|
||||
//
|
||||
// The function:
|
||||
// - Requires input to be exactly []I (not any)
|
||||
// - Validates each element using the item Type's validation logic
|
||||
// - Collects all validation errors from individual elements
|
||||
// - Provides detailed context for each element's position in error messages
|
||||
// - Maps the encode function over all elements during encoding
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec that transforms string slices to int slices
|
||||
// stringToInt := codec.MakeType[int, int, string](
|
||||
// "StringToInt",
|
||||
// func(s any) result.Result[int] { ... },
|
||||
// func(s string) codec.Validate[int] { ... },
|
||||
// func(i int) int { return i },
|
||||
// )
|
||||
// arrayCodec := codec.TranscodeArray(stringToInt)
|
||||
//
|
||||
// // Decode: []string -> []int
|
||||
// result := arrayCodec.Decode([]string{"1", "2", "3"}) // Success: Right([1, 2, 3])
|
||||
// result := arrayCodec.Decode([]string{"1", "x", "3"}) // Failure: validation error at index 1
|
||||
//
|
||||
// // Encode: []int -> []int
|
||||
// encoded := arrayCodec.Encode([]int{1, 2, 3}) // Returns: []int{1, 2, 3}
|
||||
//
|
||||
// Use TranscodeArray when:
|
||||
// - You need type-safe input validation ([]I instead of any)
|
||||
// - You're transforming between different slice element types
|
||||
// - You want compile-time guarantees about input types
|
||||
//
|
||||
// Use Array when:
|
||||
// - You need to accept various input types (any, reflect.Value, etc.)
|
||||
// - You're working with dynamic or unknown input types
|
||||
func TranscodeArray[T, O, I any](item Type[T, O, I]) Type[[]T, []O, []I] {
|
||||
validate := validateArrayFromArray(item)
|
||||
is := isFromValidate(validate)
|
||||
name := fmt.Sprintf("Array[%s]", item.Name())
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
is,
|
||||
validate,
|
||||
A.Map(item.Encode),
|
||||
)
|
||||
}
|
||||
|
||||
func validateEitherFromEither[L, R, OL, OR, IL, IR any](
|
||||
leftItem Type[L, OL, IL],
|
||||
rightItem Type[R, OR, IR],
|
||||
) Validate[either.Either[IL, IR], either.Either[L, R]] {
|
||||
|
||||
// leftName := left.Name()
|
||||
// rightName := right.Name()
|
||||
|
||||
return func(is either.Either[IL, IR]) Reader[Context, Validation[either.Either[L, R]]] {
|
||||
|
||||
return either.MonadFold(
|
||||
is,
|
||||
F.Flow2(
|
||||
leftItem.Validate,
|
||||
readereither.Map[Context, validation.Errors](either.Left[R, L]),
|
||||
),
|
||||
F.Flow2(
|
||||
rightItem.Validate,
|
||||
readereither.Map[Context, validation.Errors](either.Right[L, R]),
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// TranscodeEither creates a Type for Either values with strongly-typed left and right branches.
|
||||
// It validates and transforms Either[IL, IR] to Either[L, R] during decoding, and
|
||||
// Either[L, R] to Either[OL, OR] during encoding.
|
||||
//
|
||||
// This function is useful for handling sum types (discriminated unions) where a value can be
|
||||
// one of two possible types. Each branch (Left and Right) is validated and transformed
|
||||
// independently using its respective Type codec.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - L: The type of the decoded Left value
|
||||
// - R: The type of the decoded Right value
|
||||
// - OL: The type of the encoded Left value
|
||||
// - OR: The type of the encoded Right value
|
||||
// - IL: The type of the input Left value
|
||||
// - IR: The type of the input Right value
|
||||
//
|
||||
// Parameters:
|
||||
// - leftItem: A Type[L, OL, IL] that defines how to validate/encode Left values
|
||||
// - rightItem: A Type[R, OR, IR] that defines how to validate/encode Right values
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[Either[L, R], Either[OL, OR], Either[IL, IR]] that can validate, decode, and encode Either values
|
||||
//
|
||||
// The function:
|
||||
// - Validates Left values using leftItem's validation logic
|
||||
// - Validates Right values using rightItem's validation logic
|
||||
// - Preserves the Either structure (Left stays Left, Right stays Right)
|
||||
// - Provides context-aware error messages indicating which branch failed
|
||||
// - Transforms values through the respective codecs during encoding
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for Either[string, int]
|
||||
// stringCodec := codec.String()
|
||||
// intCodec := codec.Int()
|
||||
// eitherCodec := codec.TranscodeEither(stringCodec, intCodec)
|
||||
//
|
||||
// // Decode Left value
|
||||
// leftResult := eitherCodec.Decode(either.Left[int]("error"))
|
||||
// // Success: Right(Either.Left("error"))
|
||||
//
|
||||
// // Decode Right value
|
||||
// rightResult := eitherCodec.Decode(either.Right[string](42))
|
||||
// // Success: Right(Either.Right(42))
|
||||
//
|
||||
// // Encode Left value
|
||||
// encodedLeft := eitherCodec.Encode(either.Left[int]("error"))
|
||||
// // Returns: Either.Left("error")
|
||||
//
|
||||
// // Encode Right value
|
||||
// encodedRight := eitherCodec.Encode(either.Right[string](42))
|
||||
// // Returns: Either.Right(42)
|
||||
//
|
||||
// Use TranscodeEither when:
|
||||
// - You need to handle sum types or discriminated unions
|
||||
// - You want to validate and transform both branches of an Either independently
|
||||
// - You're working with error handling patterns (Left for errors, Right for success)
|
||||
// - You need type-safe transformations for both possible values
|
||||
//
|
||||
// Common patterns:
|
||||
// - Error handling: Either[Error, Value]
|
||||
// - Optional with reason: Either[Reason, Value]
|
||||
// - Validation results: Either[ValidationError, ValidatedData]
|
||||
func TranscodeEither[L, R, OL, OR, IL, IR any](leftItem Type[L, OL, IL], rightItem Type[R, OR, IR]) Type[either.Either[L, R], either.Either[OL, OR], either.Either[IL, IR]] {
|
||||
validate := validateEitherFromEither(leftItem, rightItem)
|
||||
is := isFromValidate(validate)
|
||||
name := fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name())
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
is,
|
||||
validate,
|
||||
either.Fold(F.Flow2(
|
||||
leftItem.Encode,
|
||||
either.Left[OR, OL],
|
||||
), F.Flow2(
|
||||
rightItem.Encode,
|
||||
either.Right[OL, OR],
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
func validateAlways[T any](is T) Reader[Context, Validation[T]] {
|
||||
return reader.Of[Context](validation.Success(is))
|
||||
}
|
||||
|
||||
// Id creates an identity Type codec that performs no transformation or validation.
|
||||
//
|
||||
// An identity codec is a Type[T, T, T] where:
|
||||
// - Decode: Always succeeds and returns the input value unchanged
|
||||
// - Encode: Returns the input value unchanged (identity function)
|
||||
// - Validation: Always succeeds without any checks
|
||||
//
|
||||
// This is useful as:
|
||||
// - A building block for more complex codecs
|
||||
// - A no-op codec when you need a Type but don't want any transformation
|
||||
// - A starting point for codec composition
|
||||
// - Testing and debugging codec pipelines
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type that passes through unchanged
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[T, T, T] that performs identity operations on type T
|
||||
//
|
||||
// The codec:
|
||||
// - Name: Uses the type's string representation (e.g., "int", "string")
|
||||
// - Is: Checks if a value is of type T
|
||||
// - Validate: Always succeeds and returns the input value
|
||||
// - Encode: Identity function (returns input unchanged)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an identity codec for strings
|
||||
// stringId := codec.Id[string]()
|
||||
//
|
||||
// // Decode always succeeds
|
||||
// result := stringId.Decode("hello") // Success: Right("hello")
|
||||
//
|
||||
// // Encode is identity
|
||||
// encoded := stringId.Encode("world") // Returns: "world"
|
||||
//
|
||||
// // Use in composition
|
||||
// arrayOfStrings := codec.TranscodeArray(stringId)
|
||||
// result := arrayOfStrings.Decode([]string{"a", "b", "c"})
|
||||
//
|
||||
// Use cases:
|
||||
// - When you need a Type but don't want any validation or transformation
|
||||
// - As a placeholder in generic code that requires a Type parameter
|
||||
// - Building blocks for TranscodeArray, TranscodeEither, etc.
|
||||
// - Testing codec composition without side effects
|
||||
//
|
||||
// Note: Unlike MakeSimpleType which validates the type, Id always succeeds
|
||||
// in validation. It only checks the type during the Is operation.
|
||||
func Id[T any]() Type[T, T, T] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("%T", *new(T)),
|
||||
Is[T](),
|
||||
validateAlways[T],
|
||||
F.Identity[T],
|
||||
)
|
||||
}
|
||||
|
||||
func validateFromRefinement[A, B any](refinement Refinement[A, B]) Validate[A, B] {
|
||||
|
||||
return func(a A) Reader[Context, Validation[B]] {
|
||||
|
||||
return func(ctx Context) Validation[B] {
|
||||
return F.Pipe2(
|
||||
a,
|
||||
refinement.GetOption,
|
||||
either.FromOption[B](func() validation.Errors {
|
||||
return array.Of(&validation.ValidationError{
|
||||
Value: a,
|
||||
Context: ctx,
|
||||
Messsage: fmt.Sprintf("type cannot be refined: %s", refinement),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isFromRefinement[A, B any](refinement Refinement[A, B]) ReaderResult[any, B] {
|
||||
|
||||
isA := Is[A]()
|
||||
isB := Is[B]()
|
||||
|
||||
err := fmt.Errorf("type cannot be refined: %s", refinement)
|
||||
|
||||
isAtoB := F.Flow2(
|
||||
isA,
|
||||
result.ChainOptionK[A, B](lazy.Of(err))(refinement.GetOption),
|
||||
)
|
||||
|
||||
return F.Pipe1(
|
||||
isAtoB,
|
||||
readereither.ChainLeft(reader.Of[error](isB)),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// FromRefinement creates a Type codec from a Refinement (Prism).
|
||||
//
|
||||
// A Refinement[A, B] represents the concept that B is a specialized/refined version of A.
|
||||
// For example, PositiveInt is a refinement of int, or NonEmptyString is a refinement of string.
|
||||
// This function converts a Prism[A, B] into a Type[B, A, A] codec that can validate and transform
|
||||
// between the base type A and the refined type B.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The base/broader type (e.g., int, string)
|
||||
// - B: The refined/specialized type (e.g., PositiveInt, NonEmptyString)
|
||||
//
|
||||
// Parameters:
|
||||
// - refinement: A Refinement[A, B] (which is a Prism[A, B]) that defines:
|
||||
// - GetOption: A → Option[B] - attempts to refine A to B (may fail if refinement conditions aren't met)
|
||||
// - ReverseGet: B → A - converts refined type back to base type (always succeeds)
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[B, A, A] codec where:
|
||||
// - Decode: A → Validation[B] - validates that A satisfies refinement conditions and produces B
|
||||
// - Encode: B → A - converts refined type back to base type using ReverseGet
|
||||
// - Is: Checks if a value is of type B
|
||||
// - Name: Descriptive name including the refinement's string representation
|
||||
//
|
||||
// The codec:
|
||||
// - Uses the refinement's GetOption for validation during decoding
|
||||
// - Returns validation errors if the refinement conditions are not met
|
||||
// - Uses the refinement's ReverseGet for encoding (always succeeds)
|
||||
// - Provides context-aware error messages indicating why refinement failed
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define a refinement for 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",
|
||||
// )
|
||||
//
|
||||
// // Create a codec from the refinement
|
||||
// positiveIntCodec := codec.FromRefinement[int, int](positiveIntPrism)
|
||||
//
|
||||
// // Decode: validates the refinement condition
|
||||
// result := positiveIntCodec.Decode(42) // Success: Right(42)
|
||||
// result = positiveIntCodec.Decode(-5) // Failure: validation error
|
||||
// result = positiveIntCodec.Decode(0) // Failure: validation error
|
||||
//
|
||||
// // Encode: converts back to base type
|
||||
// encoded := positiveIntCodec.Encode(42) // Returns: 42
|
||||
//
|
||||
// Use cases:
|
||||
// - Creating codecs for refined types (positive numbers, non-empty strings, etc.)
|
||||
// - Validating that values meet specific constraints
|
||||
// - Building type-safe APIs with refined types
|
||||
// - Composing refinements with other codecs using Pipe
|
||||
//
|
||||
// Common refinement patterns:
|
||||
// - Numeric constraints: PositiveInt, NonNegativeFloat, BoundedInt
|
||||
// - String constraints: NonEmptyString, EmailAddress, URL
|
||||
// - Collection constraints: NonEmptyArray, UniqueElements
|
||||
// - Domain-specific constraints: ValidAge, ValidZipCode, ValidCreditCard
|
||||
//
|
||||
// Note: The refinement's GetOption returning None will result in a validation error
|
||||
// with a message indicating the type cannot be refined. For more specific error messages,
|
||||
// consider using MakeType directly with custom validation logic.
|
||||
func FromRefinement[A, B any](refinement Refinement[A, B]) Type[B, A, A] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("FromRefinement(%s)", refinement),
|
||||
isFromRefinement(refinement),
|
||||
validateFromRefinement(refinement),
|
||||
refinement.ReverseGet,
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
81
v2/optics/codec/prism.go
Normal file
81
v2/optics/codec/prism.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
)
|
||||
|
||||
// TypeToPrism converts a Type codec into a Prism optic.
|
||||
//
|
||||
// A Type[A, S, S] represents a bidirectional codec that can decode S to A (with validation)
|
||||
// and encode A back to S. A Prism[S, A] is an optic that can optionally extract an A from S
|
||||
// and always construct an S from an A.
|
||||
//
|
||||
// This conversion bridges the codec and optics worlds, allowing you to use validation-based
|
||||
// codecs as prisms for functional optics composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source/encoded type (both input and output)
|
||||
// - A: The decoded/focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - t: A Type[A, S, S] codec where:
|
||||
// - Decode: S → Validation[A] (may fail with validation errors)
|
||||
// - Encode: A → S (always succeeds)
|
||||
// - Name: Provides a descriptive name for the type
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[S, A] where:
|
||||
// - GetOption: S → Option[A] (Some if decode succeeds, None if validation fails)
|
||||
// - ReverseGet: A → S (uses the codec's Encode function)
|
||||
// - Name: Inherited from the Type's name
|
||||
//
|
||||
// The conversion works as follows:
|
||||
// - GetOption: Decodes the value and converts validation result to Option
|
||||
// (Right(a) → Some(a), Left(errors) → None)
|
||||
// - ReverseGet: Directly uses the Type's Encode function
|
||||
// - Name: Preserves the Type's descriptive name
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for positive integers
|
||||
// positiveInt := codec.MakeType[int, int, int](
|
||||
// "PositiveInt",
|
||||
// func(i any) result.Result[int] { ... },
|
||||
// func(i int) codec.Validate[int] {
|
||||
// if i <= 0 {
|
||||
// return validation.FailureWithMessage(i, "must be positive")
|
||||
// }
|
||||
// return validation.Success(i)
|
||||
// },
|
||||
// func(i int) int { return i },
|
||||
// )
|
||||
//
|
||||
// // Convert to prism
|
||||
// prism := codec.TypeToPrism(positiveInt)
|
||||
//
|
||||
// // Use as prism
|
||||
// value := prism.GetOption(42) // Some(42) - validation succeeds
|
||||
// value = prism.GetOption(-5) // None - validation fails
|
||||
// result := prism.ReverseGet(10) // 10 - encoding always succeeds
|
||||
//
|
||||
// Use cases:
|
||||
// - Composing codecs with other optics (lenses, prisms, traversals)
|
||||
// - Using validation logic in optics pipelines
|
||||
// - Building complex data transformations with functional composition
|
||||
// - Integrating type-safe parsing with optics-based data access
|
||||
//
|
||||
// Note: The prism's GetOption will return None for any validation failure,
|
||||
// discarding the specific error details. If you need error information,
|
||||
// use the Type's Decode method directly instead.
|
||||
func TypeToPrism[S, A any](t Type[A, S, S]) Prism[S, A] {
|
||||
return prism.MakePrismWithName(
|
||||
F.Flow2(
|
||||
t.Decode,
|
||||
either.ToOption,
|
||||
),
|
||||
t.Encode,
|
||||
t.Name(),
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"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/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// ReaderResult represents a computation that depends on an environment R,
|
||||
// produces a value A, and may fail with an error.
|
||||
ReaderResult[R, A any] = readerresult.ReaderResult[R, A]
|
||||
|
||||
// Lazy represents a lazily evaluated value.
|
||||
@@ -26,9 +28,6 @@ type (
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Result represents a computation that may fail with an error.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
@@ -39,8 +38,12 @@ type (
|
||||
Encode encoder.Encoder[O, A]
|
||||
}
|
||||
|
||||
// Validation represents the result of a validation operation that may contain
|
||||
// validation errors or a successfully validated value of type A.
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
// Context provides contextual information for validation operations,
|
||||
// such as the current path in a nested structure.
|
||||
Context = validation.Context
|
||||
|
||||
// Validate is a function that validates input I to produce type A.
|
||||
@@ -77,7 +80,17 @@ type (
|
||||
Is(any) Result[A]
|
||||
}
|
||||
|
||||
// Endomorphism represents a function from type A to itself (A -> A).
|
||||
// It forms a monoid under function composition.
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Pair represents a tuple of two values of types L and R.
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
|
||||
// Prism is an optic that focuses on a part of a sum type S that may or may not
|
||||
// contain a value of type A. It provides a way to preview and review values.
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
// Refinement represents the concept that B is a specialized type of A
|
||||
Refinement[A, B any] = Prism[A, B]
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ func onTypeError(expType string) func(any) error {
|
||||
// Is checks if a value can be converted to type T.
|
||||
// Returns Some(value) if the conversion succeeds, None otherwise.
|
||||
// This is a type-safe cast operation.
|
||||
func Is[T any]() func(any) Result[T] {
|
||||
func Is[T any]() ReaderResult[any, T] {
|
||||
var zero T
|
||||
return result.ToType[T](onTypeError(fmt.Sprintf("%T", zero)))
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestAp(t *testing.T) {
|
||||
funcValidation := Of(double)
|
||||
valueValidation := Of(21)
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
@@ -126,7 +126,7 @@ func TestAp(t *testing.T) {
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
@@ -143,7 +143,7 @@ func TestAp(t *testing.T) {
|
||||
})
|
||||
valueValidation := Of(21)
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
@@ -162,7 +162,7 @@ func TestAp(t *testing.T) {
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
@@ -180,7 +180,7 @@ func TestAp(t *testing.T) {
|
||||
funcValidation := Of(toUpper)
|
||||
valueValidation := Of("hello")
|
||||
|
||||
result := Ap[string, string](valueValidation)(funcValidation)
|
||||
result := Ap[string](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
@@ -199,7 +199,7 @@ func TestAp(t *testing.T) {
|
||||
&ValidationError{Messsage: "value error 1"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
@@ -242,7 +242,7 @@ func TestMonadLaws(t *testing.T) {
|
||||
t.Run("applicative identity law", func(t *testing.T) {
|
||||
// Ap(v)(Of(id)) == v
|
||||
v := Of(42)
|
||||
result := Ap[int, int](v)(Of(F.Identity[int]))
|
||||
result := Ap[int](v)(Of(F.Identity[int]))
|
||||
|
||||
assert.Equal(t, v, result)
|
||||
})
|
||||
@@ -252,7 +252,7 @@ func TestMonadLaws(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := Ap[int, int](Of(x))(Of(f))
|
||||
left := Ap[int](Of(x))(Of(f))
|
||||
right := Of(f(x))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
@@ -285,7 +285,7 @@ func TestMapWithOperator(t *testing.T) {
|
||||
func TestApWithOperator(t *testing.T) {
|
||||
t.Run("Ap returns an Operator", func(t *testing.T) {
|
||||
valueValidation := Of(21)
|
||||
operator := Ap[int, int](valueValidation)
|
||||
operator := Ap[int](valueValidation)
|
||||
|
||||
// Operator can be applied to different function validations
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
@@ -34,6 +36,11 @@ type (
|
||||
// Errors is a collection of validation errors.
|
||||
Errors = []*ValidationError
|
||||
|
||||
ValidationErrors struct {
|
||||
Errors Errors
|
||||
Cause error
|
||||
}
|
||||
|
||||
// Validation represents the result of a validation operation.
|
||||
// Left contains validation errors, Right contains the successfully validated value.
|
||||
Validation[A any] = Either[Errors, A]
|
||||
|
||||
@@ -73,6 +73,78 @@ func (v *ValidationError) Format(s fmt.State, verb rune) {
|
||||
fmt.Fprint(s, result)
|
||||
}
|
||||
|
||||
// Error implements the error interface for ValidationErrors.
|
||||
// Returns a generic error message indicating validation errors occurred.
|
||||
func (ve *ValidationErrors) Error() string {
|
||||
if len(ve.Errors) == 0 {
|
||||
return "ValidationErrors: no errors"
|
||||
}
|
||||
if len(ve.Errors) == 1 {
|
||||
return "ValidationErrors: 1 error"
|
||||
}
|
||||
return fmt.Sprintf("ValidationErrors: %d errors", len(ve.Errors))
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause error if present.
|
||||
// This allows ValidationErrors to work with errors.Is and errors.As.
|
||||
func (ve *ValidationErrors) Unwrap() error {
|
||||
return ve.Cause
|
||||
}
|
||||
|
||||
// String returns a simple string representation of all validation errors.
|
||||
// Each error is listed on a separate line with its index.
|
||||
func (ve *ValidationErrors) String() string {
|
||||
if len(ve.Errors) == 0 {
|
||||
return "ValidationErrors: no errors"
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("ValidationErrors (%d):\n", len(ve.Errors))
|
||||
for i, err := range ve.Errors {
|
||||
result += fmt.Sprintf(" [%d] %s\n", i, err.String())
|
||||
}
|
||||
|
||||
if ve.Cause != nil {
|
||||
result += fmt.Sprintf(" caused by: %v\n", ve.Cause)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for custom formatting of ValidationErrors.
|
||||
// Supports verbs: %s, %v, %+v (with additional details)
|
||||
// %s and %v: compact format with error count
|
||||
// %+v: verbose format with all error details
|
||||
func (ve *ValidationErrors) Format(s fmt.State, verb rune) {
|
||||
if len(ve.Errors) == 0 {
|
||||
fmt.Fprint(s, "ValidationErrors: no errors")
|
||||
return
|
||||
}
|
||||
|
||||
// For simple format, just show the count
|
||||
if verb == 's' || (verb == 'v' && !s.Flag('+')) {
|
||||
if len(ve.Errors) == 1 {
|
||||
fmt.Fprint(s, "ValidationErrors: 1 error")
|
||||
} else {
|
||||
fmt.Fprintf(s, "ValidationErrors: %d errors", len(ve.Errors))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Verbose format with all details
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
fmt.Fprintf(s, "ValidationErrors (%d):\n", len(ve.Errors))
|
||||
for i, err := range ve.Errors {
|
||||
fmt.Fprintf(s, " [%d] ", i)
|
||||
err.Format(s, verb)
|
||||
fmt.Fprint(s, "\n")
|
||||
}
|
||||
|
||||
if ve.Cause != nil {
|
||||
fmt.Fprintf(s, " root cause: %+v\n", ve.Cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Failures creates a validation failure from a collection of errors.
|
||||
// Returns a Left Either containing the errors.
|
||||
func Failures[T any](err Errors) Validation[T] {
|
||||
@@ -123,3 +195,50 @@ func FailureWithError[T any](value any, message string) Reader[error, Reader[Con
|
||||
func Success[T any](value T) Validation[T] {
|
||||
return either.Of[Errors](value)
|
||||
}
|
||||
|
||||
// MakeValidationErrors converts a collection of validation errors into a single error.
|
||||
// It wraps the Errors slice in a ValidationErrors struct that implements the error interface.
|
||||
// This is useful for converting validation failures into standard Go errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - errors: A slice of ValidationError pointers representing validation failures
|
||||
//
|
||||
// Returns:
|
||||
// - An error that contains all the validation errors and can be used with standard error handling
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// errors := Errors{
|
||||
// &ValidationError{Value: "abc", Messsage: "expected number"},
|
||||
// &ValidationError{Value: nil, Messsage: "required field"},
|
||||
// }
|
||||
// err := MakeValidationErrors(errors)
|
||||
// fmt.Println(err) // Output: ValidationErrors: 2 errors
|
||||
func MakeValidationErrors(errors Errors) error {
|
||||
return &ValidationErrors{Errors: errors}
|
||||
}
|
||||
|
||||
// ToResult converts a Validation[T] to a Result[T].
|
||||
// It transforms the Left side (validation errors) into a standard error using MakeValidationErrors,
|
||||
// while preserving the Right side (successful value) unchanged.
|
||||
// This is useful for integrating validation results with code that expects Result types.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of the successfully validated value
|
||||
//
|
||||
// Parameters:
|
||||
// - val: A Validation[T] which is Either[Errors, T]
|
||||
//
|
||||
// Returns:
|
||||
// - A Result[T] which is Either[error, T], with validation errors converted to a single error
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validation := Success[int](42)
|
||||
// result := ToResult(validation) // Result containing 42
|
||||
//
|
||||
// validation := Failures[int](Errors{&ValidationError{Messsage: "invalid"}})
|
||||
// result := ToResult(validation) // Result containing ValidationErrors error
|
||||
func ToResult[T any](val Validation[T]) Result[T] {
|
||||
return either.MonadMapLeft(val, MakeValidationErrors)
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ func TestValidationError_String(t *testing.T) {
|
||||
expected := "ValidationError: invalid value"
|
||||
assert.Equal(t, expected, err.String())
|
||||
}
|
||||
|
||||
func TestValidationError_Unwrap(t *testing.T) {
|
||||
|
||||
t.Run("with cause", func(t *testing.T) {
|
||||
cause := errors.New("underlying error")
|
||||
err := &ValidationError{
|
||||
@@ -417,3 +417,236 @@ func TestValidationError_FormatEdgeCases(t *testing.T) {
|
||||
assert.Contains(t, result, "value: <nil>")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMakeValidationErrors(t *testing.T) {
|
||||
t.Run("creates error from single validation error", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Value: "test", Messsage: "invalid value"},
|
||||
}
|
||||
|
||||
err := MakeValidationErrors(errs)
|
||||
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "ValidationErrors: 1 error", err.Error())
|
||||
|
||||
// Verify it's a ValidationErrors type
|
||||
ve, ok := err.(*ValidationErrors)
|
||||
require.True(t, ok)
|
||||
assert.Len(t, ve.Errors, 1)
|
||||
assert.Equal(t, "invalid value", ve.Errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("creates error from multiple validation errors", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Value: "test1", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test2", Messsage: "error 2"},
|
||||
&ValidationError{Value: "test3", Messsage: "error 3"},
|
||||
}
|
||||
|
||||
err := MakeValidationErrors(errs)
|
||||
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "ValidationErrors: 3 errors", err.Error())
|
||||
|
||||
ve, ok := err.(*ValidationErrors)
|
||||
require.True(t, ok)
|
||||
assert.Len(t, ve.Errors, 3)
|
||||
})
|
||||
|
||||
t.Run("creates error from empty errors slice", func(t *testing.T) {
|
||||
errs := Errors{}
|
||||
|
||||
err := MakeValidationErrors(errs)
|
||||
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "ValidationErrors: no errors", err.Error())
|
||||
|
||||
ve, ok := err.(*ValidationErrors)
|
||||
require.True(t, ok)
|
||||
assert.Len(t, ve.Errors, 0)
|
||||
})
|
||||
|
||||
t.Run("preserves error details", func(t *testing.T) {
|
||||
cause := errors.New("underlying cause")
|
||||
errs := Errors{
|
||||
&ValidationError{
|
||||
Value: "abc",
|
||||
Context: []ContextEntry{{Key: "field"}},
|
||||
Messsage: "invalid format",
|
||||
Cause: cause,
|
||||
},
|
||||
}
|
||||
|
||||
err := MakeValidationErrors(errs)
|
||||
|
||||
ve, ok := err.(*ValidationErrors)
|
||||
require.True(t, ok)
|
||||
require.Len(t, ve.Errors, 1)
|
||||
assert.Equal(t, "abc", ve.Errors[0].Value)
|
||||
assert.Equal(t, "invalid format", ve.Errors[0].Messsage)
|
||||
assert.Equal(t, cause, ve.Errors[0].Cause)
|
||||
assert.Len(t, ve.Errors[0].Context, 1)
|
||||
})
|
||||
|
||||
t.Run("error can be formatted", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{
|
||||
Context: []ContextEntry{{Key: "user"}, {Key: "name"}},
|
||||
Messsage: "required",
|
||||
},
|
||||
}
|
||||
|
||||
err := MakeValidationErrors(errs)
|
||||
|
||||
formatted := fmt.Sprintf("%+v", err)
|
||||
assert.Contains(t, formatted, "ValidationErrors")
|
||||
assert.Contains(t, formatted, "user.name")
|
||||
assert.Contains(t, formatted, "required")
|
||||
})
|
||||
}
|
||||
|
||||
func TestToResult(t *testing.T) {
|
||||
t.Run("converts successful validation to result", func(t *testing.T) {
|
||||
validation := Success(42)
|
||||
|
||||
result := ToResult(validation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(error) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("converts failed validation to result with error", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Value: "abc", Messsage: "expected number"},
|
||||
}
|
||||
validation := Failures[int](errs)
|
||||
|
||||
result := ToResult(validation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
err := either.MonadFold(result,
|
||||
F.Identity[error],
|
||||
func(int) error { return nil },
|
||||
)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "ValidationErrors: 1 error", err.Error())
|
||||
|
||||
// Verify it's a ValidationErrors type
|
||||
ve, ok := err.(*ValidationErrors)
|
||||
require.True(t, ok)
|
||||
assert.Len(t, ve.Errors, 1)
|
||||
assert.Equal(t, "expected number", ve.Errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("converts multiple validation errors to result", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Value: "test1", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test2", Messsage: "error 2"},
|
||||
}
|
||||
validation := Failures[string](errs)
|
||||
|
||||
result := ToResult(validation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
err := either.MonadFold(result,
|
||||
F.Identity[error],
|
||||
func(string) error { return nil },
|
||||
)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "ValidationErrors: 2 errors", err.Error())
|
||||
|
||||
ve, ok := err.(*ValidationErrors)
|
||||
require.True(t, ok)
|
||||
assert.Len(t, ve.Errors, 2)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// String type
|
||||
strValidation := Success("hello")
|
||||
strResult := ToResult(strValidation)
|
||||
assert.True(t, either.IsRight(strResult))
|
||||
|
||||
// Bool type
|
||||
boolValidation := Success(true)
|
||||
boolResult := ToResult(boolValidation)
|
||||
assert.True(t, either.IsRight(boolResult))
|
||||
|
||||
// Struct type
|
||||
type User struct{ Name string }
|
||||
userValidation := Success(User{Name: "Alice"})
|
||||
userResult := ToResult(userValidation)
|
||||
assert.True(t, either.IsRight(userResult))
|
||||
user := either.MonadFold(userResult,
|
||||
func(error) User { return User{} },
|
||||
F.Identity[User],
|
||||
)
|
||||
assert.Equal(t, "Alice", user.Name)
|
||||
})
|
||||
|
||||
t.Run("preserves error context in result", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{
|
||||
Value: nil,
|
||||
Context: []ContextEntry{{Key: "user"}, {Key: "email"}},
|
||||
Messsage: "required field",
|
||||
},
|
||||
}
|
||||
validation := Failures[string](errs)
|
||||
|
||||
result := ToResult(validation)
|
||||
|
||||
err := either.MonadFold(result,
|
||||
F.Identity[error],
|
||||
func(string) error { return nil },
|
||||
)
|
||||
formatted := fmt.Sprintf("%+v", err)
|
||||
assert.Contains(t, formatted, "user.email")
|
||||
assert.Contains(t, formatted, "required field")
|
||||
})
|
||||
|
||||
t.Run("preserves cause in result error", func(t *testing.T) {
|
||||
cause := errors.New("parse error")
|
||||
errs := Errors{
|
||||
&ValidationError{
|
||||
Value: "abc",
|
||||
Messsage: "invalid number",
|
||||
Cause: cause,
|
||||
},
|
||||
}
|
||||
validation := Failures[int](errs)
|
||||
|
||||
result := ToResult(validation)
|
||||
|
||||
err := either.MonadFold(result,
|
||||
F.Identity[error],
|
||||
func(int) error { return nil },
|
||||
)
|
||||
ve, ok := err.(*ValidationErrors)
|
||||
require.True(t, ok)
|
||||
require.Len(t, ve.Errors, 1)
|
||||
assert.True(t, errors.Is(ve.Errors[0], cause))
|
||||
})
|
||||
|
||||
t.Run("result error implements error interface", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Messsage: "test error"},
|
||||
}
|
||||
validation := Failures[int](errs)
|
||||
|
||||
result := ToResult(validation)
|
||||
|
||||
err := either.MonadFold(result,
|
||||
F.Identity[error],
|
||||
func(int) error { return nil },
|
||||
)
|
||||
|
||||
// Should be usable as a standard error
|
||||
var stdErr error = err
|
||||
assert.NotNil(t, stdErr)
|
||||
assert.Contains(t, stdErr.Error(), "ValidationErrors")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -267,6 +267,11 @@ func MakeLensCurriedWithName[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A
|
||||
return Lens[S, A]{Get: get, Set: set, name: name}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MakeLensCurriedRefWithName[GET ~func(*S) A, SET ~func(A) Endomorphism[*S], S, A any](get GET, set SET, name string) Lens[*S, A] {
|
||||
return Lens[*S, A]{Get: get, Set: setCopyCurried(set), name: name}
|
||||
}
|
||||
|
||||
// MakeLensRef creates a [Lens] for pointer-based structures.
|
||||
//
|
||||
// Unlike [MakeLens], the setter does not need to create a copy manually. This function
|
||||
|
||||
252
v2/optics/lens/prism/compose.go
Normal file
252
v2/optics/lens/prism/compose.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// 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 prism
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
func compose[S, A, B any](
|
||||
creator func(get option.Kleisli[S, B], set func(B) Endomorphism[S], name string) Optional[S, B],
|
||||
p Prism[A, B]) func(Lens[S, A]) Optional[S, B] {
|
||||
|
||||
return func(l Lens[S, A]) Optional[S, B] {
|
||||
// GetOption: Lens.Get followed by Prism.GetOption
|
||||
// This extracts A from S, then tries to extract B from A
|
||||
getOption := F.Flow2(l.Get, p.GetOption)
|
||||
|
||||
// Set: Constructs a setter that respects the Optional laws
|
||||
setOption := func(b B) func(S) S {
|
||||
// Pre-compute the new A value by using Prism.ReverseGet
|
||||
// This constructs an A from the given B
|
||||
setl := l.Set(p.ReverseGet(b))
|
||||
|
||||
return func(s S) S {
|
||||
// Check if the Prism matches the current value
|
||||
return F.Pipe1(
|
||||
getOption(s),
|
||||
option.Fold(
|
||||
// None case: Prism doesn't match, return s unchanged (no-op)
|
||||
// This satisfies the GetSet law for Optional
|
||||
lazy.Of(s),
|
||||
// Some case: Prism matches, update the value
|
||||
// This satisfies the SetGet law for Optional
|
||||
func(_ B) S {
|
||||
return setl(s)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return creator(
|
||||
getOption,
|
||||
setOption,
|
||||
fmt.Sprintf("Compose[%s -> %s]", l, p),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Compose composes a Lens with a Prism to create an Optional.
|
||||
//
|
||||
// This composition allows you to focus on a part of a structure (using a Lens)
|
||||
// and then optionally extract a variant from that part (using a Prism). The result
|
||||
// is an Optional because the Prism may not match the focused value.
|
||||
//
|
||||
// The composition follows the Optional laws (a relaxed form of lens laws):
|
||||
//
|
||||
// SetGet Law (GetSet for Optional):
|
||||
// - If optional.GetOption(s) = Some(b), then optional.GetOption(optional.Set(b)(s)) = Some(b)
|
||||
// - This ensures that setting a value and then getting it returns the same value
|
||||
//
|
||||
// GetSet Law (for Optional):
|
||||
// - If optional.GetOption(s) = None, then optional.Set(b)(s) = s (no-op)
|
||||
// - This ensures that setting a value when the optional doesn't match leaves the structure unchanged
|
||||
//
|
||||
// These laws are documented in the official fp-ts documentation:
|
||||
// https://gcanti.github.io/monocle-ts/modules/Optional.ts.html
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source/outer structure type
|
||||
// - A: The intermediate type (focused by the Lens)
|
||||
// - B: The target type (focused by the Prism within A)
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A Prism[A, B] that optionally extracts B from A
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns an Optional[S, B]
|
||||
//
|
||||
// Behavior:
|
||||
// - GetOption: First uses the Lens to get A from S, then uses the Prism to try to extract B from A.
|
||||
// Returns Some(b) if both operations succeed, None otherwise.
|
||||
// - Set: When setting a value b:
|
||||
// - If GetOption(s) returns Some(_), it means the Prism matches, so we:
|
||||
// 1. Use Prism.ReverseGet to construct an A from b
|
||||
// 2. Use Lens.Set to update S with the new A
|
||||
// - If GetOption(s) returns None, the Prism doesn't match, so we return s unchanged (no-op)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Database DatabaseConfig
|
||||
// }
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// Connection ConnectionType
|
||||
// }
|
||||
//
|
||||
// type ConnectionType interface{ isConnection() }
|
||||
// type PostgreSQL struct{ Host string }
|
||||
// type MySQL struct{ Host string }
|
||||
//
|
||||
// // Lens to focus on Database field
|
||||
// dbLens := lens.MakeLens(
|
||||
// func(c Config) DatabaseConfig { return c.Database },
|
||||
// func(c Config, db DatabaseConfig) Config { c.Database = db; return c },
|
||||
// )
|
||||
//
|
||||
// // Prism to extract PostgreSQL from ConnectionType
|
||||
// pgPrism := prism.MakePrism(
|
||||
// func(ct ConnectionType) option.Option[PostgreSQL] {
|
||||
// if pg, ok := ct.(PostgreSQL); ok {
|
||||
// return option.Some(pg)
|
||||
// }
|
||||
// return option.None[PostgreSQL]()
|
||||
// },
|
||||
// func(pg PostgreSQL) ConnectionType { return pg },
|
||||
// )
|
||||
//
|
||||
// // Compose to create Optional[Config, PostgreSQL]
|
||||
// configPgOptional := Compose[Config, DatabaseConfig, PostgreSQL](pgPrism)(dbLens)
|
||||
//
|
||||
// config := Config{Database: DatabaseConfig{Connection: PostgreSQL{Host: "localhost"}}}
|
||||
// host := configPgOptional.GetOption(config) // Some(PostgreSQL{Host: "localhost"})
|
||||
//
|
||||
// updated := configPgOptional.Set(PostgreSQL{Host: "remote"})(config)
|
||||
// // updated.Database.Connection = PostgreSQL{Host: "remote"}
|
||||
//
|
||||
// configMySQL := Config{Database: DatabaseConfig{Connection: MySQL{Host: "localhost"}}}
|
||||
// none := configPgOptional.GetOption(configMySQL) // None (Prism doesn't match)
|
||||
// unchanged := configPgOptional.Set(PostgreSQL{Host: "remote"})(configMySQL)
|
||||
// // unchanged == configMySQL (no-op because Prism doesn't match)
|
||||
func Compose[S, A, B any](p Prism[A, B]) func(Lens[S, A]) Optional[S, B] {
|
||||
return compose(O.MakeOptionalCurriedWithName[S, B], p)
|
||||
}
|
||||
|
||||
// ComposeRef composes a Lens operating on pointer types with a Prism to create an Optional.
|
||||
//
|
||||
// This is the pointer-safe variant of Compose, designed for working with pointer types (*S).
|
||||
// It automatically handles nil pointer cases and creates copies before modification to ensure
|
||||
// immutability and prevent unintended side effects.
|
||||
//
|
||||
// The composition follows the same Optional laws as Compose:
|
||||
//
|
||||
// SetGet Law (GetSet for Optional):
|
||||
// - If optional.GetOption(s) = Some(b), then optional.GetOption(optional.Set(b)(s)) = Some(b)
|
||||
// - This ensures that setting a value and then getting it returns the same value
|
||||
//
|
||||
// GetSet Law (for Optional):
|
||||
// - If optional.GetOption(s) = None, then optional.Set(b)(s) = s (no-op)
|
||||
// - This ensures that setting a value when the optional doesn't match leaves the structure unchanged
|
||||
//
|
||||
// Nil Pointer Handling:
|
||||
// - When s is nil and GetOption would return None, Set operations return nil (no-op)
|
||||
// - When s is nil and GetOption would return Some (after creating default), Set creates a new instance
|
||||
// - All Set operations create a shallow copy of *S before modification to preserve immutability
|
||||
//
|
||||
// These laws are documented in the official fp-ts documentation:
|
||||
// https://gcanti.github.io/monocle-ts/modules/Optional.ts.html
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source/outer structure type (used as *S in the lens)
|
||||
// - A: The intermediate type (focused by the Lens)
|
||||
// - B: The target type (focused by the Prism within A)
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A Prism[A, B] that optionally extracts B from A
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[*S, A] and returns an Optional[*S, B]
|
||||
//
|
||||
// Behavior:
|
||||
// - GetOption: First uses the Lens to get A from *S, then uses the Prism to try to extract B from A.
|
||||
// Returns Some(b) if both operations succeed, None otherwise.
|
||||
// - Set: When setting a value b:
|
||||
// - Creates a shallow copy of *S before any modification (nil-safe)
|
||||
// - If GetOption(s) returns Some(_), it means the Prism matches, so we:
|
||||
// 1. Use Prism.ReverseGet to construct an A from b
|
||||
// 2. Use Lens.Set to update the copy of *S with the new A
|
||||
// - If GetOption(s) returns None, the Prism doesn't match, so we return s unchanged (no-op)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Connection ConnectionType
|
||||
// AppName string
|
||||
// }
|
||||
//
|
||||
// type ConnectionType interface{ isConnection() }
|
||||
// type PostgreSQL struct{ Host string }
|
||||
// type MySQL struct{ Host string }
|
||||
//
|
||||
// // Lens to focus on Connection field (pointer-based)
|
||||
// connLens := lens.MakeLensRef(
|
||||
// func(c *Config) ConnectionType { return c.Connection },
|
||||
// func(c *Config, ct ConnectionType) *Config { c.Connection = ct; return c },
|
||||
// )
|
||||
//
|
||||
// // Prism to extract PostgreSQL from ConnectionType
|
||||
// pgPrism := prism.MakePrism(
|
||||
// func(ct ConnectionType) option.Option[PostgreSQL] {
|
||||
// if pg, ok := ct.(PostgreSQL); ok {
|
||||
// return option.Some(pg)
|
||||
// }
|
||||
// return option.None[PostgreSQL]()
|
||||
// },
|
||||
// func(pg PostgreSQL) ConnectionType { return pg },
|
||||
// )
|
||||
//
|
||||
// // Compose to create Optional[*Config, PostgreSQL]
|
||||
// configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
//
|
||||
// // Works with non-nil pointers
|
||||
// config := &Config{Connection: PostgreSQL{Host: "localhost"}}
|
||||
// host := configPgOptional.GetOption(config) // Some(PostgreSQL{Host: "localhost"})
|
||||
// updated := configPgOptional.Set(PostgreSQL{Host: "remote"})(config)
|
||||
// // updated is a new *Config with Connection = PostgreSQL{Host: "remote"}
|
||||
// // original config is unchanged (immutability preserved)
|
||||
//
|
||||
// // Handles nil pointers safely
|
||||
// var nilConfig *Config = nil
|
||||
// none := configPgOptional.GetOption(nilConfig) // None (nil pointer)
|
||||
// unchanged := configPgOptional.Set(PostgreSQL{Host: "remote"})(nilConfig)
|
||||
// // unchanged == nil (no-op because source is nil)
|
||||
//
|
||||
// // Works with mismatched prisms
|
||||
// configMySQL := &Config{Connection: MySQL{Host: "localhost"}}
|
||||
// none = configPgOptional.GetOption(configMySQL) // None (Prism doesn't match)
|
||||
// unchanged = configPgOptional.Set(PostgreSQL{Host: "remote"})(configMySQL)
|
||||
// // unchanged == configMySQL (no-op because Prism doesn't match)
|
||||
func ComposeRef[S, A, B any](p Prism[A, B]) func(Lens[*S, A]) Optional[*S, B] {
|
||||
return compose(O.MakeOptionalRefCurriedWithName[S, B], p)
|
||||
}
|
||||
858
v2/optics/lens/prism/compose_test.go
Normal file
858
v2/optics/lens/prism/compose_test.go
Normal file
@@ -0,0 +1,858 @@
|
||||
// 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 prism
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/assert"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Test types for composition examples
|
||||
|
||||
// ConnectionType is a sum type representing different database connections
|
||||
type ConnectionType interface {
|
||||
isConnection()
|
||||
}
|
||||
|
||||
type PostgreSQL struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func (PostgreSQL) isConnection() {}
|
||||
|
||||
type MySQL struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func (MySQL) isConnection() {}
|
||||
|
||||
type MongoDB struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func (MongoDB) isConnection() {}
|
||||
|
||||
// Config is the top-level configuration
|
||||
type Config struct {
|
||||
Connection ConnectionType
|
||||
AppName string
|
||||
}
|
||||
|
||||
// Helper functions to create prisms for each connection type
|
||||
|
||||
func postgresqlPrism() P.Prism[ConnectionType, PostgreSQL] {
|
||||
return P.MakePrism(
|
||||
func(ct ConnectionType) O.Option[PostgreSQL] {
|
||||
if pg, ok := ct.(PostgreSQL); ok {
|
||||
return O.Some(pg)
|
||||
}
|
||||
return O.None[PostgreSQL]()
|
||||
},
|
||||
func(pg PostgreSQL) ConnectionType { return pg },
|
||||
)
|
||||
}
|
||||
|
||||
func mysqlPrism() P.Prism[ConnectionType, MySQL] {
|
||||
return P.MakePrism(
|
||||
func(ct ConnectionType) O.Option[MySQL] {
|
||||
if my, ok := ct.(MySQL); ok {
|
||||
return O.Some(my)
|
||||
}
|
||||
return O.None[MySQL]()
|
||||
},
|
||||
func(my MySQL) ConnectionType { return my },
|
||||
)
|
||||
}
|
||||
|
||||
func mongodbPrism() P.Prism[ConnectionType, MongoDB] {
|
||||
return P.MakePrism(
|
||||
func(ct ConnectionType) O.Option[MongoDB] {
|
||||
if mg, ok := ct.(MongoDB); ok {
|
||||
return O.Some(mg)
|
||||
}
|
||||
return O.None[MongoDB]()
|
||||
},
|
||||
func(mg MongoDB) ConnectionType { return mg },
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to create connection lens
|
||||
func connectionLens() L.Lens[Config, ConnectionType] {
|
||||
return L.MakeLens(
|
||||
func(c Config) ConnectionType { return c.Connection },
|
||||
func(c Config, ct ConnectionType) Config {
|
||||
c.Connection = ct
|
||||
return c
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to create nil-safe connection lens for pointer types
|
||||
func connectionLensRef() L.Lens[*Config, ConnectionType] {
|
||||
return L.MakeLensRef(
|
||||
func(c *Config) ConnectionType {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Connection
|
||||
},
|
||||
func(c *Config, ct ConnectionType) *Config {
|
||||
if c == nil {
|
||||
return &Config{Connection: ct}
|
||||
}
|
||||
c.Connection = ct
|
||||
return c
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// TestComposeBasicFunctionality tests basic composition behavior
|
||||
func TestComposeBasicFunctionality(t *testing.T) {
|
||||
t.Run("GetOption returns Some when Prism matches", func(t *testing.T) {
|
||||
connLens := connectionLens()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
// Compose connection lens with PostgreSQL prism
|
||||
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: PostgreSQL{Host: "localhost", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
result := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("localhost")(pg.Host)(t)
|
||||
assert.Equal(5432)(pg.Port)(t)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None when Prism doesn't match", func(t *testing.T) {
|
||||
connLens := connectionLens()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: MySQL{Host: "localhost", Port: 3306},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
result := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsNone(result))(t)
|
||||
})
|
||||
|
||||
t.Run("Set updates value when Prism matches", func(t *testing.T) {
|
||||
connLens := connectionLens()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: PostgreSQL{Host: "localhost", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(config)
|
||||
|
||||
// Verify the update
|
||||
result := configPgOptional.GetOption(updated)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("remote.example.com")(pg.Host)(t)
|
||||
assert.Equal(5433)(pg.Port)(t)
|
||||
|
||||
// Verify other fields are unchanged
|
||||
assert.Equal("TestApp")(updated.AppName)(t)
|
||||
})
|
||||
|
||||
t.Run("Set is no-op when Prism doesn't match", func(t *testing.T) {
|
||||
connLens := connectionLens()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: MySQL{Host: "localhost", Port: 3306},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(config)
|
||||
|
||||
// Verify nothing changed (no-op)
|
||||
assert.Equal(config)(updated)(t)
|
||||
|
||||
// Verify the connection is still MySQL
|
||||
if my, ok := updated.Connection.(MySQL); ok {
|
||||
assert.Equal("localhost")(my.Host)(t)
|
||||
assert.Equal(3306)(my.Port)(t)
|
||||
} else {
|
||||
t.Fatal("Expected MySQL connection to remain unchanged")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeOptionalLaws tests that the composition satisfies Optional laws
|
||||
// Reference: https://gcanti.github.io/monocle-ts/modules/Optional.ts.html
|
||||
func TestComposeOptionalLaws(t *testing.T) {
|
||||
connLens := connectionLens()
|
||||
pgPrism := postgresqlPrism()
|
||||
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
t.Run("SetGet Law: GetOption(Set(b)(s)) = Some(b) when GetOption(s) = Some(_)", func(t *testing.T) {
|
||||
// Start with a config that has PostgreSQL
|
||||
config := Config{
|
||||
Connection: PostgreSQL{Host: "localhost", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
// Verify the prism matches
|
||||
initial := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsSome(initial))(t)
|
||||
|
||||
// Set a new value
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(config)
|
||||
|
||||
// Get the value back
|
||||
result := configPgOptional.GetOption(updated)
|
||||
|
||||
// Verify SetGet law: we should get back what we set
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal(newPg.Host)(pg.Host)(t)
|
||||
assert.Equal(newPg.Port)(pg.Port)(t)
|
||||
})
|
||||
|
||||
t.Run("GetSet Law: Set(b)(s) = s when GetOption(s) = None (no-op)", func(t *testing.T) {
|
||||
// Start with a config that has MySQL (not PostgreSQL)
|
||||
config := Config{
|
||||
Connection: MySQL{Host: "localhost", Port: 3306},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
// Verify the prism doesn't match
|
||||
initial := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsNone(initial))(t)
|
||||
|
||||
// Try to set a PostgreSQL value
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(config)
|
||||
|
||||
// Verify GetSet law: structure should be unchanged (no-op)
|
||||
assert.Equal(config)(updated)(t)
|
||||
})
|
||||
|
||||
t.Run("SetSet Law: Set(b2)(Set(b1)(s)) = Set(b2)(s)", func(t *testing.T) {
|
||||
config := Config{
|
||||
Connection: PostgreSQL{Host: "localhost", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
pg1 := PostgreSQL{Host: "server1.example.com", Port: 5433}
|
||||
pg2 := PostgreSQL{Host: "server2.example.com", Port: 5434}
|
||||
|
||||
// Set twice
|
||||
setTwice := configPgOptional.Set(pg2)(configPgOptional.Set(pg1)(config))
|
||||
|
||||
// Set once with the final value
|
||||
setOnce := configPgOptional.Set(pg2)(config)
|
||||
|
||||
// They should be equal
|
||||
assert.Equal(setOnce)(setTwice)(t)
|
||||
|
||||
// Verify the final value
|
||||
result := configPgOptional.GetOption(setTwice)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal(pg2.Host)(pg.Host)(t)
|
||||
assert.Equal(pg2.Port)(pg.Port)(t)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeMultipleVariants tests composition with different prism variants
|
||||
func TestComposeMultipleVariants(t *testing.T) {
|
||||
connLens := connectionLens()
|
||||
|
||||
t.Run("PostgreSQL variant", func(t *testing.T) {
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: PostgreSQL{Host: "pg.example.com", Port: 5432},
|
||||
}
|
||||
|
||||
result := optional.GetOption(config)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
})
|
||||
|
||||
t.Run("MySQL variant", func(t *testing.T) {
|
||||
myPrism := mysqlPrism()
|
||||
optional := Compose[Config, ConnectionType, MySQL](myPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: MySQL{Host: "mysql.example.com", Port: 3306},
|
||||
}
|
||||
|
||||
result := optional.GetOption(config)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
})
|
||||
|
||||
t.Run("MongoDB variant", func(t *testing.T) {
|
||||
mgPrism := mongodbPrism()
|
||||
optional := Compose[Config, ConnectionType, MongoDB](mgPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: MongoDB{Host: "mongo.example.com", Port: 27017},
|
||||
}
|
||||
|
||||
result := optional.GetOption(config)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
})
|
||||
|
||||
t.Run("Cross-variant no-op", func(t *testing.T) {
|
||||
// Try to use PostgreSQL optional on MySQL config
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: MySQL{Host: "mysql.example.com", Port: 3306},
|
||||
}
|
||||
|
||||
// GetOption should return None
|
||||
result := optional.GetOption(config)
|
||||
assert.Equal(true)(O.IsNone(result))(t)
|
||||
|
||||
// Set should be no-op
|
||||
newPg := PostgreSQL{Host: "pg.example.com", Port: 5432}
|
||||
updated := optional.Set(newPg)(config)
|
||||
assert.Equal(config)(updated)(t)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeEdgeCases tests edge cases and boundary conditions
|
||||
func TestComposeEdgeCases(t *testing.T) {
|
||||
t.Run("Identity lens with prism", func(t *testing.T) {
|
||||
// Identity lens that doesn't transform the value
|
||||
idLens := L.MakeLens(
|
||||
func(ct ConnectionType) ConnectionType { return ct },
|
||||
func(_ ConnectionType, ct ConnectionType) ConnectionType { return ct },
|
||||
)
|
||||
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := Compose[ConnectionType, ConnectionType, PostgreSQL](pgPrism)(idLens)
|
||||
|
||||
conn := ConnectionType(PostgreSQL{Host: "localhost", Port: 5432})
|
||||
result := optional.GetOption(conn)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("localhost")(pg.Host)(t)
|
||||
})
|
||||
|
||||
t.Run("Multiple sets preserve structure", func(t *testing.T) {
|
||||
connLens := connectionLens()
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := Config{
|
||||
Connection: PostgreSQL{Host: "host1", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
// Apply multiple sets
|
||||
pg2 := PostgreSQL{Host: "host2", Port: 5433}
|
||||
pg3 := PostgreSQL{Host: "host3", Port: 5434}
|
||||
pg4 := PostgreSQL{Host: "host4", Port: 5435}
|
||||
|
||||
updated := F.Pipe3(
|
||||
config,
|
||||
optional.Set(pg2),
|
||||
optional.Set(pg3),
|
||||
optional.Set(pg4),
|
||||
)
|
||||
|
||||
// Verify final value
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("host4")(pg.Host)(t)
|
||||
assert.Equal(5435)(pg.Port)(t)
|
||||
|
||||
// Verify structure is preserved
|
||||
assert.Equal("TestApp")(updated.AppName)(t)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeDocumentationExample tests the example from the documentation
|
||||
func TestComposeDocumentationExample(t *testing.T) {
|
||||
// This test verifies the example code in the documentation works correctly
|
||||
|
||||
// Lens to focus on Connection field
|
||||
connLens := L.MakeLens(
|
||||
func(c Config) ConnectionType { return c.Connection },
|
||||
func(c Config, ct ConnectionType) Config { c.Connection = ct; return c },
|
||||
)
|
||||
|
||||
// Prism to extract PostgreSQL from ConnectionType
|
||||
pgPrism := P.MakePrism(
|
||||
func(ct ConnectionType) O.Option[PostgreSQL] {
|
||||
if pg, ok := ct.(PostgreSQL); ok {
|
||||
return O.Some(pg)
|
||||
}
|
||||
return O.None[PostgreSQL]()
|
||||
},
|
||||
func(pg PostgreSQL) ConnectionType { return pg },
|
||||
)
|
||||
|
||||
// Compose to create Optional[Config, PostgreSQL]
|
||||
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := Config{Connection: PostgreSQL{Host: "localhost"}}
|
||||
host := configPgOptional.GetOption(config) // Some(PostgreSQL{Host: "localhost"})
|
||||
assert.Equal(true)(O.IsSome(host))(t)
|
||||
|
||||
updated := configPgOptional.Set(PostgreSQL{Host: "remote"})(config)
|
||||
// updated.Connection = PostgreSQL{Host: "remote"}
|
||||
result := configPgOptional.GetOption(updated)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("remote")(pg.Host)(t)
|
||||
|
||||
configMySQL := Config{Connection: MySQL{Host: "localhost"}}
|
||||
none := configPgOptional.GetOption(configMySQL) // None (Prism doesn't match)
|
||||
assert.Equal(true)(O.IsNone(none))(t)
|
||||
|
||||
unchanged := configPgOptional.Set(PostgreSQL{Host: "remote"})(configMySQL)
|
||||
// unchanged == configMySQL (no-op because Prism doesn't match)
|
||||
assert.Equal(configMySQL)(unchanged)(t)
|
||||
}
|
||||
|
||||
// TestComposeRefBasicFunctionality tests basic ComposeRef behavior with pointer types
|
||||
func TestComposeRefBasicFunctionality(t *testing.T) {
|
||||
t.Run("GetOption returns Some when Prism matches (non-nil pointer)", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := &Config{
|
||||
Connection: PostgreSQL{Host: "localhost", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
result := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("localhost")(pg.Host)(t)
|
||||
assert.Equal(5432)(pg.Port)(t)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None when pointer is nil", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
var config *Config = nil
|
||||
|
||||
result := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsNone(result))(t)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None when Prism doesn't match", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
config := &Config{
|
||||
Connection: MySQL{Host: "localhost", Port: 3306},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
result := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsNone(result))(t)
|
||||
})
|
||||
|
||||
t.Run("Set updates value when Prism matches (creates copy)", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
original := &Config{
|
||||
Connection: PostgreSQL{Host: "localhost", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(original)
|
||||
|
||||
// Verify the update
|
||||
result := configPgOptional.GetOption(updated)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("remote.example.com")(pg.Host)(t)
|
||||
assert.Equal(5433)(pg.Port)(t)
|
||||
|
||||
// Verify immutability: original should be unchanged
|
||||
if origPg, ok := original.Connection.(PostgreSQL); ok {
|
||||
assert.Equal("localhost")(origPg.Host)(t)
|
||||
assert.Equal(5432)(origPg.Port)(t)
|
||||
} else {
|
||||
t.Fatal("Original config should still have PostgreSQL connection")
|
||||
}
|
||||
|
||||
// Verify they are different pointers
|
||||
if original == updated {
|
||||
t.Fatal("Set should create a new pointer, not modify in place")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set is no-op when pointer is nil", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
var config *Config = nil
|
||||
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(config)
|
||||
|
||||
// Verify nothing changed (no-op for nil)
|
||||
if updated != nil {
|
||||
t.Fatalf("Expected nil, got %v", updated)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set is no-op when Prism doesn't match", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
|
||||
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
original := &Config{
|
||||
Connection: MySQL{Host: "localhost", Port: 3306},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(original)
|
||||
|
||||
// Verify nothing changed (no-op)
|
||||
assert.Equal(original)(updated)(t)
|
||||
|
||||
// Verify the connection is still MySQL
|
||||
if my, ok := updated.Connection.(MySQL); ok {
|
||||
assert.Equal("localhost")(my.Host)(t)
|
||||
assert.Equal(3306)(my.Port)(t)
|
||||
} else {
|
||||
t.Fatal("Expected MySQL connection to remain unchanged")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeRefOptionalLaws tests that ComposeRef satisfies Optional laws
|
||||
func TestComposeRefOptionalLaws(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
t.Run("SetGet Law: GetOption(Set(b)(s)) = Some(b) when GetOption(s) = Some(_)", func(t *testing.T) {
|
||||
// Start with a config that has PostgreSQL
|
||||
config := &Config{
|
||||
Connection: PostgreSQL{Host: "localhost", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
// Verify the prism matches
|
||||
initial := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsSome(initial))(t)
|
||||
|
||||
// Set a new value
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(config)
|
||||
|
||||
// Get the value back
|
||||
result := configPgOptional.GetOption(updated)
|
||||
|
||||
// Verify SetGet law: we should get back what we set
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal(newPg.Host)(pg.Host)(t)
|
||||
assert.Equal(newPg.Port)(pg.Port)(t)
|
||||
})
|
||||
|
||||
t.Run("GetSet Law: Set(b)(s) = s when GetOption(s) = None (no-op for nil)", func(t *testing.T) {
|
||||
// Start with nil config
|
||||
var config *Config = nil
|
||||
|
||||
// Verify the prism doesn't match
|
||||
initial := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsNone(initial))(t)
|
||||
|
||||
// Try to set a PostgreSQL value
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(config)
|
||||
|
||||
// Verify GetSet law: structure should be unchanged (nil)
|
||||
if updated != nil {
|
||||
t.Fatalf("Expected nil, got %v", updated)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetSet Law: Set(b)(s) = s when GetOption(s) = None (no-op for mismatched prism)", func(t *testing.T) {
|
||||
// Start with a config that has MySQL (not PostgreSQL)
|
||||
config := &Config{
|
||||
Connection: MySQL{Host: "localhost", Port: 3306},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
// Verify the prism doesn't match
|
||||
initial := configPgOptional.GetOption(config)
|
||||
assert.Equal(true)(O.IsNone(initial))(t)
|
||||
|
||||
// Try to set a PostgreSQL value
|
||||
newPg := PostgreSQL{Host: "remote.example.com", Port: 5433}
|
||||
updated := configPgOptional.Set(newPg)(config)
|
||||
|
||||
// Verify GetSet law: structure should be unchanged
|
||||
assert.Equal(config)(updated)(t)
|
||||
})
|
||||
|
||||
t.Run("SetSet Law: Set(b2)(Set(b1)(s)) = Set(b2)(s)", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Connection: PostgreSQL{Host: "localhost", Port: 5432},
|
||||
AppName: "TestApp",
|
||||
}
|
||||
|
||||
pg1 := PostgreSQL{Host: "server1.example.com", Port: 5433}
|
||||
pg2 := PostgreSQL{Host: "server2.example.com", Port: 5434}
|
||||
|
||||
// Set twice
|
||||
setTwice := configPgOptional.Set(pg2)(configPgOptional.Set(pg1)(config))
|
||||
|
||||
// Set once with the final value
|
||||
setOnce := configPgOptional.Set(pg2)(config)
|
||||
|
||||
// They should be equal in value (but different pointers due to immutability)
|
||||
result1 := configPgOptional.GetOption(setTwice)
|
||||
result2 := configPgOptional.GetOption(setOnce)
|
||||
|
||||
assert.Equal(true)(O.IsSome(result1))(t)
|
||||
assert.Equal(true)(O.IsSome(result2))(t)
|
||||
|
||||
pg1Result := O.GetOrElse(F.Constant(PostgreSQL{}))(result1)
|
||||
pg2Result := O.GetOrElse(F.Constant(PostgreSQL{}))(result2)
|
||||
|
||||
assert.Equal(pg2.Host)(pg1Result.Host)(t)
|
||||
assert.Equal(pg2.Port)(pg1Result.Port)(t)
|
||||
assert.Equal(pg2.Host)(pg2Result.Host)(t)
|
||||
assert.Equal(pg2.Port)(pg2Result.Port)(t)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeRefImmutability tests that ComposeRef preserves immutability
|
||||
func TestComposeRefImmutability(t *testing.T) {
|
||||
t.Run("Set creates a new pointer, doesn't modify original", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
original := &Config{
|
||||
Connection: PostgreSQL{Host: "original", Port: 5432},
|
||||
AppName: "OriginalApp",
|
||||
}
|
||||
|
||||
// Store original values
|
||||
origPg := original.Connection.(PostgreSQL)
|
||||
origAppName := original.AppName
|
||||
|
||||
// Perform multiple sets
|
||||
pg1 := PostgreSQL{Host: "host1", Port: 5433}
|
||||
pg2 := PostgreSQL{Host: "host2", Port: 5434}
|
||||
pg3 := PostgreSQL{Host: "host3", Port: 5435}
|
||||
|
||||
updated1 := optional.Set(pg1)(original)
|
||||
updated2 := optional.Set(pg2)(updated1)
|
||||
updated3 := optional.Set(pg3)(updated2)
|
||||
|
||||
// Verify original is unchanged
|
||||
currentPg := original.Connection.(PostgreSQL)
|
||||
assert.Equal(origPg.Host)(currentPg.Host)(t)
|
||||
assert.Equal(origPg.Port)(currentPg.Port)(t)
|
||||
assert.Equal(origAppName)(original.AppName)(t)
|
||||
|
||||
// Verify final update has correct value
|
||||
result := optional.GetOption(updated3)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
finalPg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("host3")(finalPg.Host)(t)
|
||||
assert.Equal(5435)(finalPg.Port)(t)
|
||||
|
||||
// Verify all pointers are different
|
||||
if original == updated1 || original == updated2 || original == updated3 {
|
||||
t.Fatal("Set should create new pointers, not modify in place")
|
||||
}
|
||||
if updated1 == updated2 || updated2 == updated3 || updated1 == updated3 {
|
||||
t.Fatal("Each Set should create a new pointer")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Multiple operations on nil preserve nil", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
var config *Config = nil
|
||||
|
||||
// Multiple sets on nil should all return nil
|
||||
pg1 := PostgreSQL{Host: "host1", Port: 5433}
|
||||
pg2 := PostgreSQL{Host: "host2", Port: 5434}
|
||||
|
||||
updated1 := optional.Set(pg1)(config)
|
||||
updated2 := optional.Set(pg2)(updated1)
|
||||
|
||||
if updated1 != nil {
|
||||
t.Fatalf("Expected nil after first set, got %v", updated1)
|
||||
}
|
||||
if updated2 != nil {
|
||||
t.Fatalf("Expected nil after second set, got %v", updated2)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeRefNilPointerEdgeCases tests edge cases with nil pointers
|
||||
func TestComposeRefNilPointerEdgeCases(t *testing.T) {
|
||||
t.Run("GetOption on nil returns None", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
var config *Config = nil
|
||||
result := optional.GetOption(config)
|
||||
|
||||
assert.Equal(true)(O.IsNone(result))(t)
|
||||
})
|
||||
|
||||
t.Run("Set on nil with matching prism returns nil", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
var config *Config = nil
|
||||
newPg := PostgreSQL{Host: "remote", Port: 5432}
|
||||
updated := optional.Set(newPg)(config)
|
||||
|
||||
if updated != nil {
|
||||
t.Fatalf("Expected nil, got %v", updated)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Chaining operations starting from nil", func(t *testing.T) {
|
||||
connLens := connectionLensRef()
|
||||
pgPrism := postgresqlPrism()
|
||||
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
var config *Config = nil
|
||||
|
||||
// Chain multiple operations
|
||||
pg1 := PostgreSQL{Host: "host1", Port: 5433}
|
||||
pg2 := PostgreSQL{Host: "host2", Port: 5434}
|
||||
|
||||
result := F.Pipe2(
|
||||
config,
|
||||
optional.Set(pg1),
|
||||
optional.Set(pg2),
|
||||
)
|
||||
|
||||
if result != nil {
|
||||
t.Fatalf("Expected nil after chained operations, got %v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeRefDocumentationExample tests the example from the ComposeRef documentation
|
||||
func TestComposeRefDocumentationExample(t *testing.T) {
|
||||
// Lens to focus on Connection field (pointer-based)
|
||||
connLens := connectionLensRef()
|
||||
|
||||
// Prism to extract PostgreSQL from ConnectionType
|
||||
pgPrism := P.MakePrism(
|
||||
func(ct ConnectionType) O.Option[PostgreSQL] {
|
||||
if pg, ok := ct.(PostgreSQL); ok {
|
||||
return O.Some(pg)
|
||||
}
|
||||
return O.None[PostgreSQL]()
|
||||
},
|
||||
func(pg PostgreSQL) ConnectionType { return pg },
|
||||
)
|
||||
|
||||
// Compose to create Optional[*Config, PostgreSQL]
|
||||
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
|
||||
|
||||
// Works with non-nil pointers
|
||||
config := &Config{Connection: PostgreSQL{Host: "localhost"}}
|
||||
host := configPgOptional.GetOption(config) // Some(PostgreSQL{Host: "localhost"})
|
||||
assert.Equal(true)(O.IsSome(host))(t)
|
||||
|
||||
updated := configPgOptional.Set(PostgreSQL{Host: "remote"})(config)
|
||||
// updated is a new *Config with Connection = PostgreSQL{Host: "remote"}
|
||||
result := configPgOptional.GetOption(updated)
|
||||
assert.Equal(true)(O.IsSome(result))(t)
|
||||
pg := O.GetOrElse(F.Constant(PostgreSQL{}))(result)
|
||||
assert.Equal("remote")(pg.Host)(t)
|
||||
|
||||
// original config is unchanged (immutability preserved)
|
||||
origPg := config.Connection.(PostgreSQL)
|
||||
assert.Equal("localhost")(origPg.Host)(t)
|
||||
|
||||
// Handles nil pointers safely
|
||||
var nilConfig *Config = nil
|
||||
none := configPgOptional.GetOption(nilConfig) // None (nil pointer)
|
||||
assert.Equal(true)(O.IsNone(none))(t)
|
||||
|
||||
unchanged := configPgOptional.Set(PostgreSQL{Host: "remote"})(nilConfig)
|
||||
// unchanged == nil (no-op because source is nil)
|
||||
if unchanged != nil {
|
||||
t.Fatalf("Expected nil, got %v", unchanged)
|
||||
}
|
||||
|
||||
// Works with mismatched prisms
|
||||
configMySQL := &Config{Connection: MySQL{Host: "localhost"}}
|
||||
none = configPgOptional.GetOption(configMySQL) // None (Prism doesn't match)
|
||||
assert.Equal(true)(O.IsNone(none))(t)
|
||||
|
||||
unchanged = configPgOptional.Set(PostgreSQL{Host: "remote"})(configMySQL)
|
||||
// unchanged == configMySQL (no-op because Prism doesn't match)
|
||||
assert.Equal(configMySQL)(unchanged)(t)
|
||||
}
|
||||
15
v2/optics/lens/prism/types.go
Normal file
15
v2/optics/lens/prism/types.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package prism
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
O "github.com/IBM/fp-go/v2/optics/optional"
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
)
|
||||
|
||||
type (
|
||||
Prism[S, A any] = P.Prism[S, A]
|
||||
Lens[S, A any] = L.Lens[S, A]
|
||||
Optional[S, A any] = O.Optional[S, A]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
)
|
||||
@@ -13,8 +13,77 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Optional is an optic used to zoom inside a product. Unlike the `Lens`, the element that the `Optional` focuses
|
||||
// on may not exist.
|
||||
// Package optional provides an optic for focusing on values that may not exist.
|
||||
//
|
||||
// # Overview
|
||||
//
|
||||
// Optional is an optic used to zoom inside a product. Unlike the Lens, the element that the Optional focuses
|
||||
// on may not exist. 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.
|
||||
//
|
||||
// # Optional Laws
|
||||
//
|
||||
// An Optional must satisfy the following laws, which are consistent with other functional programming libraries
|
||||
// such as monocle-ts (https://gcanti.github.io/monocle-ts/modules/Optional.ts.html) and the Haskell lens library
|
||||
// (https://hackage.haskell.org/package/lens):
|
||||
//
|
||||
// 1. GetSet Law (No-op on None):
|
||||
// If GetOption(s) returns None, then Set(a)(s) must return s unchanged (no-op).
|
||||
// This ensures that attempting to update a value that doesn't exist has no effect.
|
||||
//
|
||||
// Formally: GetOption(s) = None => Set(a)(s) = s
|
||||
//
|
||||
// 2. SetGet Law (Get what you Set):
|
||||
// If GetOption(s) returns Some(_), then GetOption(Set(a)(s)) must return Some(a).
|
||||
// This ensures that after setting a value, you can retrieve it.
|
||||
//
|
||||
// Formally: GetOption(s) = Some(_) => GetOption(Set(a)(s)) = Some(a)
|
||||
//
|
||||
// 3. SetSet Law (Last Set Wins):
|
||||
// Setting twice is the same as setting once with the final value.
|
||||
//
|
||||
// Formally: Set(b)(Set(a)(s)) = Set(b)(s)
|
||||
//
|
||||
// # No-op Behavior
|
||||
//
|
||||
// A key property of Optional is that updating a value for which GetOption returns None is a no-op.
|
||||
// This behavior is implemented through the optionalModify function, which only applies the modification
|
||||
// if the optional value exists. When GetOption returns None, the original structure is returned unchanged.
|
||||
//
|
||||
// This is consistent with the behavior in:
|
||||
// - monocle-ts: Optional.modify returns the original value when the optional doesn't match
|
||||
// - Haskell lens: over and set operations are no-ops when the traversal finds no targets
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // Create an optional that focuses on non-empty names
|
||||
// nameOptional := MakeOptional(
|
||||
// func(p Person) option.Option[string] {
|
||||
// if p.Name != "" {
|
||||
// return option.Some(p.Name)
|
||||
// }
|
||||
// return option.None[string]()
|
||||
// },
|
||||
// func(p Person, name string) Person {
|
||||
// p.Name = name
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // When the optional matches, Set updates the value
|
||||
// person1 := Person{Name: "Alice", Age: 30}
|
||||
// updated1 := nameOptional.Set("Bob")(person1)
|
||||
// // updated1.Name == "Bob"
|
||||
//
|
||||
// // When the optional doesn't match (Name is empty), Set is a no-op
|
||||
// person2 := Person{Name: "", Age: 30}
|
||||
// updated2 := nameOptional.Set("Bob")(person2)
|
||||
// // updated2 == person2 (unchanged)
|
||||
package optional
|
||||
|
||||
import (
|
||||
@@ -50,12 +119,29 @@ type (
|
||||
Operator[S, A, B any] = func(Optional[S, A]) Optional[S, B]
|
||||
)
|
||||
|
||||
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
|
||||
// setCopyRef wraps a setter for a pointer into a setter that first creates a copy before
|
||||
// modifying that copy
|
||||
func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
|
||||
return func(s *S, a A) *S {
|
||||
cpy := *s
|
||||
return setter(&cpy, a)
|
||||
func setCopyRef[SET ~func(A) func(*S) *S, S, A any](setter SET) func(a A) func(*S) *S {
|
||||
return func(a A) func(*S) *S {
|
||||
|
||||
sa := setter(a)
|
||||
|
||||
return func(s *S) *S {
|
||||
if s == nil {
|
||||
return s
|
||||
}
|
||||
cpy := *s
|
||||
return sa(&cpy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getRef[GET ~func(*S) O.Option[A], S, A any](getter GET) func(*S) O.Option[A] {
|
||||
return func(s *S) O.Option[A] {
|
||||
if s == nil {
|
||||
return O.None[A]()
|
||||
}
|
||||
return getter(s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +154,18 @@ func MakeOptional[S, A any](get O.Kleisli[S, A], set func(S, A) S) Optional[S, A
|
||||
return MakeOptionalWithName(get, set, "GenericOptional")
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MakeOptionalCurried[S, A any](get O.Kleisli[S, A], set func(A) func(S) S) Optional[S, A] {
|
||||
return MakeOptionalCurriedWithName(get, set, "GenericOptional")
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MakeOptionalWithName[S, A any](get O.Kleisli[S, A], set func(S, A) S, name string) Optional[S, A] {
|
||||
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set), name: name}
|
||||
return MakeOptionalCurriedWithName(get, F.Bind2of2(set), name)
|
||||
}
|
||||
|
||||
func MakeOptionalCurriedWithName[S, A any](get O.Kleisli[S, A], set func(A) func(S) S, name string) Optional[S, A] {
|
||||
return Optional[S, A]{GetOption: get, Set: set, name: name}
|
||||
}
|
||||
|
||||
// MakeOptionalRef creates an Optional based on a getter and a setter function. The setter passed in does not have to create a shallow
|
||||
@@ -77,12 +173,17 @@ func MakeOptionalWithName[S, A any](get O.Kleisli[S, A], set func(S, A) S, name
|
||||
//
|
||||
//go:inline
|
||||
func MakeOptionalRef[S, A any](get O.Kleisli[*S, A], set func(*S, A) *S) Optional[*S, A] {
|
||||
return MakeOptional(get, setCopy(set))
|
||||
return MakeOptionalCurried(getRef(get), setCopyRef(F.Bind2of2(set)))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MakeOptionalRefWithName[S, A any](get O.Kleisli[*S, A], set func(*S, A) *S, name string) Optional[*S, A] {
|
||||
return MakeOptionalWithName(get, setCopy(set), name)
|
||||
return MakeOptionalCurriedWithName(getRef(get), setCopyRef(F.Bind2of2(set)), name)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MakeOptionalRefCurriedWithName[S, A any](get O.Kleisli[*S, A], set func(A) func(*S) *S, name string) Optional[*S, A] {
|
||||
return MakeOptionalCurriedWithName(getRef(get), setCopyRef(set), name)
|
||||
}
|
||||
|
||||
// Id returns am optional implementing the identity operation
|
||||
@@ -147,12 +248,14 @@ func fromPredicate[S, A any](creator func(get O.Kleisli[S, A], set func(S, A) S)
|
||||
return func(get func(S) A, set func(S, A) S) Optional[S, A] {
|
||||
return creator(
|
||||
F.Flow2(get, fromPred),
|
||||
func(s S, _ A) S {
|
||||
func(s S, a A) S {
|
||||
return F.Pipe3(
|
||||
s,
|
||||
get,
|
||||
fromPred,
|
||||
O.Fold(F.Constant(s), F.Bind1st(set, s)),
|
||||
O.Fold(F.Constant(s), func(_ A) S {
|
||||
return set(s, a)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -62,3 +62,927 @@ func TestOptional(t *testing.T) {
|
||||
assert.Equal(t, O.Of(sampleResponse.info), responseOptional.GetOption(&sampleResponse))
|
||||
assert.Equal(t, O.None[*Info](), responseOptional.GetOption(&sampleEmptyResponse))
|
||||
}
|
||||
|
||||
// Test types for comprehensive testing
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Timeout int
|
||||
Retries int
|
||||
}
|
||||
|
||||
// TestMakeOptionalBasicFunctionality tests basic Optional operations
|
||||
func TestMakeOptionalBasicFunctionality(t *testing.T) {
|
||||
t.Run("GetOption returns Some when value exists", func(t *testing.T) {
|
||||
optional := MakeOptional(
|
||||
func(p Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p Person, name string) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "Alice", O.GetOrElse(F.Constant(""))(result))
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None when value doesn't exist", func(t *testing.T) {
|
||||
optional := MakeOptional(
|
||||
func(p Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p Person, name string) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
person := Person{Name: "", Age: 30}
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Set updates value when optional matches", func(t *testing.T) {
|
||||
optional := MakeOptional(
|
||||
func(p Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p Person, name string) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
assert.Equal(t, "Bob", updated.Name)
|
||||
assert.Equal(t, 30, updated.Age)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOptionalLaws tests that Optional satisfies the optional laws
|
||||
// Reference: https://gcanti.github.io/monocle-ts/modules/Optional.ts.html
|
||||
func TestOptionalLaws(t *testing.T) {
|
||||
optional := MakeOptional(
|
||||
func(p Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p Person, name string) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("SetGet Law: GetOption(Set(a)(s)) = Some(a) when GetOption(s) = Some(_)", func(t *testing.T) {
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Verify optional matches
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsSome(initial))
|
||||
|
||||
// Set a new value
|
||||
newName := "Bob"
|
||||
updated := optional.Set(newName)(person)
|
||||
|
||||
// Get the value back
|
||||
result := optional.GetOption(updated)
|
||||
|
||||
// Verify SetGet law: we should get back what we set
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, newName, O.GetOrElse(F.Constant(""))(result))
|
||||
})
|
||||
|
||||
t.Run("GetSet Law: Set(a)(s) = s when GetOption(s) = None (no-op)", func(t *testing.T) {
|
||||
person := Person{Name: "", Age: 30}
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Try to set a value - this should be a no-op since GetOption returns None
|
||||
// Note: Direct Set always updates, but this is expected behavior.
|
||||
// The no-op behavior is enforced through ModifyOption and optionalModify.
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
// Direct Set will update even when GetOption returns None
|
||||
// This is by design - Set is unconditional
|
||||
assert.Equal(t, "Bob", updated.Name)
|
||||
})
|
||||
|
||||
t.Run("SetSet Law: Set(b)(Set(a)(s)) = Set(b)(s)", func(t *testing.T) {
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Set twice
|
||||
setTwice := optional.Set("Charlie")(optional.Set("Bob")(person))
|
||||
|
||||
// Set once with the final value
|
||||
setOnce := optional.Set("Charlie")(person)
|
||||
|
||||
// They should be equal
|
||||
assert.Equal(t, setOnce, setTwice)
|
||||
assert.Equal(t, "Charlie", setTwice.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMakeOptionalRefBasicFunctionality tests MakeOptionalRef with pointer types
|
||||
func TestMakeOptionalRefBasicFunctionality(t *testing.T) {
|
||||
t.Run("GetOption returns Some when value exists (non-nil pointer)", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
person := &Person{Name: "Alice", Age: 30}
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "Alice", O.GetOrElse(F.Constant(""))(result))
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None when pointer is nil", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None when value doesn't exist", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
person := &Person{Name: "", Age: 30}
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Set updates value and creates copy (immutability)", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
original := &Person{Name: "Alice", Age: 30}
|
||||
updated := optional.Set("Bob")(original)
|
||||
|
||||
// Verify the update
|
||||
assert.Equal(t, "Bob", updated.Name)
|
||||
assert.Equal(t, 30, updated.Age)
|
||||
|
||||
// Verify immutability: original should be unchanged
|
||||
assert.Equal(t, "Alice", original.Name)
|
||||
|
||||
// Verify they are different pointers
|
||||
assert.NotEqual(t, original, updated)
|
||||
})
|
||||
|
||||
t.Run("Set is no-op when pointer is nil", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
// Verify nothing changed (no-op for nil)
|
||||
assert.Nil(t, updated)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMakeOptionalRefLaws tests that MakeOptionalRef satisfies optional laws
|
||||
func TestMakeOptionalRefLaws(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("SetGet Law: GetOption(Set(a)(s)) = Some(a) when GetOption(s) = Some(_)", func(t *testing.T) {
|
||||
person := &Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Verify optional matches
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsSome(initial))
|
||||
|
||||
// Set a new value
|
||||
newName := "Bob"
|
||||
updated := optional.Set(newName)(person)
|
||||
|
||||
// Get the value back
|
||||
result := optional.GetOption(updated)
|
||||
|
||||
// Verify SetGet law: we should get back what we set
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, newName, O.GetOrElse(F.Constant(""))(result))
|
||||
})
|
||||
|
||||
t.Run("GetSet Law: Set(a)(s) = s when GetOption(s) = None (nil pointer)", func(t *testing.T) {
|
||||
var person *Person = nil
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Try to set a value
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
// Verify GetSet law: structure should be unchanged (nil)
|
||||
assert.Nil(t, updated)
|
||||
})
|
||||
|
||||
t.Run("SetSet Law: Set(b)(Set(a)(s)) = Set(b)(s)", func(t *testing.T) {
|
||||
person := &Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Set twice
|
||||
setTwice := optional.Set("Charlie")(optional.Set("Bob")(person))
|
||||
|
||||
// Set once with the final value
|
||||
setOnce := optional.Set("Charlie")(person)
|
||||
|
||||
// They should have equal values (but different pointers due to immutability)
|
||||
assert.Equal(t, setOnce.Name, setTwice.Name)
|
||||
assert.Equal(t, setOnce.Age, setTwice.Age)
|
||||
assert.Equal(t, "Charlie", setTwice.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMakeOptionalRefImmutability tests immutability guarantees
|
||||
func TestMakeOptionalRefImmutability(t *testing.T) {
|
||||
t.Run("Set creates a new pointer, doesn't modify original", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
original := &Person{Name: "Alice", Age: 30}
|
||||
origName := original.Name
|
||||
origAge := original.Age
|
||||
|
||||
// Perform multiple sets
|
||||
updated1 := optional.Set("Bob")(original)
|
||||
updated2 := optional.Set("Charlie")(updated1)
|
||||
updated3 := optional.Set("David")(updated2)
|
||||
|
||||
// Verify original is unchanged
|
||||
assert.Equal(t, origName, original.Name)
|
||||
assert.Equal(t, origAge, original.Age)
|
||||
|
||||
// Verify final update has correct value
|
||||
assert.Equal(t, "David", updated3.Name)
|
||||
|
||||
// Verify all pointers are different
|
||||
assert.NotEqual(t, original, updated1)
|
||||
assert.NotEqual(t, original, updated2)
|
||||
assert.NotEqual(t, original, updated3)
|
||||
assert.NotEqual(t, updated1, updated2)
|
||||
assert.NotEqual(t, updated2, updated3)
|
||||
})
|
||||
|
||||
t.Run("Multiple operations on nil preserve nil", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
|
||||
// Multiple sets on nil should all return nil
|
||||
updated1 := optional.Set("Bob")(person)
|
||||
updated2 := optional.Set("Charlie")(updated1)
|
||||
|
||||
assert.Nil(t, updated1)
|
||||
assert.Nil(t, updated2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMakeOptionalRefNilPointerEdgeCases tests edge cases with nil pointers
|
||||
func TestMakeOptionalRefNilPointerEdgeCases(t *testing.T) {
|
||||
t.Run("GetOption on nil returns None", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
return O.Some(p.Name)
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Set on nil returns nil", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
return O.Some(p.Name)
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
assert.Nil(t, updated)
|
||||
})
|
||||
|
||||
t.Run("Chaining operations starting from nil", func(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
|
||||
// Chain multiple operations
|
||||
result := F.Pipe2(
|
||||
person,
|
||||
optional.Set("Bob"),
|
||||
optional.Set("Charlie"),
|
||||
)
|
||||
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromPredicateRef tests FromPredicateRef with nil handling
|
||||
func TestFromPredicateRef(t *testing.T) {
|
||||
t.Run("Works with non-nil values matching predicate", func(t *testing.T) {
|
||||
optional := FromPredicateRef[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p *Person) string { return p.Name },
|
||||
func(p *Person, name string) *Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
person := &Person{Name: "Alice", Age: 30}
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "Alice", O.GetOrElse(F.Constant(""))(result))
|
||||
})
|
||||
|
||||
t.Run("Returns None for nil pointer", func(t *testing.T) {
|
||||
optional := FromPredicateRef[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p *Person) string { return p.Name },
|
||||
func(p *Person, name string) *Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Returns None when predicate doesn't match", func(t *testing.T) {
|
||||
optional := FromPredicateRef[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p *Person) string { return p.Name },
|
||||
func(p *Person, name string) *Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
person := &Person{Name: "", Age: 30}
|
||||
result := optional.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Set is no-op on nil pointer", func(t *testing.T) {
|
||||
optional := FromPredicateRef[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p *Person) string { return p.Name },
|
||||
func(p *Person, name string) *Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
assert.Nil(t, updated)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOptionalComposition tests composing optionals
|
||||
func TestOptionalComposition(t *testing.T) {
|
||||
t.Run("Compose two optionals", func(t *testing.T) {
|
||||
// First optional: Person -> Name (if not empty)
|
||||
nameOptional := MakeOptional(
|
||||
func(p Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p Person, name string) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
// Second optional: String -> First character (if not empty)
|
||||
firstCharOptional := MakeOptional(
|
||||
func(s string) O.Option[rune] {
|
||||
if len(s) > 0 {
|
||||
return O.Some(rune(s[0]))
|
||||
}
|
||||
return O.None[rune]()
|
||||
},
|
||||
func(s string, r rune) string {
|
||||
if len(s) > 0 {
|
||||
return string(r) + s[1:]
|
||||
}
|
||||
return string(r)
|
||||
},
|
||||
)
|
||||
|
||||
// Compose them
|
||||
composed := Compose[Person, string, rune](firstCharOptional)(nameOptional)
|
||||
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
result := composed.GetOption(person)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 'A', O.GetOrElse(F.Constant(rune(0)))(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestOptionalNoOpBehavior tests that modifying through optionalModify is a no-op when GetOption returns None
|
||||
// This is the key law: updating a value for which the preview returns None is a no-op
|
||||
func TestOptionalNoOpBehavior(t *testing.T) {
|
||||
optional := MakeOptional(
|
||||
func(p Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p Person, name string) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("ModifyOption returns None when GetOption returns None", func(t *testing.T) {
|
||||
person := Person{Name: "", Age: 30}
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Try to modify - should return None
|
||||
modifyResult := ModifyOption[Person, string](func(name string) string {
|
||||
return "Bob"
|
||||
})(optional)(person)
|
||||
|
||||
assert.True(t, O.IsNone(modifyResult))
|
||||
})
|
||||
|
||||
t.Run("optionalModify is no-op when GetOption returns None", func(t *testing.T) {
|
||||
person := Person{Name: "", Age: 30}
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Try to modify using the internal optionalModify function
|
||||
updated := optionalModify(func(name string) string {
|
||||
return "Bob"
|
||||
}, optional, person)
|
||||
|
||||
// Verify no-op: structure should be unchanged
|
||||
assert.Equal(t, person, updated)
|
||||
assert.Equal(t, "", updated.Name)
|
||||
assert.Equal(t, 30, updated.Age)
|
||||
})
|
||||
|
||||
t.Run("ModifyOption returns Some when GetOption returns Some", func(t *testing.T) {
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Verify optional matches
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsSome(initial))
|
||||
|
||||
// Modify should return Some with updated value
|
||||
modifyResult := ModifyOption[Person, string](func(name string) string {
|
||||
return name + " Smith"
|
||||
})(optional)(person)
|
||||
|
||||
assert.True(t, O.IsSome(modifyResult))
|
||||
updatedPerson := O.GetOrElse(F.Constant(person))(modifyResult)
|
||||
assert.Equal(t, "Alice Smith", updatedPerson.Name)
|
||||
})
|
||||
|
||||
t.Run("optionalModify updates when GetOption returns Some", func(t *testing.T) {
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Verify optional matches
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsSome(initial))
|
||||
|
||||
// Modify should update the value
|
||||
updated := optionalModify(func(name string) string {
|
||||
return name + " Smith"
|
||||
}, optional, person)
|
||||
|
||||
assert.Equal(t, "Alice Smith", updated.Name)
|
||||
assert.Equal(t, 30, updated.Age)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOptionalNoOpBehaviorRef tests no-op behavior with pointer types
|
||||
func TestOptionalNoOpBehaviorRef(t *testing.T) {
|
||||
optional := MakeOptionalRef(
|
||||
func(p *Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p *Person, name string) *Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("ModifyOption returns None when GetOption returns None (empty name)", func(t *testing.T) {
|
||||
person := &Person{Name: "", Age: 30}
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Try to modify - should return None
|
||||
modifyResult := ModifyOption[*Person, string](func(name string) string {
|
||||
return "Bob"
|
||||
})(optional)(person)
|
||||
|
||||
assert.True(t, O.IsNone(modifyResult))
|
||||
})
|
||||
|
||||
t.Run("ModifyOption returns None when pointer is nil", func(t *testing.T) {
|
||||
var person *Person = nil
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Try to modify - should return None
|
||||
modifyResult := ModifyOption[*Person, string](func(name string) string {
|
||||
return "Bob"
|
||||
})(optional)(person)
|
||||
|
||||
assert.True(t, O.IsNone(modifyResult))
|
||||
})
|
||||
|
||||
t.Run("optionalModify is no-op when GetOption returns None", func(t *testing.T) {
|
||||
person := &Person{Name: "", Age: 30}
|
||||
originalName := person.Name
|
||||
originalAge := person.Age
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Try to modify
|
||||
updated := optionalModify(func(name string) string {
|
||||
return "Bob"
|
||||
}, optional, person)
|
||||
|
||||
// Verify no-op: structure should be unchanged
|
||||
assert.Equal(t, originalName, updated.Name)
|
||||
assert.Equal(t, originalAge, updated.Age)
|
||||
})
|
||||
|
||||
t.Run("optionalModify is no-op when pointer is nil", func(t *testing.T) {
|
||||
var person *Person = nil
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Try to modify
|
||||
updated := optionalModify(func(name string) string {
|
||||
return "Bob"
|
||||
}, optional, person)
|
||||
|
||||
// Verify no-op: should still be nil
|
||||
assert.Nil(t, updated)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromPredicateNoOpBehavior tests that FromPredicate properly implements no-op behavior
|
||||
func TestFromPredicateNoOpBehavior(t *testing.T) {
|
||||
t.Run("FromPredicate Set is no-op when predicate doesn't match", func(t *testing.T) {
|
||||
optional := FromPredicate[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
person := Person{Name: "", Age: 30}
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Set should be no-op when predicate doesn't match
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
// Verify no-op: structure should be unchanged
|
||||
assert.Equal(t, person, updated)
|
||||
assert.Equal(t, "", updated.Name)
|
||||
assert.Equal(t, 30, updated.Age)
|
||||
})
|
||||
|
||||
t.Run("FromPredicate Set updates when predicate matches on current value", func(t *testing.T) {
|
||||
optional := FromPredicate[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Verify optional matches
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsSome(initial))
|
||||
|
||||
// Set should update when predicate matches on the CURRENT value
|
||||
// Note: FromPredicate's setter checks the predicate on the current value,
|
||||
// not the new value. This is the correct behavior for the no-op law.
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
assert.Equal(t, "Bob", updated.Name)
|
||||
assert.Equal(t, 30, updated.Age)
|
||||
})
|
||||
|
||||
t.Run("FromPredicate demonstrates the no-op law correctly", func(t *testing.T) {
|
||||
// This test shows that FromPredicate implements the no-op law:
|
||||
// The setter checks if the CURRENT value matches the predicate
|
||||
optional := FromPredicate[Person](func(age int) bool {
|
||||
return age >= 18 // Adult predicate
|
||||
})(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, age int) Person { p.Age = age; return p },
|
||||
)
|
||||
|
||||
// Case 1: Current value matches predicate (adult) - Set should work
|
||||
adult := Person{Name: "Alice", Age: 30}
|
||||
updatedAdult := optional.Set(25)(adult)
|
||||
assert.Equal(t, 25, updatedAdult.Age)
|
||||
|
||||
// Case 2: Current value doesn't match predicate (child) - Set is no-op
|
||||
child := Person{Name: "Bob", Age: 10}
|
||||
updatedChild := optional.Set(25)(child)
|
||||
assert.Equal(t, 10, updatedChild.Age) // Unchanged - no-op!
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromPredicateRefNoOpBehavior tests that FromPredicateRef properly implements no-op behavior
|
||||
func TestFromPredicateRefNoOpBehavior(t *testing.T) {
|
||||
t.Run("FromPredicateRef Set is no-op when predicate doesn't match", func(t *testing.T) {
|
||||
optional := FromPredicateRef[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p *Person) string { return p.Name },
|
||||
func(p *Person, name string) *Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
person := &Person{Name: "", Age: 30}
|
||||
originalName := person.Name
|
||||
originalAge := person.Age
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Set should be no-op when predicate doesn't match
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
// Verify no-op: structure should be unchanged
|
||||
assert.Equal(t, originalName, updated.Name)
|
||||
assert.Equal(t, originalAge, updated.Age)
|
||||
// Original should also be unchanged (immutability)
|
||||
assert.Equal(t, originalName, person.Name)
|
||||
})
|
||||
|
||||
t.Run("FromPredicateRef Set is no-op when pointer is nil", func(t *testing.T) {
|
||||
optional := FromPredicateRef[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p *Person) string { return p.Name },
|
||||
func(p *Person, name string) *Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
var person *Person = nil
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// Set should be no-op (return nil)
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
assert.Nil(t, updated)
|
||||
})
|
||||
|
||||
t.Run("FromPredicateRef Set updates when predicate matches on current value", func(t *testing.T) {
|
||||
optional := FromPredicateRef[Person](func(name string) bool {
|
||||
return name != ""
|
||||
})(
|
||||
func(p *Person) string { return p.Name },
|
||||
func(p *Person, name string) *Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
person := &Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Verify optional matches
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsSome(initial))
|
||||
|
||||
// Set should update when predicate matches on the CURRENT value
|
||||
updated := optional.Set("Bob")(person)
|
||||
|
||||
assert.Equal(t, "Bob", updated.Name)
|
||||
assert.Equal(t, 30, updated.Age)
|
||||
// Original should be unchanged (immutability)
|
||||
assert.Equal(t, "Alice", person.Name)
|
||||
})
|
||||
|
||||
t.Run("FromPredicateRef demonstrates the no-op law correctly", func(t *testing.T) {
|
||||
// This test shows that FromPredicateRef implements the no-op law
|
||||
optional := FromPredicateRef[Person](func(age int) bool {
|
||||
return age >= 18 // Adult predicate
|
||||
})(
|
||||
func(p *Person) int { return p.Age },
|
||||
func(p *Person, age int) *Person { p.Age = age; return p },
|
||||
)
|
||||
|
||||
// Case 1: Current value matches predicate (adult) - Set should work
|
||||
adult := &Person{Name: "Alice", Age: 30}
|
||||
updatedAdult := optional.Set(25)(adult)
|
||||
assert.Equal(t, 25, updatedAdult.Age)
|
||||
assert.Equal(t, 30, adult.Age) // Original unchanged
|
||||
|
||||
// Case 2: Current value doesn't match predicate (child) - Set is no-op
|
||||
child := &Person{Name: "Bob", Age: 10}
|
||||
updatedChild := optional.Set(25)(child)
|
||||
assert.Equal(t, 10, updatedChild.Age) // Unchanged - no-op!
|
||||
assert.Equal(t, 10, child.Age) // Original also unchanged
|
||||
})
|
||||
}
|
||||
|
||||
// TestSetOptionNoOpBehavior tests SetOption behavior with None
|
||||
func TestSetOptionNoOpBehavior(t *testing.T) {
|
||||
optional := MakeOptional(
|
||||
func(p Person) O.Option[string] {
|
||||
if p.Name != "" {
|
||||
return O.Some(p.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
},
|
||||
func(p Person, name string) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("SetOption returns None when GetOption returns None", func(t *testing.T) {
|
||||
person := Person{Name: "", Age: 30}
|
||||
|
||||
// Verify optional doesn't match
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsNone(initial))
|
||||
|
||||
// SetOption should return None
|
||||
result := SetOption[Person, string]("Bob")(optional)(person)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("SetOption returns Some when GetOption returns Some", func(t *testing.T) {
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
|
||||
// Verify optional matches
|
||||
initial := optional.GetOption(person)
|
||||
assert.True(t, O.IsSome(initial))
|
||||
|
||||
// SetOption should return Some with updated value
|
||||
result := SetOption[Person, string]("Bob")(optional)(person)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
updatedPerson := O.GetOrElse(F.Constant(person))(result)
|
||||
assert.Equal(t, "Bob", updatedPerson.Name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1037,3 +1037,63 @@ func FromOption[T any]() Prism[Option[T], T] {
|
||||
"PrismFromOption",
|
||||
)
|
||||
}
|
||||
|
||||
// NonEmptyString creates a prism that matches non-empty strings.
|
||||
// It provides a safe way to work with non-empty string values, handling
|
||||
// empty strings gracefully through the Option type.
|
||||
//
|
||||
// This is a specialized version of FromNonZero[string]() that makes the intent
|
||||
// clearer when working specifically with strings that must not be empty.
|
||||
//
|
||||
// The prism's GetOption returns Some(s) if the string is not empty;
|
||||
// otherwise, it returns None.
|
||||
//
|
||||
// The prism's ReverseGet is the identity function, returning the string unchanged.
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, string] that matches non-empty strings
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a prism for non-empty strings
|
||||
// nonEmptyPrism := NonEmptyString()
|
||||
//
|
||||
// // Match non-empty string
|
||||
// result := nonEmptyPrism.GetOption("hello") // Some("hello")
|
||||
//
|
||||
// // Empty string returns None
|
||||
// result = nonEmptyPrism.GetOption("") // None[string]()
|
||||
//
|
||||
// // ReverseGet is identity
|
||||
// value := nonEmptyPrism.ReverseGet("world") // "world"
|
||||
//
|
||||
// // Use with Set to update non-empty strings
|
||||
// setter := Set[string, string]("updated")
|
||||
// result := setter(nonEmptyPrism)("original") // "updated"
|
||||
// result = setter(nonEmptyPrism)("") // "" (unchanged)
|
||||
//
|
||||
// // Compose with other prisms for validation pipelines
|
||||
// // Example: Parse a non-empty string as an integer
|
||||
// nonEmptyIntPrism := Compose[string, string, int](
|
||||
// NonEmptyString(),
|
||||
// ParseInt(),
|
||||
// )
|
||||
// value := nonEmptyIntPrism.GetOption("42") // Some(42)
|
||||
// value = nonEmptyIntPrism.GetOption("") // None[int]()
|
||||
// value = nonEmptyIntPrism.GetOption("abc") // None[int]()
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating required string fields (usernames, names, IDs)
|
||||
// - Filtering empty strings from data pipelines
|
||||
// - Ensuring configuration values are non-empty
|
||||
// - Composing with parsing prisms to validate input before parsing
|
||||
// - Working with user input that must not be blank
|
||||
//
|
||||
// Key insight: This prism is particularly useful for validation scenarios where
|
||||
// an empty string represents an invalid or missing value, allowing you to handle
|
||||
// such cases gracefully through the Option type rather than with error handling.
|
||||
//
|
||||
//go:inline
|
||||
func NonEmptyString() Prism[string, string] {
|
||||
return FromNonZero[string]()
|
||||
}
|
||||
|
||||
@@ -1145,3 +1145,254 @@ func TestFromOptionComposition(t *testing.T) {
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestNonEmptyString tests the NonEmptyString prism
|
||||
func TestNonEmptyString(t *testing.T) {
|
||||
t.Run("match non-empty string", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
result := prism.GetOption("hello")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "hello", O.GetOrElse(F.Constant("default"))(result))
|
||||
})
|
||||
|
||||
t.Run("empty string returns None", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
result := prism.GetOption("")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("whitespace string is non-empty", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
result := prism.GetOption(" ")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, " ", O.GetOrElse(F.Constant("default"))(result))
|
||||
})
|
||||
|
||||
t.Run("single character string", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
result := prism.GetOption("a")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "a", O.GetOrElse(F.Constant("default"))(result))
|
||||
})
|
||||
|
||||
t.Run("multiline string", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
multiline := "line1\nline2\nline3"
|
||||
result := prism.GetOption(multiline)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, multiline, O.GetOrElse(F.Constant("default"))(result))
|
||||
})
|
||||
|
||||
t.Run("unicode string", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
unicode := "Hello 世界 🌍"
|
||||
result := prism.GetOption(unicode)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, unicode, O.GetOrElse(F.Constant("default"))(result))
|
||||
})
|
||||
|
||||
t.Run("reverse get is identity", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
assert.Equal(t, "", prism.ReverseGet(""))
|
||||
assert.Equal(t, "hello", prism.ReverseGet("hello"))
|
||||
assert.Equal(t, "world", prism.ReverseGet("world"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestNonEmptyStringWithSet tests using Set with NonEmptyString prism
|
||||
func TestNonEmptyStringWithSet(t *testing.T) {
|
||||
t.Run("set on non-empty string", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
setter := Set[string]("updated")
|
||||
result := setter(prism)("original")
|
||||
|
||||
assert.Equal(t, "updated", result)
|
||||
})
|
||||
|
||||
t.Run("set on empty string returns original", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
setter := Set[string]("updated")
|
||||
result := setter(prism)("")
|
||||
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("set with empty value on non-empty string", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
setter := Set[string]("")
|
||||
result := setter(prism)("original")
|
||||
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestNonEmptyStringPrismLaws tests that NonEmptyString satisfies prism laws
|
||||
func TestNonEmptyStringPrismLaws(t *testing.T) {
|
||||
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
// For any non-empty string a, GetOption(ReverseGet(a)) should return Some(a)
|
||||
testCases := []string{"hello", "world", "a", "test string", "123"}
|
||||
for _, testCase := range testCases {
|
||||
reversed := prism.ReverseGet(testCase)
|
||||
result := prism.GetOption(reversed)
|
||||
|
||||
assert.True(t, O.IsSome(result), "Expected Some for: %s", testCase)
|
||||
assert.Equal(t, testCase, O.GetOrElse(F.Constant(""))(result))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("law 2: if GetOption(s) == Some(a), then ReverseGet(a) == s", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
// For any non-empty string s where GetOption(s) returns Some(a),
|
||||
// ReverseGet(a) should equal s
|
||||
testCases := []string{"hello", "world", "test", " ", "123"}
|
||||
for _, testCase := range testCases {
|
||||
optResult := prism.GetOption(testCase)
|
||||
if O.IsSome(optResult) {
|
||||
extracted := O.GetOrElse(F.Constant(""))(optResult)
|
||||
reversed := prism.ReverseGet(extracted)
|
||||
assert.Equal(t, testCase, reversed)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("law 3: GetOption is idempotent", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
testCases := []string{"hello", "", "world", " "}
|
||||
for _, testCase := range testCases {
|
||||
result1 := prism.GetOption(testCase)
|
||||
result2 := prism.GetOption(testCase)
|
||||
|
||||
assert.Equal(t, result1, result2, "GetOption should be idempotent for: %s", testCase)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestNonEmptyStringComposition tests composing NonEmptyString with other prisms
|
||||
func TestNonEmptyStringComposition(t *testing.T) {
|
||||
t.Run("compose with ParseInt", func(t *testing.T) {
|
||||
// Create a prism that only parses non-empty strings to int
|
||||
nonEmptyPrism := NonEmptyString()
|
||||
intPrism := ParseInt()
|
||||
|
||||
// Compose: string -> non-empty string -> int
|
||||
composed := Compose[string](intPrism)(nonEmptyPrism)
|
||||
|
||||
// Test with valid non-empty string
|
||||
result := composed.GetOption("42")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(result))
|
||||
|
||||
// Test with empty string
|
||||
result = composed.GetOption("")
|
||||
assert.True(t, O.IsNone(result))
|
||||
|
||||
// Test with invalid non-empty string
|
||||
result = composed.GetOption("abc")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("compose with ParseFloat64", func(t *testing.T) {
|
||||
// Create a prism that only parses non-empty strings to float64
|
||||
nonEmptyPrism := NonEmptyString()
|
||||
floatPrism := ParseFloat64()
|
||||
|
||||
composed := Compose[string](floatPrism)(nonEmptyPrism)
|
||||
|
||||
// Test with valid non-empty string
|
||||
result := composed.GetOption("3.14")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 3.14, O.GetOrElse(F.Constant(-1.0))(result))
|
||||
|
||||
// Test with empty string
|
||||
result = composed.GetOption("")
|
||||
assert.True(t, O.IsNone(result))
|
||||
|
||||
// Test with invalid non-empty string
|
||||
result = composed.GetOption("not a number")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("compose with FromOption", func(t *testing.T) {
|
||||
// Create a prism that extracts non-empty strings from Option[string]
|
||||
optionPrism := FromOption[string]()
|
||||
nonEmptyPrism := NonEmptyString()
|
||||
|
||||
composed := Compose[Option[string]](nonEmptyPrism)(optionPrism)
|
||||
|
||||
// Test with Some(non-empty)
|
||||
someNonEmpty := O.Some("hello")
|
||||
result := composed.GetOption(someNonEmpty)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "hello", O.GetOrElse(F.Constant(""))(result))
|
||||
|
||||
// Test with Some(empty)
|
||||
someEmpty := O.Some("")
|
||||
result = composed.GetOption(someEmpty)
|
||||
assert.True(t, O.IsNone(result))
|
||||
|
||||
// Test with None
|
||||
none := O.None[string]()
|
||||
result = composed.GetOption(none)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestNonEmptyStringValidation tests NonEmptyString for validation scenarios
|
||||
func TestNonEmptyStringValidation(t *testing.T) {
|
||||
t.Run("validate username", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
// Valid username
|
||||
validUsername := "john_doe"
|
||||
result := prism.GetOption(validUsername)
|
||||
assert.True(t, O.IsSome(result))
|
||||
|
||||
// Invalid empty username
|
||||
emptyUsername := ""
|
||||
result = prism.GetOption(emptyUsername)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("validate configuration value", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
// Valid config value
|
||||
configValue := "production"
|
||||
result := prism.GetOption(configValue)
|
||||
assert.True(t, O.IsSome(result))
|
||||
|
||||
// Invalid empty config
|
||||
emptyConfig := ""
|
||||
result = prism.GetOption(emptyConfig)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("filter non-empty strings from slice", func(t *testing.T) {
|
||||
prism := NonEmptyString()
|
||||
|
||||
inputs := []string{"hello", "", "world", "", "test"}
|
||||
var nonEmpty []string
|
||||
|
||||
for _, input := range inputs {
|
||||
if result := prism.GetOption(input); O.IsSome(result) {
|
||||
nonEmpty = append(nonEmpty, O.GetOrElse(F.Constant(""))(result))
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"hello", "world", "test"}, nonEmpty)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -124,4 +125,6 @@ type (
|
||||
// - A: The original focus type
|
||||
// - B: The new focus type
|
||||
Operator[S, A, B any] = func(Prism[S, A]) Prism[S, B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -479,6 +479,15 @@ func Second[A, B, C any](pbc Reader[B, C]) Reader[T.Tuple2[A, B], T.Tuple2[A, C]
|
||||
// Read applies a context to a Reader to obtain its value.
|
||||
// This is the "run" operation that executes a Reader with a specific environment.
|
||||
//
|
||||
// Note: Read is functionally identical to identity.Flap[A](e). Both take a value and
|
||||
// return a function that applies that value to a function. The difference is semantic:
|
||||
// - identity.Flap: Generic function application (applies value to any function)
|
||||
// - reader.Read: Reader-specific execution (applies environment to a Reader)
|
||||
//
|
||||
// Recommendation: Use reader.Read when working in a Reader context, as it makes the
|
||||
// intent clearer that you're executing a Reader computation with an environment.
|
||||
// Use identity.Flap for general-purpose function application outside the Reader context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Port int }
|
||||
|
||||
@@ -173,6 +173,85 @@ func TestLocal(t *testing.T) {
|
||||
assert.Equal(t, "localhost", result)
|
||||
}
|
||||
|
||||
func TestContramap(t *testing.T) {
|
||||
t.Run("transforms environment before passing to Reader", func(t *testing.T) {
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
type SimpleConfig struct{ Host string }
|
||||
|
||||
detailed := DetailedConfig{Host: "localhost", Port: 8080}
|
||||
getHost := func(c SimpleConfig) string { return c.Host }
|
||||
simplify := func(d DetailedConfig) SimpleConfig { return SimpleConfig{Host: d.Host} }
|
||||
r := Contramap[string](simplify)(getHost)
|
||||
result := r(detailed)
|
||||
assert.Equal(t, "localhost", result)
|
||||
})
|
||||
|
||||
t.Run("is functionally identical to Local", func(t *testing.T) {
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
type SimpleConfig struct{ Host string }
|
||||
|
||||
getHost := func(c SimpleConfig) string { return c.Host }
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host}
|
||||
}
|
||||
|
||||
// Using Contramap
|
||||
contramapResult := Contramap[string](simplify)(getHost)
|
||||
|
||||
// Using Local
|
||||
localResult := Local[string](simplify)(getHost)
|
||||
|
||||
detailed := DetailedConfig{Host: "localhost", Port: 8080}
|
||||
assert.Equal(t, contramapResult(detailed), localResult(detailed))
|
||||
assert.Equal(t, "localhost", contramapResult(detailed))
|
||||
})
|
||||
|
||||
t.Run("works with numeric transformations", func(t *testing.T) {
|
||||
type LargeEnv struct{ Value int }
|
||||
type SmallEnv struct{ Value int }
|
||||
|
||||
// Reader that doubles a value
|
||||
doubler := func(e SmallEnv) int { return e.Value * 2 }
|
||||
|
||||
// Transform that extracts and scales
|
||||
extract := func(l LargeEnv) SmallEnv {
|
||||
return SmallEnv{Value: l.Value / 10}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](extract)(doubler)
|
||||
result := adapted(LargeEnv{Value: 100})
|
||||
assert.Equal(t, 20, result) // (100/10) * 2 = 20
|
||||
})
|
||||
|
||||
t.Run("can be composed with Map for full profunctor behavior", func(t *testing.T) {
|
||||
type Env struct{ Config Config }
|
||||
env := Env{Config: Config{Port: 8080}}
|
||||
|
||||
// Extract config (contravariant)
|
||||
extractConfig := func(e Env) Config { return e.Config }
|
||||
|
||||
// Get port and convert to string (covariant)
|
||||
getPort := func(c Config) int { return c.Port }
|
||||
toString := strconv.Itoa
|
||||
|
||||
// Contramap on input, Map on output
|
||||
r := F.Pipe2(
|
||||
getPort,
|
||||
Contramap[int](extractConfig),
|
||||
Map[Env](toString),
|
||||
)
|
||||
|
||||
result := r(env)
|
||||
assert.Equal(t, "8080", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithLocal(t *testing.T) {
|
||||
t.Run("transforms environment before passing to Reader", func(t *testing.T) {
|
||||
type DetailedConfig struct {
|
||||
|
||||
@@ -15,71 +15,42 @@
|
||||
|
||||
package reader
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
import "github.com/IBM/fp-go/v2/monoid"
|
||||
|
||||
// ApplySemigroup lifts a Semigroup[A] into a Semigroup[Reader[R, A]].
|
||||
// This allows you to combine two Readers that produce semigroup values by combining
|
||||
// their results using the semigroup's concat operation.
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [Reader] instances via their applicative.
|
||||
// This combines two Reader values by applying the underlying monoid's combine operation
|
||||
// to their results using applicative application.
|
||||
//
|
||||
// The _map and _ap parameters are the Map and Ap operations for the Reader type,
|
||||
// typically obtained from the reader package.
|
||||
// The applicative behavior means that both Reader computations are executed with the same
|
||||
// environment, and their results are combined using the underlying monoid. This is useful
|
||||
// for accumulating values from multiple Reader computations that all depend on the same
|
||||
// environment.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for Reader[R, A].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// // Using the additive semigroup for integers
|
||||
// intSemigroup := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
// readerSemigroup := reader.ApplySemigroup(
|
||||
// reader.MonadMap[Config, int, func(int) int],
|
||||
// reader.MonadAp[int, Config, int],
|
||||
// intSemigroup,
|
||||
// )
|
||||
// type Config struct { Port int; Timeout int }
|
||||
// intMonoid := number.MonoidSum[int]()
|
||||
// readerMonoid := ApplicativeMonoid[Config](intMonoid)
|
||||
//
|
||||
// r1 := reader.Of[Config](5)
|
||||
// r2 := reader.Of[Config](3)
|
||||
// combined := readerSemigroup.Concat(r1, r2)
|
||||
// result := combined(Config{Multiplier: 1}) // 8
|
||||
func ApplySemigroup[R, A any](
|
||||
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
|
||||
_ap func(func(R, func(A) A), func(R) A) func(R) A,
|
||||
|
||||
s S.Semigroup[A],
|
||||
) S.Semigroup[func(R) A] {
|
||||
return S.ApplySemigroup(_map, _ap, s)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid lifts a Monoid[A] into a Monoid[Reader[R, A]].
|
||||
// This allows you to combine Readers that produce monoid values, with an empty/identity Reader.
|
||||
//
|
||||
// The _of parameter is the Of operation (pure/return) for the Reader type.
|
||||
// The _map and _ap parameters are the Map and Ap operations for the Reader type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Prefix string }
|
||||
// // Using the string concatenation monoid
|
||||
// stringMonoid := monoid.MakeMonoid("", func(a, b string) string { return a + b })
|
||||
// readerMonoid := reader.ApplicativeMonoid(
|
||||
// reader.Of[Config, string],
|
||||
// reader.MonadMap[Config, string, func(string) string],
|
||||
// reader.MonadAp[string, Config, string],
|
||||
// stringMonoid,
|
||||
// )
|
||||
//
|
||||
// r1 := reader.Asks(func(c Config) string { return c.Prefix })
|
||||
// r2 := reader.Of[Config]("hello")
|
||||
// combined := readerMonoid.Concat(r1, r2)
|
||||
// result := combined(Config{Prefix: ">> "}) // ">> hello"
|
||||
// empty := readerMonoid.Empty()(Config{Prefix: "any"}) // ""
|
||||
func ApplicativeMonoid[R, A any](
|
||||
_of func(A) func(R) A,
|
||||
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
|
||||
_ap func(func(R, func(A) A), func(R) A) func(R) A,
|
||||
|
||||
m M.Monoid[A],
|
||||
) M.Monoid[func(R) A] {
|
||||
return M.ApplicativeMonoid(_of, _map, _ap, m)
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// getTimeout := func(c Config) int { return c.Timeout }
|
||||
// combined := readerMonoid.Concat(getPort, getTimeout)
|
||||
// // Result: func(c Config) int { return c.Port + c.Timeout }
|
||||
//
|
||||
// config := Config{Port: 8080, Timeout: 30}
|
||||
// result := combined(config) // 8110
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) monoid.Monoid[Reader[R, A]] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[A, R, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
478
v2/reader/semigroup_test.go
Normal file
478
v2/reader/semigroup_test.go
Normal file
@@ -0,0 +1,478 @@
|
||||
// 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 reader
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// SemigroupConfig represents a test configuration environment for semigroup tests
|
||||
type SemigroupConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout int
|
||||
MaxRetries int
|
||||
Debug bool
|
||||
}
|
||||
|
||||
var (
|
||||
defaultSemigroupConfig = SemigroupConfig{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Timeout: 30,
|
||||
MaxRetries: 3,
|
||||
Debug: false,
|
||||
}
|
||||
|
||||
semigroupIntAddMonoid = N.MonoidSum[int]()
|
||||
semigroupIntMulMonoid = N.MonoidProduct[int]()
|
||||
semigroupStrMonoid = S.Monoid
|
||||
)
|
||||
|
||||
// TestApplicativeMonoidSemigroup tests the ApplicativeMonoid function
|
||||
func TestApplicativeMonoidSemigroup(t *testing.T) {
|
||||
readerMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := readerMonoid.Empty()
|
||||
result := empty(defaultSemigroupConfig)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("concat two readers", func(t *testing.T) {
|
||||
r1 := func(c SemigroupConfig) int { return c.Port }
|
||||
r2 := func(c SemigroupConfig) int { return c.Timeout }
|
||||
combined := readerMonoid.Concat(r1, r2)
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// 8080 + 30 = 8110
|
||||
assert.Equal(t, 8110, result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - left identity", func(t *testing.T) {
|
||||
r := func(c SemigroupConfig) int { return c.Port }
|
||||
combined := readerMonoid.Concat(readerMonoid.Empty(), r)
|
||||
result := combined(defaultSemigroupConfig)
|
||||
assert.Equal(t, 8080, result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - right identity", func(t *testing.T) {
|
||||
r := func(c SemigroupConfig) int { return c.Port }
|
||||
combined := readerMonoid.Concat(r, readerMonoid.Empty())
|
||||
result := combined(defaultSemigroupConfig)
|
||||
assert.Equal(t, 8080, result)
|
||||
})
|
||||
|
||||
t.Run("concat multiple readers", func(t *testing.T) {
|
||||
r1 := func(c SemigroupConfig) int { return c.Port }
|
||||
r2 := func(c SemigroupConfig) int { return c.Timeout }
|
||||
r3 := func(c SemigroupConfig) int { return c.MaxRetries }
|
||||
r4 := Of[SemigroupConfig](100)
|
||||
|
||||
// Chain concat calls: ((r1 + r2) + r3) + r4
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(
|
||||
readerMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
),
|
||||
r4,
|
||||
)
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// 8080 + 30 + 3 + 100 = 8213
|
||||
assert.Equal(t, 8213, result)
|
||||
})
|
||||
|
||||
t.Run("concat constant readers", func(t *testing.T) {
|
||||
r1 := Of[SemigroupConfig](10)
|
||||
r2 := Of[SemigroupConfig](20)
|
||||
r3 := Of[SemigroupConfig](30)
|
||||
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
)
|
||||
result := combined(defaultSemigroupConfig)
|
||||
assert.Equal(t, 60, result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
strReaderMonoid := ApplicativeMonoid[SemigroupConfig](semigroupStrMonoid)
|
||||
|
||||
r1 := func(c SemigroupConfig) string { return c.Host }
|
||||
r2 := Of[SemigroupConfig](":")
|
||||
r3 := Asks(func(c SemigroupConfig) string {
|
||||
return strconv.Itoa(c.Port)
|
||||
})
|
||||
|
||||
combined := strReaderMonoid.Concat(
|
||||
strReaderMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
)
|
||||
result := combined(defaultSemigroupConfig)
|
||||
assert.Equal(t, "localhost:8080", result)
|
||||
})
|
||||
|
||||
t.Run("multiplication monoid", func(t *testing.T) {
|
||||
mulReaderMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntMulMonoid)
|
||||
|
||||
r1 := func(c SemigroupConfig) int { return c.MaxRetries }
|
||||
r2 := Of[SemigroupConfig](10)
|
||||
r3 := Of[SemigroupConfig](2)
|
||||
|
||||
combined := mulReaderMonoid.Concat(
|
||||
mulReaderMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
)
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// 3 * 10 * 2 = 60
|
||||
assert.Equal(t, 60, result)
|
||||
})
|
||||
|
||||
t.Run("environment dependent computation", func(t *testing.T) {
|
||||
// Create readers that use different parts of the environment
|
||||
getPort := Asks(func(c SemigroupConfig) int { return c.Port })
|
||||
getTimeout := Asks(func(c SemigroupConfig) int { return c.Timeout })
|
||||
getRetries := Asks(func(c SemigroupConfig) int { return c.MaxRetries })
|
||||
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(getPort, getTimeout),
|
||||
getRetries,
|
||||
)
|
||||
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// 8080 + 30 + 3 = 8113
|
||||
assert.Equal(t, 8113, result)
|
||||
})
|
||||
|
||||
t.Run("mixed constant and environment readers", func(t *testing.T) {
|
||||
r1 := Of[SemigroupConfig](1000)
|
||||
r2 := func(c SemigroupConfig) int { return c.Port }
|
||||
r3 := Of[SemigroupConfig](5)
|
||||
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
)
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// 1000 + 8080 + 5 = 9085
|
||||
assert.Equal(t, 9085, result)
|
||||
})
|
||||
|
||||
t.Run("different environment values", func(t *testing.T) {
|
||||
r1 := func(c SemigroupConfig) int { return c.Port }
|
||||
r2 := func(c SemigroupConfig) int { return c.Timeout }
|
||||
|
||||
combined := readerMonoid.Concat(r1, r2)
|
||||
|
||||
// Test with different configs
|
||||
config1 := SemigroupConfig{Port: 3000, Timeout: 60}
|
||||
config2 := SemigroupConfig{Port: 9000, Timeout: 120}
|
||||
|
||||
result1 := combined(config1)
|
||||
result2 := combined(config2)
|
||||
|
||||
assert.Equal(t, 3060, result1)
|
||||
assert.Equal(t, 9120, result2)
|
||||
})
|
||||
|
||||
t.Run("conditional reader based on environment", func(t *testing.T) {
|
||||
r1 := func(c SemigroupConfig) int {
|
||||
if c.Debug {
|
||||
return c.Port * 2
|
||||
}
|
||||
return c.Port
|
||||
}
|
||||
r2 := func(c SemigroupConfig) int { return c.Timeout }
|
||||
|
||||
combined := readerMonoid.Concat(r1, r2)
|
||||
|
||||
// Test with debug off
|
||||
result1 := combined(defaultSemigroupConfig)
|
||||
assert.Equal(t, 8110, result1) // 8080 + 30
|
||||
|
||||
// Test with debug on
|
||||
debugConfig := defaultSemigroupConfig
|
||||
debugConfig.Debug = true
|
||||
result2 := combined(debugConfig)
|
||||
assert.Equal(t, 16190, result2) // (8080 * 2) + 30
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidLawsSemigroup verifies that the monoid laws hold for ApplicativeMonoid
|
||||
func TestMonoidLawsSemigroup(t *testing.T) {
|
||||
readerMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntAddMonoid)
|
||||
|
||||
t.Run("left identity law", func(t *testing.T) {
|
||||
// empty <> x == x
|
||||
x := func(c SemigroupConfig) int { return c.Port }
|
||||
result1 := readerMonoid.Concat(readerMonoid.Empty(), x)(defaultSemigroupConfig)
|
||||
result2 := x(defaultSemigroupConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("right identity law", func(t *testing.T) {
|
||||
// x <> empty == x
|
||||
x := func(c SemigroupConfig) int { return c.Port }
|
||||
result1 := readerMonoid.Concat(x, readerMonoid.Empty())(defaultSemigroupConfig)
|
||||
result2 := x(defaultSemigroupConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("associativity law", func(t *testing.T) {
|
||||
// (x <> y) <> z == x <> (y <> z)
|
||||
x := func(c SemigroupConfig) int { return c.Port }
|
||||
y := func(c SemigroupConfig) int { return c.Timeout }
|
||||
z := func(c SemigroupConfig) int { return c.MaxRetries }
|
||||
|
||||
left := readerMonoid.Concat(readerMonoid.Concat(x, y), z)(defaultSemigroupConfig)
|
||||
right := readerMonoid.Concat(x, readerMonoid.Concat(y, z))(defaultSemigroupConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("associativity with constants", func(t *testing.T) {
|
||||
x := Of[SemigroupConfig](10)
|
||||
y := Of[SemigroupConfig](20)
|
||||
z := Of[SemigroupConfig](30)
|
||||
|
||||
left := readerMonoid.Concat(readerMonoid.Concat(x, y), z)(defaultSemigroupConfig)
|
||||
right := readerMonoid.Concat(x, readerMonoid.Concat(y, z))(defaultSemigroupConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("associativity with mixed readers", func(t *testing.T) {
|
||||
x := func(c SemigroupConfig) int { return c.Port }
|
||||
y := Of[SemigroupConfig](100)
|
||||
z := func(c SemigroupConfig) int { return c.Timeout }
|
||||
|
||||
left := readerMonoid.Concat(readerMonoid.Concat(x, y), z)(defaultSemigroupConfig)
|
||||
right := readerMonoid.Concat(x, readerMonoid.Concat(y, z))(defaultSemigroupConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidWithDifferentTypesSemigroup tests monoid with various types
|
||||
func TestMonoidWithDifferentTypesSemigroup(t *testing.T) {
|
||||
t.Run("string monoid", func(t *testing.T) {
|
||||
strReaderMonoid := ApplicativeMonoid[SemigroupConfig](semigroupStrMonoid)
|
||||
|
||||
r1 := func(c SemigroupConfig) string { return "Host: " }
|
||||
r2 := func(c SemigroupConfig) string { return c.Host }
|
||||
r3 := Of[SemigroupConfig](" | Port: ")
|
||||
r4 := Asks(func(c SemigroupConfig) string { return strconv.Itoa(c.Port) })
|
||||
|
||||
combined := strReaderMonoid.Concat(
|
||||
strReaderMonoid.Concat(
|
||||
strReaderMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
),
|
||||
r4,
|
||||
)
|
||||
|
||||
result := combined(defaultSemigroupConfig)
|
||||
assert.Equal(t, "Host: localhost | Port: 8080", result)
|
||||
})
|
||||
|
||||
t.Run("product monoid", func(t *testing.T) {
|
||||
mulReaderMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntMulMonoid)
|
||||
|
||||
r1 := func(c SemigroupConfig) int { return 2 }
|
||||
r2 := func(c SemigroupConfig) int { return c.MaxRetries }
|
||||
r3 := Of[SemigroupConfig](5)
|
||||
|
||||
combined := mulReaderMonoid.Concat(
|
||||
mulReaderMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
)
|
||||
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// 2 * 3 * 5 = 30
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComplexScenariosSemigroup tests more complex real-world scenarios
|
||||
func TestComplexScenariosSemigroup(t *testing.T) {
|
||||
t.Run("accumulate configuration values", func(t *testing.T) {
|
||||
readerMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntAddMonoid)
|
||||
|
||||
// Accumulate multiple configuration values
|
||||
getPort := Asks(func(c SemigroupConfig) int { return c.Port })
|
||||
getTimeout := Asks(func(c SemigroupConfig) int { return c.Timeout })
|
||||
getRetries := Asks(func(c SemigroupConfig) int { return c.MaxRetries })
|
||||
getConstant := Of[SemigroupConfig](1000)
|
||||
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(
|
||||
readerMonoid.Concat(getPort, getTimeout),
|
||||
getRetries,
|
||||
),
|
||||
getConstant,
|
||||
)
|
||||
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// 8080 + 30 + 3 + 1000 = 9113
|
||||
assert.Equal(t, 9113, result)
|
||||
})
|
||||
|
||||
t.Run("build connection string", func(t *testing.T) {
|
||||
strReaderMonoid := ApplicativeMonoid[SemigroupConfig](semigroupStrMonoid)
|
||||
|
||||
protocol := Of[SemigroupConfig]("http://")
|
||||
host := func(c SemigroupConfig) string { return c.Host }
|
||||
colon := Of[SemigroupConfig](":")
|
||||
port := Asks(func(c SemigroupConfig) string { return strconv.Itoa(c.Port) })
|
||||
|
||||
buildURL := strReaderMonoid.Concat(
|
||||
strReaderMonoid.Concat(
|
||||
strReaderMonoid.Concat(protocol, host),
|
||||
colon,
|
||||
),
|
||||
port,
|
||||
)
|
||||
|
||||
result := buildURL(defaultSemigroupConfig)
|
||||
assert.Equal(t, "http://localhost:8080", result)
|
||||
})
|
||||
|
||||
t.Run("calculate total score", func(t *testing.T) {
|
||||
type ScoreConfig struct {
|
||||
BaseScore int
|
||||
BonusPoints int
|
||||
Multiplier int
|
||||
PenaltyDeduction int
|
||||
}
|
||||
|
||||
scoreConfig := ScoreConfig{
|
||||
BaseScore: 100,
|
||||
BonusPoints: 50,
|
||||
Multiplier: 2,
|
||||
PenaltyDeduction: 10,
|
||||
}
|
||||
|
||||
readerMonoid := ApplicativeMonoid[ScoreConfig](semigroupIntAddMonoid)
|
||||
|
||||
getBase := func(c ScoreConfig) int { return c.BaseScore }
|
||||
getBonus := func(c ScoreConfig) int { return c.BonusPoints }
|
||||
getPenalty := func(c ScoreConfig) int { return -c.PenaltyDeduction }
|
||||
|
||||
totalScore := readerMonoid.Concat(
|
||||
readerMonoid.Concat(getBase, getBonus),
|
||||
getPenalty,
|
||||
)
|
||||
|
||||
result := totalScore(scoreConfig)
|
||||
// 100 + 50 - 10 = 140
|
||||
assert.Equal(t, 140, result)
|
||||
})
|
||||
|
||||
t.Run("compose multiple readers with empty", func(t *testing.T) {
|
||||
readerMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntAddMonoid)
|
||||
|
||||
r1 := func(c SemigroupConfig) int { return c.Port }
|
||||
r2 := readerMonoid.Empty()
|
||||
r3 := func(c SemigroupConfig) int { return c.Timeout }
|
||||
r4 := readerMonoid.Empty()
|
||||
r5 := Of[SemigroupConfig](100)
|
||||
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(
|
||||
readerMonoid.Concat(
|
||||
readerMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
),
|
||||
r4,
|
||||
),
|
||||
r5,
|
||||
)
|
||||
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// 8080 + 0 + 30 + 0 + 100 = 8210
|
||||
assert.Equal(t, 8210, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEdgeCasesSemigroup tests edge cases and boundary conditions
|
||||
func TestEdgeCasesSemigroup(t *testing.T) {
|
||||
t.Run("empty config struct", func(t *testing.T) {
|
||||
type EmptyConfig struct{}
|
||||
emptyConfig := EmptyConfig{}
|
||||
|
||||
readerMonoid := ApplicativeMonoid[EmptyConfig](semigroupIntAddMonoid)
|
||||
|
||||
r1 := Of[EmptyConfig](10)
|
||||
r2 := Of[EmptyConfig](20)
|
||||
|
||||
combined := readerMonoid.Concat(r1, r2)
|
||||
result := combined(emptyConfig)
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
|
||||
t.Run("zero values", func(t *testing.T) {
|
||||
readerMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntAddMonoid)
|
||||
|
||||
r1 := Of[SemigroupConfig](0)
|
||||
r2 := Of[SemigroupConfig](0)
|
||||
r3 := Of[SemigroupConfig](0)
|
||||
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
)
|
||||
|
||||
result := combined(defaultSemigroupConfig)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("negative values", func(t *testing.T) {
|
||||
readerMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntAddMonoid)
|
||||
|
||||
r1 := Of[SemigroupConfig](-100)
|
||||
r2 := Of[SemigroupConfig](50)
|
||||
r3 := Of[SemigroupConfig](-30)
|
||||
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
)
|
||||
|
||||
result := combined(defaultSemigroupConfig)
|
||||
// -100 + 50 - 30 = -80
|
||||
assert.Equal(t, -80, result)
|
||||
})
|
||||
|
||||
t.Run("large values", func(t *testing.T) {
|
||||
readerMonoid := ApplicativeMonoid[SemigroupConfig](semigroupIntAddMonoid)
|
||||
|
||||
r1 := Of[SemigroupConfig](1000000)
|
||||
r2 := Of[SemigroupConfig](2000000)
|
||||
r3 := Of[SemigroupConfig](3000000)
|
||||
|
||||
combined := readerMonoid.Concat(
|
||||
readerMonoid.Concat(r1, r2),
|
||||
r3,
|
||||
)
|
||||
|
||||
result := combined(defaultSemigroupConfig)
|
||||
assert.Equal(t, 6000000, result)
|
||||
})
|
||||
}
|
||||
135
v2/readereither/monoid.go
Normal file
135
v2/readereither/monoid.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// 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 readereither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [ReaderEither] instances via their applicative.
|
||||
// This combines two ReaderEither values by applying the underlying monoid's combine operation
|
||||
// to their success values using applicative application.
|
||||
//
|
||||
// The applicative behavior means that if either computation fails (returns Left), the entire
|
||||
// combination fails. Both computations must succeed (return Right) for the result to succeed.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderEither[R, E, A].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intMonoid := number.MonoidSum[int]()
|
||||
// reMonoid := ApplicativeMonoid[Config, string](intMonoid)
|
||||
//
|
||||
// re1 := Right[Config, string](5)
|
||||
// re2 := Right[Config, string](3)
|
||||
// combined := reMonoid.Concat(re1, re2)
|
||||
// // Result: Right(8)
|
||||
//
|
||||
// re3 := Left[Config, int]("error")
|
||||
// failed := reMonoid.Concat(re1, re3)
|
||||
// // Result: Left("error")
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[R, E, A any](m monoid.Monoid[A]) monoid.Monoid[ReaderEither[R, E, A]] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, E, A],
|
||||
MonadMap[R, E, A, func(A) A],
|
||||
MonadAp[A, R, E, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid is the alternative [Monoid] for [ReaderEither].
|
||||
// This combines ReaderEither values using the alternative semantics,
|
||||
// where the second value is only evaluated if the first fails.
|
||||
//
|
||||
// The alternative behavior provides fallback semantics: if the first computation
|
||||
// succeeds (returns Right), its value is used. If it fails (returns Left), the
|
||||
// second computation is tried. If both succeed, their values are combined using
|
||||
// the underlying monoid.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderEither[R, E, A] with alternative semantics.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intMonoid := number.MonoidSum[int]()
|
||||
// reMonoid := AlternativeMonoid[Config, string](intMonoid)
|
||||
//
|
||||
// re1 := Left[Config, int]("error1")
|
||||
// re2 := Right[Config, string](42)
|
||||
// combined := reMonoid.Concat(re1, re2)
|
||||
// // Result: Right(42) - falls back to second
|
||||
//
|
||||
// re3 := Right[Config, string](5)
|
||||
// re4 := Right[Config, string](3)
|
||||
// both := reMonoid.Concat(re3, re4)
|
||||
// // Result: Right(8) - combines both successes
|
||||
//
|
||||
//go:inline
|
||||
func AlternativeMonoid[R, E, A any](m monoid.Monoid[A]) monoid.Monoid[ReaderEither[R, E, A]] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[R, E, A],
|
||||
MonadMap[R, E, A, func(A) A],
|
||||
MonadAp[A, R, E, A],
|
||||
MonadAlt[R, E, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid is the alternative [Monoid] for a [ReaderEither].
|
||||
// This creates a monoid where the empty value is provided lazily,
|
||||
// and combination uses the Alt operation (try first, fallback to second on failure).
|
||||
//
|
||||
// Unlike AlternativeMonoid, this does not combine successful values using an underlying
|
||||
// monoid. Instead, it simply returns the first successful value, or falls back to the
|
||||
// second if the first fails.
|
||||
//
|
||||
// Parameters:
|
||||
// - zero: Lazy computation that provides the empty/identity value
|
||||
//
|
||||
// Returns a Monoid for ReaderEither[R, E, A] with Alt-based combination.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// zero := lazy.MakeLazy(func() ReaderEither[Config, string, int] {
|
||||
// return Left[Config, int]("no value")
|
||||
// })
|
||||
// reMonoid := AltMonoid(zero)
|
||||
//
|
||||
// re1 := Left[Config, int]("error1")
|
||||
// re2 := Right[Config, string](42)
|
||||
// combined := reMonoid.Concat(re1, re2)
|
||||
// // Result: Right(42) - uses first success
|
||||
//
|
||||
// re3 := Right[Config, string](100)
|
||||
// re4 := Right[Config, string](200)
|
||||
// first := reMonoid.Concat(re3, re4)
|
||||
// // Result: Right(100) - uses first success, doesn't combine
|
||||
//
|
||||
//go:inline
|
||||
func AltMonoid[R, E, A any](zero lazy.Lazy[ReaderEither[R, E, A]]) monoid.Monoid[ReaderEither[R, E, A]] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[R, E, A],
|
||||
)
|
||||
}
|
||||
747
v2/readereither/monoid_test.go
Normal file
747
v2/readereither/monoid_test.go
Normal file
@@ -0,0 +1,747 @@
|
||||
// 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 readereither
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Config represents a test configuration environment
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout int
|
||||
Debug bool
|
||||
}
|
||||
|
||||
var (
|
||||
defaultConfig = Config{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Timeout: 30,
|
||||
Debug: false,
|
||||
}
|
||||
|
||||
intAddMonoid = N.MonoidSum[int]()
|
||||
strMonoid = S.Monoid
|
||||
)
|
||||
|
||||
// TestApplicativeMonoid tests the ApplicativeMonoid function
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
reMonoid := ApplicativeMonoid[Config, string](intAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := reMonoid.Empty()
|
||||
result := empty(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](0), result)
|
||||
})
|
||||
|
||||
t.Run("concat two Right values", func(t *testing.T) {
|
||||
re1 := Right[Config, string](5)
|
||||
re2 := Right[Config, string](3)
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](8), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - left identity", func(t *testing.T) {
|
||||
re := Right[Config, string](42)
|
||||
combined := reMonoid.Concat(reMonoid.Empty(), re)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - right identity", func(t *testing.T) {
|
||||
re := Right[Config, string](42)
|
||||
combined := reMonoid.Concat(re, reMonoid.Empty())
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("concat with left error", func(t *testing.T) {
|
||||
reSuccess := Right[Config, string](5)
|
||||
reFailure := Left[Config, int]("error occurred")
|
||||
|
||||
combined := reMonoid.Concat(reFailure, reSuccess)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Left[int]("error occurred"), result)
|
||||
})
|
||||
|
||||
t.Run("concat with right error", func(t *testing.T) {
|
||||
reSuccess := Right[Config, string](5)
|
||||
reFailure := Left[Config, int]("error occurred")
|
||||
|
||||
combined := reMonoid.Concat(reSuccess, reFailure)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Left[int]("error occurred"), result)
|
||||
})
|
||||
|
||||
t.Run("concat both errors", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Left[Config, int]("error2")
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// First error is returned
|
||||
assert.Equal(t, E.Left[int]("error1"), result)
|
||||
})
|
||||
|
||||
t.Run("concat multiple values", func(t *testing.T) {
|
||||
re1 := Right[Config, string](1)
|
||||
re2 := Right[Config, string](2)
|
||||
re3 := Right[Config, string](3)
|
||||
re4 := Right[Config, string](4)
|
||||
|
||||
// Chain concat calls: ((1 + 2) + 3) + 4
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(
|
||||
reMonoid.Concat(re1, re2),
|
||||
re3,
|
||||
),
|
||||
re4,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](10), result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
strREMonoid := ApplicativeMonoid[Config, string](strMonoid)
|
||||
|
||||
re1 := Right[Config, string]("Hello")
|
||||
re2 := Right[Config, string](" ")
|
||||
re3 := Right[Config, string]("World")
|
||||
|
||||
combined := strREMonoid.Concat(
|
||||
strREMonoid.Concat(re1, re2),
|
||||
re3,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string]("Hello World"), result)
|
||||
})
|
||||
|
||||
t.Run("environment dependent computation", func(t *testing.T) {
|
||||
// Create computations that use the environment
|
||||
re1 := Asks[string](func(cfg Config) int {
|
||||
return cfg.Port
|
||||
})
|
||||
re2 := Right[Config, string](100)
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// defaultConfig.Port is 8080, so 8080 + 100 = 8180
|
||||
assert.Equal(t, E.Right[string](8180), result)
|
||||
})
|
||||
|
||||
t.Run("environment dependent with error", func(t *testing.T) {
|
||||
re1 := MonadChain(
|
||||
Ask[Config, string](),
|
||||
func(cfg Config) ReaderEither[Config, string, int] {
|
||||
if cfg.Debug {
|
||||
return Right[Config, string](cfg.Timeout)
|
||||
}
|
||||
return Left[Config, int]("debug mode disabled")
|
||||
},
|
||||
)
|
||||
re2 := Right[Config, string](50)
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// defaultConfig.Debug is false, so should fail
|
||||
assert.Equal(t, E.Left[int]("debug mode disabled"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlternativeMonoid tests the AlternativeMonoid function
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
reMonoid := AlternativeMonoid[Config, string](intAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := reMonoid.Empty()
|
||||
result := empty(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](0), result)
|
||||
})
|
||||
|
||||
t.Run("concat two Right values", func(t *testing.T) {
|
||||
re1 := Right[Config, string](5)
|
||||
re2 := Right[Config, string](3)
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// Alternative combines successful values
|
||||
assert.Equal(t, E.Right[string](8), result)
|
||||
})
|
||||
|
||||
t.Run("concat Left then Right - fallback behavior", func(t *testing.T) {
|
||||
reFailure := Left[Config, int]("error")
|
||||
reSuccess := Right[Config, string](42)
|
||||
|
||||
combined := reMonoid.Concat(reFailure, reSuccess)
|
||||
result := combined(defaultConfig)
|
||||
// Should fall back to second when first fails
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("concat Right then Left - uses first", func(t *testing.T) {
|
||||
reSuccess := Right[Config, string](42)
|
||||
reFailure := Left[Config, int]("error")
|
||||
|
||||
combined := reMonoid.Concat(reSuccess, reFailure)
|
||||
result := combined(defaultConfig)
|
||||
// Should use first successful value
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("concat both errors", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Left[Config, int]("error2")
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// Second error is returned when both fail
|
||||
assert.Equal(t, E.Left[int]("error2"), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - left identity", func(t *testing.T) {
|
||||
re := Right[Config, string](42)
|
||||
combined := reMonoid.Concat(reMonoid.Empty(), re)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - right identity", func(t *testing.T) {
|
||||
re := Right[Config, string](42)
|
||||
combined := reMonoid.Concat(re, reMonoid.Empty())
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("multiple values with some failures", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Right[Config, string](5)
|
||||
re3 := Left[Config, int]("error2")
|
||||
re4 := Right[Config, string](10)
|
||||
|
||||
// Alternative should skip failures and accumulate successes
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(
|
||||
reMonoid.Concat(re1, re2),
|
||||
re3,
|
||||
),
|
||||
re4,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
// Should accumulate successful values: 5 + 10 = 15
|
||||
assert.Equal(t, E.Right[string](15), result)
|
||||
})
|
||||
|
||||
t.Run("fallback chain", func(t *testing.T) {
|
||||
// Simulate trying multiple sources until one succeeds
|
||||
primary := Left[Config, string]("primary failed")
|
||||
secondary := Left[Config, string]("secondary failed")
|
||||
tertiary := Right[Config, string]("tertiary success")
|
||||
|
||||
strREMonoid := AlternativeMonoid[Config, string](strMonoid)
|
||||
|
||||
// Chain concat: try primary, then secondary, then tertiary
|
||||
combined := strREMonoid.Concat(
|
||||
strREMonoid.Concat(primary, secondary),
|
||||
tertiary,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string]("tertiary success"), result)
|
||||
})
|
||||
|
||||
t.Run("environment dependent with fallback", func(t *testing.T) {
|
||||
// First computation fails
|
||||
re1 := Left[Config, int]("error")
|
||||
// Second computation uses environment
|
||||
re2 := Asks[string](func(cfg Config) int {
|
||||
return cfg.Timeout
|
||||
})
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// Should fall back to second computation
|
||||
assert.Equal(t, E.Right[string](30), result)
|
||||
})
|
||||
|
||||
t.Run("all failures in chain", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Left[Config, int]("error2")
|
||||
re3 := Left[Config, int]("error3")
|
||||
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(re1, re2),
|
||||
re3,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
// Last error is returned
|
||||
assert.Equal(t, E.Left[int]("error3"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltMonoid tests the AltMonoid function
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
zero := func() ReaderEither[Config, string, int] {
|
||||
return Left[Config, int]("no value")
|
||||
}
|
||||
reMonoid := AltMonoid(zero)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := reMonoid.Empty()
|
||||
result := empty(defaultConfig)
|
||||
assert.Equal(t, E.Left[int]("no value"), result)
|
||||
})
|
||||
|
||||
t.Run("concat Left then Right - uses second", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Right[Config, string](42)
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// Should use first success
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("concat Right then Right - uses first", func(t *testing.T) {
|
||||
re1 := Right[Config, string](100)
|
||||
re2 := Right[Config, string](200)
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// Uses first success, doesn't combine
|
||||
assert.Equal(t, E.Right[string](100), result)
|
||||
})
|
||||
|
||||
t.Run("concat Right then Left - uses first", func(t *testing.T) {
|
||||
re1 := Right[Config, string](42)
|
||||
re2 := Left[Config, int]("error")
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("concat both errors", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Left[Config, int]("error2")
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// Second error is returned
|
||||
assert.Equal(t, E.Left[int]("error2"), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - left identity", func(t *testing.T) {
|
||||
re := Right[Config, string](42)
|
||||
combined := reMonoid.Concat(reMonoid.Empty(), re)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - right identity", func(t *testing.T) {
|
||||
re := Right[Config, string](42)
|
||||
combined := reMonoid.Concat(re, reMonoid.Empty())
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("fallback chain with first success", func(t *testing.T) {
|
||||
re1 := Right[Config, string](10)
|
||||
re2 := Right[Config, string](20)
|
||||
re3 := Right[Config, string](30)
|
||||
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(re1, re2),
|
||||
re3,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
// First success is used
|
||||
assert.Equal(t, E.Right[string](10), result)
|
||||
})
|
||||
|
||||
t.Run("fallback chain with middle success", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Right[Config, string](20)
|
||||
re3 := Right[Config, string](30)
|
||||
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(re1, re2),
|
||||
re3,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
// First success (re2) is used
|
||||
assert.Equal(t, E.Right[string](20), result)
|
||||
})
|
||||
|
||||
t.Run("fallback chain with last success", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Left[Config, int]("error2")
|
||||
re3 := Right[Config, string](30)
|
||||
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(re1, re2),
|
||||
re3,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
// Last success is used
|
||||
assert.Equal(t, E.Right[string](30), result)
|
||||
})
|
||||
|
||||
t.Run("environment dependent fallback", func(t *testing.T) {
|
||||
re1 := MonadChain(
|
||||
Ask[Config, string](),
|
||||
func(cfg Config) ReaderEither[Config, string, int] {
|
||||
if cfg.Debug {
|
||||
return Right[Config, string](cfg.Port)
|
||||
}
|
||||
return Left[Config, int]("debug disabled")
|
||||
},
|
||||
)
|
||||
re2 := Right[Config, string](9999)
|
||||
|
||||
combined := reMonoid.Concat(re1, re2)
|
||||
result := combined(defaultConfig)
|
||||
// First fails (debug is false), falls back to second
|
||||
assert.Equal(t, E.Right[string](9999), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidLaws verifies that the monoid laws hold for ApplicativeMonoid
|
||||
func TestApplicativeMonoidLaws(t *testing.T) {
|
||||
reMonoid := ApplicativeMonoid[Config, string](intAddMonoid)
|
||||
|
||||
t.Run("left identity law", func(t *testing.T) {
|
||||
// empty <> x == x
|
||||
x := Right[Config, string](42)
|
||||
result1 := reMonoid.Concat(reMonoid.Empty(), x)(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("right identity law", func(t *testing.T) {
|
||||
// x <> empty == x
|
||||
x := Right[Config, string](42)
|
||||
result1 := reMonoid.Concat(x, reMonoid.Empty())(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("associativity law", func(t *testing.T) {
|
||||
// (x <> y) <> z == x <> (y <> z)
|
||||
x := Right[Config, string](1)
|
||||
y := Right[Config, string](2)
|
||||
z := Right[Config, string](3)
|
||||
|
||||
left := reMonoid.Concat(reMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := reMonoid.Concat(x, reMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("associativity with Left values", func(t *testing.T) {
|
||||
// Verify associativity even with Left values
|
||||
x := Right[Config, string](5)
|
||||
y := Left[Config, int]("error")
|
||||
z := Right[Config, string](10)
|
||||
|
||||
left := reMonoid.Concat(reMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := reMonoid.Concat(x, reMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlternativeMonoidLaws verifies that the monoid laws hold for AlternativeMonoid
|
||||
func TestAlternativeMonoidLaws(t *testing.T) {
|
||||
reMonoid := AlternativeMonoid[Config, string](intAddMonoid)
|
||||
|
||||
t.Run("left identity law", func(t *testing.T) {
|
||||
// empty <> x == x
|
||||
x := Right[Config, string](42)
|
||||
result1 := reMonoid.Concat(reMonoid.Empty(), x)(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("right identity law", func(t *testing.T) {
|
||||
// x <> empty == x
|
||||
x := Right[Config, string](42)
|
||||
result1 := reMonoid.Concat(x, reMonoid.Empty())(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("associativity law", func(t *testing.T) {
|
||||
// (x <> y) <> z == x <> (y <> z)
|
||||
x := Right[Config, string](1)
|
||||
y := Right[Config, string](2)
|
||||
z := Right[Config, string](3)
|
||||
|
||||
left := reMonoid.Concat(reMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := reMonoid.Concat(x, reMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("associativity with Left values", func(t *testing.T) {
|
||||
// Verify associativity even with Left values
|
||||
x := Left[Config, int]("error1")
|
||||
y := Right[Config, string](5)
|
||||
z := Right[Config, string](10)
|
||||
|
||||
left := reMonoid.Concat(reMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := reMonoid.Concat(x, reMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltMonoidLaws verifies that the monoid laws hold for AltMonoid
|
||||
func TestAltMonoidLaws(t *testing.T) {
|
||||
zero := func() ReaderEither[Config, string, int] {
|
||||
return Left[Config, int]("no value")
|
||||
}
|
||||
reMonoid := AltMonoid(zero)
|
||||
|
||||
t.Run("left identity law", func(t *testing.T) {
|
||||
// empty <> x == x
|
||||
x := Right[Config, string](42)
|
||||
result1 := reMonoid.Concat(reMonoid.Empty(), x)(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("right identity law", func(t *testing.T) {
|
||||
// x <> empty == x
|
||||
x := Right[Config, string](42)
|
||||
result1 := reMonoid.Concat(x, reMonoid.Empty())(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("associativity law", func(t *testing.T) {
|
||||
// (x <> y) <> z == x <> (y <> z)
|
||||
x := Right[Config, string](1)
|
||||
y := Right[Config, string](2)
|
||||
z := Right[Config, string](3)
|
||||
|
||||
left := reMonoid.Concat(reMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := reMonoid.Concat(x, reMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("associativity with Left values", func(t *testing.T) {
|
||||
// Verify associativity even with Left values
|
||||
x := Left[Config, int]("error1")
|
||||
y := Left[Config, int]("error2")
|
||||
z := Right[Config, string](10)
|
||||
|
||||
left := reMonoid.Concat(reMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := reMonoid.Concat(x, reMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVsAlternative compares the behavior of both monoids
|
||||
func TestApplicativeVsAlternative(t *testing.T) {
|
||||
applicativeMonoid := ApplicativeMonoid[Config, string](intAddMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[Config, string](intAddMonoid)
|
||||
|
||||
t.Run("both succeed - same result", func(t *testing.T) {
|
||||
re1 := Right[Config, string](5)
|
||||
re2 := Right[Config, string](3)
|
||||
|
||||
appResult := applicativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
altResult := alternativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
|
||||
assert.Equal(t, E.Right[string](8), appResult)
|
||||
assert.Equal(t, E.Right[string](8), altResult)
|
||||
assert.Equal(t, appResult, altResult)
|
||||
})
|
||||
|
||||
t.Run("first fails - different behavior", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error")
|
||||
re2 := Right[Config, string](42)
|
||||
|
||||
appResult := applicativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
altResult := alternativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
|
||||
// Applicative fails if any fails
|
||||
assert.Equal(t, E.Left[int]("error"), appResult)
|
||||
// Alternative falls back to second
|
||||
assert.Equal(t, E.Right[string](42), altResult)
|
||||
})
|
||||
|
||||
t.Run("second fails - different behavior", func(t *testing.T) {
|
||||
re1 := Right[Config, string](42)
|
||||
re2 := Left[Config, int]("error")
|
||||
|
||||
appResult := applicativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
altResult := alternativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
|
||||
// Applicative fails if any fails
|
||||
assert.Equal(t, E.Left[int]("error"), appResult)
|
||||
// Alternative uses first success
|
||||
assert.Equal(t, E.Right[string](42), altResult)
|
||||
})
|
||||
|
||||
t.Run("both fail - different behavior", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error1")
|
||||
re2 := Left[Config, int]("error2")
|
||||
|
||||
appResult := applicativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
altResult := alternativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
|
||||
// Applicative returns first error
|
||||
assert.Equal(t, E.Left[int]("error1"), appResult)
|
||||
// Alternative returns second error
|
||||
assert.Equal(t, E.Left[int]("error2"), altResult)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlternativeVsAlt compares AlternativeMonoid and AltMonoid
|
||||
func TestAlternativeVsAlt(t *testing.T) {
|
||||
alternativeMonoid := AlternativeMonoid[Config, string](intAddMonoid)
|
||||
zero := func() ReaderEither[Config, string, int] {
|
||||
return Left[Config, int]("no value")
|
||||
}
|
||||
altMonoid := AltMonoid(zero)
|
||||
|
||||
t.Run("both succeed - different behavior", func(t *testing.T) {
|
||||
re1 := Right[Config, string](5)
|
||||
re2 := Right[Config, string](3)
|
||||
|
||||
altResult := alternativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
altMonoidResult := altMonoid.Concat(re1, re2)(defaultConfig)
|
||||
|
||||
// Alternative combines values
|
||||
assert.Equal(t, E.Right[string](8), altResult)
|
||||
// AltMonoid uses first success
|
||||
assert.Equal(t, E.Right[string](5), altMonoidResult)
|
||||
})
|
||||
|
||||
t.Run("first fails - same behavior", func(t *testing.T) {
|
||||
re1 := Left[Config, int]("error")
|
||||
re2 := Right[Config, string](42)
|
||||
|
||||
altResult := alternativeMonoid.Concat(re1, re2)(defaultConfig)
|
||||
altMonoidResult := altMonoid.Concat(re1, re2)(defaultConfig)
|
||||
|
||||
// Both fall back to second
|
||||
assert.Equal(t, E.Right[string](42), altResult)
|
||||
assert.Equal(t, E.Right[string](42), altMonoidResult)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComplexScenarios tests more complex real-world scenarios
|
||||
func TestComplexScenarios(t *testing.T) {
|
||||
t.Run("accumulate configuration values", func(t *testing.T) {
|
||||
reMonoid := ApplicativeMonoid[Config, string](intAddMonoid)
|
||||
|
||||
// Accumulate multiple configuration values
|
||||
getPort := Asks[string](func(cfg Config) int {
|
||||
return cfg.Port
|
||||
})
|
||||
getTimeout := Asks[string](func(cfg Config) int {
|
||||
return cfg.Timeout
|
||||
})
|
||||
getConstant := Right[Config, string](100)
|
||||
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(getPort, getTimeout),
|
||||
getConstant,
|
||||
)
|
||||
|
||||
result := combined(defaultConfig)
|
||||
// 8080 + 30 + 100 = 8210
|
||||
assert.Equal(t, E.Right[string](8210), result)
|
||||
})
|
||||
|
||||
t.Run("fallback configuration loading", func(t *testing.T) {
|
||||
reMonoid := AlternativeMonoid[Config, string](strMonoid)
|
||||
|
||||
// Simulate trying to load config from multiple sources
|
||||
fromEnv := Left[Config, string]("env not set")
|
||||
fromFile := Left[Config, string]("file not found")
|
||||
fromDefault := Right[Config, string]("default-config")
|
||||
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(fromEnv, fromFile),
|
||||
fromDefault,
|
||||
)
|
||||
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, E.Right[string]("default-config"), result)
|
||||
})
|
||||
|
||||
t.Run("partial success accumulation", func(t *testing.T) {
|
||||
reMonoid := AlternativeMonoid[Config, string](intAddMonoid)
|
||||
|
||||
// Simulate collecting metrics where some may fail
|
||||
metric1 := Right[Config, string](100)
|
||||
metric2 := Left[Config, int]("metric2 failed") // Failed to collect
|
||||
metric3 := Right[Config, string](200)
|
||||
metric4 := Left[Config, int]("metric4 failed") // Failed to collect
|
||||
metric5 := Right[Config, string](300)
|
||||
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(
|
||||
reMonoid.Concat(
|
||||
reMonoid.Concat(metric1, metric2),
|
||||
metric3,
|
||||
),
|
||||
metric4,
|
||||
),
|
||||
metric5,
|
||||
)
|
||||
|
||||
result := combined(defaultConfig)
|
||||
// Should accumulate only successful metrics: 100 + 200 + 300 = 600
|
||||
assert.Equal(t, E.Right[string](600), result)
|
||||
})
|
||||
|
||||
t.Run("cascading fallback with AltMonoid", func(t *testing.T) {
|
||||
zero := func() ReaderEither[Config, string, string] {
|
||||
return Left[Config, string]("all sources failed")
|
||||
}
|
||||
reMonoid := AltMonoid(zero)
|
||||
|
||||
// Try multiple data sources in order
|
||||
primaryDB := Left[Config, string]("primary DB down")
|
||||
secondaryDB := Left[Config, string]("secondary DB down")
|
||||
cache := Right[Config, string]("cached-data")
|
||||
fallback := Right[Config, string]("fallback-data")
|
||||
|
||||
combined := reMonoid.Concat(
|
||||
reMonoid.Concat(
|
||||
reMonoid.Concat(primaryDB, secondaryDB),
|
||||
cache,
|
||||
),
|
||||
fallback,
|
||||
)
|
||||
|
||||
result := combined(defaultConfig)
|
||||
// Should use first successful source (cache)
|
||||
assert.Equal(t, E.Right[string]("cached-data"), result)
|
||||
})
|
||||
}
|
||||
@@ -461,3 +461,24 @@ func TapLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA, A, A]
|
||||
func MonadFold[E, L, A, B any](ma ReaderEither[E, L, A], onLeft func(L) Reader[E, B], onRight func(A) Reader[E, B]) Reader[E, B] {
|
||||
return Fold(onLeft, onRight)(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadAlt[R, E, A any](first ReaderEither[R, E, A], second Lazy[ReaderEither[R, E, A]]) ReaderEither[R, E, A] {
|
||||
return eithert.MonadAlt(
|
||||
reader.Of[R, Either[E, A]],
|
||||
reader.MonadChain[R, Either[E, A], Either[E, A]],
|
||||
|
||||
first,
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Alt[R, E, A any](second Lazy[ReaderEither[R, E, A]]) Operator[R, E, A, A] {
|
||||
return eithert.Alt(
|
||||
reader.Of[R, Either[E, A]],
|
||||
reader.Chain[R, Either[E, A], Either[E, A]],
|
||||
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ func TestReadEither(t *testing.T) {
|
||||
Map[Config, string](func(cfg Config) string {
|
||||
return cfg.host + "/data"
|
||||
}),
|
||||
Chain[Config, string, string, int](func(url string) ReaderEither[Config, string, int] {
|
||||
Chain(func(url string) ReaderEither[Config, string, int] {
|
||||
return func(cfg Config) Either[string, int] {
|
||||
if cfg.apiKey != "" {
|
||||
return ET.Right[string](len(url))
|
||||
|
||||
@@ -17,6 +17,7 @@ package readereither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
@@ -44,4 +45,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].
|
||||
Operator[R, E, A, B any] = Kleisli[R, E, ReaderEither[R, E, A], B]
|
||||
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ type (
|
||||
//
|
||||
// Returns a Monoid for ReaderIOResult[A].
|
||||
func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return RIOE.AlternativeMonoid[R, error](m)
|
||||
return RIOE.ApplicativeMonoid[R, error](m)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidSeq returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
|
||||
|
||||
145
v2/readeroption/monoid.go
Normal file
145
v2/readeroption/monoid.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readeroption
|
||||
|
||||
import "github.com/IBM/fp-go/v2/monoid"
|
||||
|
||||
// ApplicativeMonoid creates a Monoid for ReaderOption based on Applicative functor composition.
|
||||
// The empty element is Of(m.Empty()), and concat combines two computations using the underlying monoid.
|
||||
// Both computations must succeed (return Some) for the result to succeed.
|
||||
//
|
||||
// This is useful for accumulating results from multiple independent computations that all need
|
||||
// to succeed. If any computation returns None, the entire result is None.
|
||||
//
|
||||
// The resulting monoid satisfies the monoid laws:
|
||||
// - Left identity: Concat(Empty(), x) = x
|
||||
// - Right identity: Concat(x, Empty()) = x
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for combining success values of type A
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[ReaderOption[R, A]] that combines ReaderOption computations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// RO "github.com/IBM/fp-go/v2/readeroption"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid for integer addition
|
||||
// intAdd := N.MonoidSum[int]()
|
||||
// roMonoid := RO.ApplicativeMonoid[Config](intAdd)
|
||||
//
|
||||
// // Combine successful computations
|
||||
// ro1 := RO.Of[Config](5)
|
||||
// ro2 := RO.Of[Config](3)
|
||||
// combined := roMonoid.Concat(ro1, ro2)
|
||||
// // combined(cfg) returns option.Some(8)
|
||||
//
|
||||
// // If either fails, the whole computation fails
|
||||
// ro3 := RO.None[Config, int]()
|
||||
// failed := roMonoid.Concat(ro1, ro3)
|
||||
// // failed(cfg) returns option.None[int]()
|
||||
//
|
||||
// // Empty element is the identity
|
||||
// withEmpty := roMonoid.Concat(ro1, roMonoid.Empty())
|
||||
// // withEmpty(cfg) returns option.Some(5)
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) monoid.Monoid[ReaderOption[R, A]] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[R, A, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a Monoid for ReaderOption that combines both Alternative and Applicative behavior.
|
||||
// It uses the provided monoid for the success values and falls back to alternative computations on failure.
|
||||
//
|
||||
// The empty element is Of(m.Empty()), and concat tries the first computation, falling back to the second
|
||||
// if it fails (returns None), then combines successful values using the underlying monoid.
|
||||
//
|
||||
// This is particularly useful when you want to:
|
||||
// - Try multiple computations and accumulate their results
|
||||
// - Provide fallback behavior when computations fail
|
||||
// - Combine results from computations that may or may not succeed
|
||||
//
|
||||
// The behavior differs from ApplicativeMonoid in that it provides fallback semantics:
|
||||
// - If the first computation succeeds, use its value
|
||||
// - If the first fails but the second succeeds, use the second's value
|
||||
// - If both succeed, combine their values using the underlying monoid
|
||||
// - If both fail, the result is None
|
||||
//
|
||||
// The resulting monoid satisfies the monoid laws:
|
||||
// - Left identity: Concat(Empty(), x) = x
|
||||
// - Right identity: Concat(x, Empty()) = x
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for combining success values of type A
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[ReaderOption[R, A]] that combines ReaderOption computations with fallback
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// RO "github.com/IBM/fp-go/v2/readeroption"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid for integer addition with alternative behavior
|
||||
// intAdd := N.MonoidSum[int]()
|
||||
// roMonoid := RO.AlternativeMonoid[Config](intAdd)
|
||||
//
|
||||
// // Combine successful computations
|
||||
// ro1 := RO.Of[Config](5)
|
||||
// ro2 := RO.Of[Config](3)
|
||||
// combined := roMonoid.Concat(ro1, ro2)
|
||||
// // combined(cfg) returns option.Some(8)
|
||||
//
|
||||
// // Fallback when first fails
|
||||
// ro3 := RO.None[Config, int]()
|
||||
// ro4 := RO.Of[Config](10)
|
||||
// withFallback := roMonoid.Concat(ro3, ro4)
|
||||
// // withFallback(cfg) returns option.Some(10)
|
||||
//
|
||||
// // Use first success when available
|
||||
// withFirst := roMonoid.Concat(ro1, ro3)
|
||||
// // withFirst(cfg) returns option.Some(5)
|
||||
//
|
||||
// // Accumulate multiple values with some failures
|
||||
// result := roMonoid.Concat(
|
||||
// roMonoid.Concat(ro3, ro1), // None + 5 = 5
|
||||
// ro2, // 5 + 3 = 8
|
||||
// )
|
||||
// // result(cfg) returns option.Some(8)
|
||||
//
|
||||
//go:inline
|
||||
func AlternativeMonoid[R, A any](m monoid.Monoid[A]) monoid.Monoid[ReaderOption[R, A]] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[R, A, A],
|
||||
MonadAlt[R, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
472
v2/readeroption/monoid_test.go
Normal file
472
v2/readeroption/monoid_test.go
Normal file
@@ -0,0 +1,472 @@
|
||||
// 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 readeroption
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
intAddMonoid = N.MonoidSum[int]()
|
||||
strMonoid = S.Monoid
|
||||
)
|
||||
|
||||
// TestApplicativeMonoid tests the ApplicativeMonoid function
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
roMonoid := ApplicativeMonoid[Config](intAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := roMonoid.Empty()
|
||||
result := empty(defaultConfig)
|
||||
assert.Equal(t, O.Some(0), result)
|
||||
})
|
||||
|
||||
t.Run("concat two Some values", func(t *testing.T) {
|
||||
ro1 := Of[Config](5)
|
||||
ro2 := Of[Config](3)
|
||||
combined := roMonoid.Concat(ro1, ro2)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some(8), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - left identity", func(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
combined := roMonoid.Concat(roMonoid.Empty(), ro)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - right identity", func(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
combined := roMonoid.Concat(ro, roMonoid.Empty())
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat with left None", func(t *testing.T) {
|
||||
roSuccess := Of[Config](5)
|
||||
roFailure := None[Config, int]()
|
||||
|
||||
combined := roMonoid.Concat(roFailure, roSuccess)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("concat with right None", func(t *testing.T) {
|
||||
roSuccess := Of[Config](5)
|
||||
roFailure := None[Config, int]()
|
||||
|
||||
combined := roMonoid.Concat(roSuccess, roFailure)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("concat both None", func(t *testing.T) {
|
||||
ro1 := None[Config, int]()
|
||||
ro2 := None[Config, int]()
|
||||
|
||||
combined := roMonoid.Concat(ro1, ro2)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("concat multiple values", func(t *testing.T) {
|
||||
ro1 := Of[Config](1)
|
||||
ro2 := Of[Config](2)
|
||||
ro3 := Of[Config](3)
|
||||
ro4 := Of[Config](4)
|
||||
|
||||
// Chain concat calls: ((1 + 2) + 3) + 4
|
||||
combined := roMonoid.Concat(
|
||||
roMonoid.Concat(
|
||||
roMonoid.Concat(ro1, ro2),
|
||||
ro3,
|
||||
),
|
||||
ro4,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
strROMonoid := ApplicativeMonoid[Config](strMonoid)
|
||||
|
||||
ro1 := Of[Config]("Hello")
|
||||
ro2 := Of[Config](" ")
|
||||
ro3 := Of[Config]("World")
|
||||
|
||||
combined := strROMonoid.Concat(
|
||||
strROMonoid.Concat(ro1, ro2),
|
||||
ro3,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some("Hello World"), result)
|
||||
})
|
||||
|
||||
t.Run("environment dependent computation", func(t *testing.T) {
|
||||
// Create computations that use the environment
|
||||
ro1 := Asks(func(cfg Config) int {
|
||||
return cfg.Port
|
||||
})
|
||||
ro2 := Of[Config](100)
|
||||
|
||||
combined := roMonoid.Concat(ro1, ro2)
|
||||
result := combined(defaultConfig)
|
||||
// defaultConfig.Port is 8080, so 8080 + 100 = 8180
|
||||
assert.Equal(t, O.Some(8180), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlternativeMonoid tests the AlternativeMonoid function
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
roMonoid := AlternativeMonoid[Config](intAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := roMonoid.Empty()
|
||||
result := empty(defaultConfig)
|
||||
assert.Equal(t, O.Some(0), result)
|
||||
})
|
||||
|
||||
t.Run("concat two Some values", func(t *testing.T) {
|
||||
ro1 := Of[Config](5)
|
||||
ro2 := Of[Config](3)
|
||||
combined := roMonoid.Concat(ro1, ro2)
|
||||
result := combined(defaultConfig)
|
||||
// Alternative combines successful values
|
||||
assert.Equal(t, O.Some(8), result)
|
||||
})
|
||||
|
||||
t.Run("concat None then Some - fallback behavior", func(t *testing.T) {
|
||||
roFailure := None[Config, int]()
|
||||
roSuccess := Of[Config](42)
|
||||
|
||||
combined := roMonoid.Concat(roFailure, roSuccess)
|
||||
result := combined(defaultConfig)
|
||||
// Should fall back to second when first fails
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat Some then None - uses first", func(t *testing.T) {
|
||||
roSuccess := Of[Config](42)
|
||||
roFailure := None[Config, int]()
|
||||
|
||||
combined := roMonoid.Concat(roSuccess, roFailure)
|
||||
result := combined(defaultConfig)
|
||||
// Should use first successful value
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat both None", func(t *testing.T) {
|
||||
ro1 := None[Config, int]()
|
||||
ro2 := None[Config, int]()
|
||||
|
||||
combined := roMonoid.Concat(ro1, ro2)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - left identity", func(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
combined := roMonoid.Concat(roMonoid.Empty(), ro)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty - right identity", func(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
combined := roMonoid.Concat(ro, roMonoid.Empty())
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("multiple values with some failures", func(t *testing.T) {
|
||||
ro1 := None[Config, int]()
|
||||
ro2 := Of[Config](5)
|
||||
ro3 := None[Config, int]()
|
||||
ro4 := Of[Config](10)
|
||||
|
||||
// Alternative should skip failures and accumulate successes
|
||||
combined := roMonoid.Concat(
|
||||
roMonoid.Concat(
|
||||
roMonoid.Concat(ro1, ro2),
|
||||
ro3,
|
||||
),
|
||||
ro4,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
// Should accumulate successful values: 5 + 10 = 15
|
||||
assert.Equal(t, O.Some(15), result)
|
||||
})
|
||||
|
||||
t.Run("fallback chain", func(t *testing.T) {
|
||||
// Simulate trying multiple sources until one succeeds
|
||||
primary := None[Config, string]()
|
||||
secondary := None[Config, string]()
|
||||
tertiary := Of[Config]("tertiary success")
|
||||
|
||||
strROMonoid := AlternativeMonoid[Config](strMonoid)
|
||||
|
||||
// Chain concat: try primary, then secondary, then tertiary
|
||||
combined := strROMonoid.Concat(
|
||||
strROMonoid.Concat(primary, secondary),
|
||||
tertiary,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some("tertiary success"), result)
|
||||
})
|
||||
|
||||
t.Run("environment dependent with fallback", func(t *testing.T) {
|
||||
// First computation fails
|
||||
ro1 := None[Config, int]()
|
||||
// Second computation uses environment
|
||||
ro2 := Asks(func(cfg Config) int {
|
||||
return cfg.Timeout
|
||||
})
|
||||
|
||||
combined := roMonoid.Concat(ro1, ro2)
|
||||
result := combined(defaultConfig)
|
||||
// Should fall back to second computation
|
||||
assert.Equal(t, O.Some(30), result)
|
||||
})
|
||||
|
||||
t.Run("all failures in chain", func(t *testing.T) {
|
||||
ro1 := None[Config, int]()
|
||||
ro2 := None[Config, int]()
|
||||
ro3 := None[Config, int]()
|
||||
|
||||
combined := roMonoid.Concat(
|
||||
roMonoid.Concat(ro1, ro2),
|
||||
ro3,
|
||||
)
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidLaws verifies that the monoid laws hold for ApplicativeMonoid
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
roMonoid := ApplicativeMonoid[Config](intAddMonoid)
|
||||
|
||||
t.Run("left identity law", func(t *testing.T) {
|
||||
// empty <> x == x
|
||||
x := Of[Config](42)
|
||||
result1 := roMonoid.Concat(roMonoid.Empty(), x)(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("right identity law", func(t *testing.T) {
|
||||
// x <> empty == x
|
||||
x := Of[Config](42)
|
||||
result1 := roMonoid.Concat(x, roMonoid.Empty())(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("associativity law", func(t *testing.T) {
|
||||
// (x <> y) <> z == x <> (y <> z)
|
||||
x := Of[Config](1)
|
||||
y := Of[Config](2)
|
||||
z := Of[Config](3)
|
||||
|
||||
left := roMonoid.Concat(roMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := roMonoid.Concat(x, roMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("associativity with None values", func(t *testing.T) {
|
||||
// Verify associativity even with None values
|
||||
x := Of[Config](5)
|
||||
y := None[Config, int]()
|
||||
z := Of[Config](10)
|
||||
|
||||
left := roMonoid.Concat(roMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := roMonoid.Concat(x, roMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlternativeMonoidLaws verifies that the monoid laws hold for AlternativeMonoid
|
||||
func TestAlternativeMonoidLaws(t *testing.T) {
|
||||
roMonoid := AlternativeMonoid[Config](intAddMonoid)
|
||||
|
||||
t.Run("left identity law", func(t *testing.T) {
|
||||
// empty <> x == x
|
||||
x := Of[Config](42)
|
||||
result1 := roMonoid.Concat(roMonoid.Empty(), x)(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("right identity law", func(t *testing.T) {
|
||||
// x <> empty == x
|
||||
x := Of[Config](42)
|
||||
result1 := roMonoid.Concat(x, roMonoid.Empty())(defaultConfig)
|
||||
result2 := x(defaultConfig)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
t.Run("associativity law", func(t *testing.T) {
|
||||
// (x <> y) <> z == x <> (y <> z)
|
||||
x := Of[Config](1)
|
||||
y := Of[Config](2)
|
||||
z := Of[Config](3)
|
||||
|
||||
left := roMonoid.Concat(roMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := roMonoid.Concat(x, roMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("associativity with None values", func(t *testing.T) {
|
||||
// Verify associativity even with None values
|
||||
x := None[Config, int]()
|
||||
y := Of[Config](5)
|
||||
z := Of[Config](10)
|
||||
|
||||
left := roMonoid.Concat(roMonoid.Concat(x, y), z)(defaultConfig)
|
||||
right := roMonoid.Concat(x, roMonoid.Concat(y, z))(defaultConfig)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVsAlternative compares the behavior of both monoids
|
||||
func TestApplicativeVsAlternative(t *testing.T) {
|
||||
applicativeMonoid := ApplicativeMonoid[Config](intAddMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[Config](intAddMonoid)
|
||||
|
||||
t.Run("both succeed - same result", func(t *testing.T) {
|
||||
ro1 := Of[Config](5)
|
||||
ro2 := Of[Config](3)
|
||||
|
||||
appResult := applicativeMonoid.Concat(ro1, ro2)(defaultConfig)
|
||||
altResult := alternativeMonoid.Concat(ro1, ro2)(defaultConfig)
|
||||
|
||||
assert.Equal(t, O.Some(8), appResult)
|
||||
assert.Equal(t, O.Some(8), altResult)
|
||||
assert.Equal(t, appResult, altResult)
|
||||
})
|
||||
|
||||
t.Run("first fails - different behavior", func(t *testing.T) {
|
||||
ro1 := None[Config, int]()
|
||||
ro2 := Of[Config](42)
|
||||
|
||||
appResult := applicativeMonoid.Concat(ro1, ro2)(defaultConfig)
|
||||
altResult := alternativeMonoid.Concat(ro1, ro2)(defaultConfig)
|
||||
|
||||
// Applicative fails if any fails
|
||||
assert.Equal(t, O.None[int](), appResult)
|
||||
// Alternative falls back to second
|
||||
assert.Equal(t, O.Some(42), altResult)
|
||||
})
|
||||
|
||||
t.Run("second fails - different behavior", func(t *testing.T) {
|
||||
ro1 := Of[Config](42)
|
||||
ro2 := None[Config, int]()
|
||||
|
||||
appResult := applicativeMonoid.Concat(ro1, ro2)(defaultConfig)
|
||||
altResult := alternativeMonoid.Concat(ro1, ro2)(defaultConfig)
|
||||
|
||||
// Applicative fails if any fails
|
||||
assert.Equal(t, O.None[int](), appResult)
|
||||
// Alternative uses first success
|
||||
assert.Equal(t, O.Some(42), altResult)
|
||||
})
|
||||
|
||||
t.Run("both fail - same result", func(t *testing.T) {
|
||||
ro1 := None[Config, int]()
|
||||
ro2 := None[Config, int]()
|
||||
|
||||
appResult := applicativeMonoid.Concat(ro1, ro2)(defaultConfig)
|
||||
altResult := alternativeMonoid.Concat(ro1, ro2)(defaultConfig)
|
||||
|
||||
assert.Equal(t, O.None[int](), appResult)
|
||||
assert.Equal(t, O.None[int](), altResult)
|
||||
assert.Equal(t, appResult, altResult)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComplexScenarios tests more complex real-world scenarios
|
||||
func TestComplexScenarios(t *testing.T) {
|
||||
t.Run("accumulate configuration values", func(t *testing.T) {
|
||||
roMonoid := ApplicativeMonoid[Config](intAddMonoid)
|
||||
|
||||
// Accumulate multiple configuration values
|
||||
getPort := Asks(func(cfg Config) int { return cfg.Port })
|
||||
getTimeout := Asks(func(cfg Config) int { return cfg.Timeout })
|
||||
getConstant := Of[Config](100)
|
||||
|
||||
combined := roMonoid.Concat(
|
||||
roMonoid.Concat(getPort, getTimeout),
|
||||
getConstant,
|
||||
)
|
||||
|
||||
result := combined(defaultConfig)
|
||||
// 8080 + 30 + 100 = 8210
|
||||
assert.Equal(t, O.Some(8210), result)
|
||||
})
|
||||
|
||||
t.Run("fallback configuration loading", func(t *testing.T) {
|
||||
roMonoid := AlternativeMonoid[Config](strMonoid)
|
||||
|
||||
// Simulate trying to load config from multiple sources
|
||||
fromEnv := None[Config, string]()
|
||||
fromFile := None[Config, string]()
|
||||
fromDefault := Of[Config]("default-config")
|
||||
|
||||
combined := roMonoid.Concat(
|
||||
roMonoid.Concat(fromEnv, fromFile),
|
||||
fromDefault,
|
||||
)
|
||||
|
||||
result := combined(defaultConfig)
|
||||
assert.Equal(t, O.Some("default-config"), result)
|
||||
})
|
||||
|
||||
t.Run("partial success accumulation", func(t *testing.T) {
|
||||
roMonoid := AlternativeMonoid[Config](intAddMonoid)
|
||||
|
||||
// Simulate collecting metrics where some may fail
|
||||
metric1 := Of[Config](100)
|
||||
metric2 := None[Config, int]() // Failed to collect
|
||||
metric3 := Of[Config](200)
|
||||
metric4 := None[Config, int]() // Failed to collect
|
||||
metric5 := Of[Config](300)
|
||||
|
||||
combined := roMonoid.Concat(
|
||||
roMonoid.Concat(
|
||||
roMonoid.Concat(
|
||||
roMonoid.Concat(metric1, metric2),
|
||||
metric3,
|
||||
),
|
||||
metric4,
|
||||
),
|
||||
metric5,
|
||||
)
|
||||
|
||||
result := combined(defaultConfig)
|
||||
// Should accumulate only successful metrics: 100 + 200 + 300 = 600
|
||||
assert.Equal(t, O.Some(600), result)
|
||||
})
|
||||
}
|
||||
@@ -384,8 +384,13 @@ func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
|
||||
// result := readeroption.MonadAlt(primary, fallback)
|
||||
//
|
||||
//go:inline
|
||||
func MonadAlt[E, A any](fa, that ReaderOption[E, A]) ReaderOption[E, A] {
|
||||
return MonadFold(fa, that, Of[E, A])
|
||||
func MonadAlt[E, A any](first ReaderOption[E, A], second Lazy[ReaderOption[E, A]]) ReaderOption[E, A] {
|
||||
return optiont.MonadAlt(
|
||||
reader.Of[E, Option[A]],
|
||||
reader.MonadChain[E, Option[A], Option[A]],
|
||||
first,
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
// Alt returns a function that provides an alternative ReaderOption if the first one returns None.
|
||||
@@ -399,6 +404,10 @@ func MonadAlt[E, A any](fa, that ReaderOption[E, A]) ReaderOption[E, A] {
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Alt[E, A any](that ReaderOption[E, A]) Operator[E, A, A] {
|
||||
return Fold(that, Of[E, A])
|
||||
func Alt[E, A any](second Lazy[ReaderOption[E, A]]) Operator[E, A, A] {
|
||||
return optiont.Alt(
|
||||
reader.Of[E, Option[A]],
|
||||
reader.Chain[E, Option[A], Option[A]],
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -473,21 +474,21 @@ func TestMonadAlt(t *testing.T) {
|
||||
t.Run("Alt with first Some", func(t *testing.T) {
|
||||
primary := Of[Config](42)
|
||||
fallback := Of[Config](99)
|
||||
result := MonadAlt(primary, fallback)
|
||||
result := MonadAlt(primary, lazy.Of(fallback))
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Alt with first None", func(t *testing.T) {
|
||||
primary := None[Config, int]()
|
||||
fallback := Of[Config](99)
|
||||
result := MonadAlt(primary, fallback)
|
||||
result := MonadAlt(primary, lazy.Of(fallback))
|
||||
assert.Equal(t, O.Some(99), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Alt with both None", func(t *testing.T) {
|
||||
primary := None[Config, int]()
|
||||
fallback := None[Config, int]()
|
||||
result := MonadAlt(primary, fallback)
|
||||
result := MonadAlt(primary, lazy.Of(fallback))
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
@@ -497,7 +498,7 @@ func TestAlt(t *testing.T) {
|
||||
t.Run("Alt with first Some", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](42),
|
||||
Alt(Of[Config](99)),
|
||||
Alt(lazy.Of(Of[Config](99))),
|
||||
)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
@@ -505,7 +506,7 @@ func TestAlt(t *testing.T) {
|
||||
t.Run("Alt with first None", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
None[Config, int](),
|
||||
Alt(Of[Config](99)),
|
||||
Alt(lazy.Of(Of[Config](99))),
|
||||
)
|
||||
assert.Equal(t, O.Some(99), result(defaultConfig))
|
||||
})
|
||||
|
||||
@@ -150,7 +150,7 @@ func TestApS(t *testing.T) {
|
||||
func TestBindIOEitherK(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[OuterCtx, InnerCtx, error](utils.Empty),
|
||||
BindIOEitherK[OuterCtx, InnerCtx, error](
|
||||
BindIOEitherK[OuterCtx, InnerCtx](
|
||||
utils.SetLastName,
|
||||
func(s utils.Initial) IOE.IOEither[error, string] {
|
||||
return IOE.Of[error]("Smith")
|
||||
@@ -168,7 +168,7 @@ func TestBindIOEitherKError(t *testing.T) {
|
||||
err := errors.New("io error")
|
||||
res := F.Pipe2(
|
||||
Do[OuterCtx, InnerCtx, error](utils.Empty),
|
||||
BindIOEitherK[OuterCtx, InnerCtx, error](
|
||||
BindIOEitherK[OuterCtx, InnerCtx](
|
||||
utils.SetLastName,
|
||||
func(s utils.Initial) IOE.IOEither[error, string] {
|
||||
return IOE.Left[string](err)
|
||||
@@ -244,7 +244,7 @@ func TestBindReaderIOK(t *testing.T) {
|
||||
func TestBindEitherK(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[OuterCtx, InnerCtx, error](utils.Empty),
|
||||
BindEitherK[OuterCtx, InnerCtx, error](
|
||||
BindEitherK[OuterCtx, InnerCtx](
|
||||
utils.SetLastName,
|
||||
func(s utils.Initial) E.Either[error, string] {
|
||||
return E.Of[error]("Brown")
|
||||
@@ -262,7 +262,7 @@ func TestBindEitherKError(t *testing.T) {
|
||||
err := errors.New("either error")
|
||||
res := F.Pipe2(
|
||||
Do[OuterCtx, InnerCtx, error](utils.Empty),
|
||||
BindEitherK[OuterCtx, InnerCtx, error](
|
||||
BindEitherK[OuterCtx, InnerCtx](
|
||||
utils.SetLastName,
|
||||
func(s utils.Initial) E.Either[error, string] {
|
||||
return E.Left[string](err)
|
||||
@@ -279,7 +279,7 @@ func TestBindEitherKError(t *testing.T) {
|
||||
func TestApIOEitherS(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[OuterCtx, InnerCtx, error](utils.Empty),
|
||||
ApIOEitherS[OuterCtx, InnerCtx, error](utils.SetLastName, IOE.Of[error]("Williams")),
|
||||
ApIOEitherS[OuterCtx, InnerCtx](utils.SetLastName, IOE.Of[error]("Williams")),
|
||||
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
|
||||
return s.LastName
|
||||
}),
|
||||
@@ -335,7 +335,7 @@ func TestApReaderIOS(t *testing.T) {
|
||||
func TestApEitherS(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[OuterCtx, InnerCtx, error](utils.Empty),
|
||||
ApEitherS[OuterCtx, InnerCtx, error](utils.SetLastName, E.Of[error]("Miller")),
|
||||
ApEitherS[OuterCtx, InnerCtx](utils.SetLastName, E.Of[error]("Miller")),
|
||||
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
|
||||
return s.LastName
|
||||
}),
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestSequence(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := Sequence[Config1, Config2, Context, error, int](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -79,7 +79,7 @@ func TestSequence(t *testing.T) {
|
||||
return RIOE.Left[Context, ReaderReaderIOEither[Config1, Context, error, int]](testErr)
|
||||
}
|
||||
|
||||
sequenced := Sequence[Config1, Config2, Context, error, int](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -105,7 +105,7 @@ func TestSequence(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := Sequence[Config1, Config2, Context, error, string](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
ctx := Context{contextID: "test"}
|
||||
|
||||
@@ -129,7 +129,7 @@ func TestSequence(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := Sequence[Config1, Config2, Context, error, int](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
result := sequenced(Config1{value1: 0})(Config2{value2: ""})(Context{contextID: ""})()
|
||||
assert.Equal(t, E.Right[error](0), result)
|
||||
@@ -142,7 +142,7 @@ func TestSequence(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := Sequence[Config1, Config2, Context, error, int](original)
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 3}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -166,7 +166,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := SequenceReader[Config1, Config2, Context, error, int](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -194,7 +194,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
return RIOE.Left[Context, R.Reader[Config1, int]](testErr)
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[Config1, Config2, Context, error, int](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
result := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(Context{contextID: "test"})()
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
@@ -210,7 +210,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[Config1, Config2, Context, error, string](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
ctx := Context{contextID: "test"}
|
||||
|
||||
@@ -230,7 +230,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[Config1, Config2, Context, error, int](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
result := sequenced(Config1{value1: 0})(Config2{value2: ""})(Context{contextID: ""})()
|
||||
assert.Equal(t, E.Right[error](0), result)
|
||||
@@ -243,7 +243,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[Config1, Config2, Context, error, int](original)
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
cfg1 := Config1{value1: 3}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -267,7 +267,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := SequenceReaderIO[Config1, Config2, Context, error, int](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -295,7 +295,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
return RIOE.Left[Context, readerio.ReaderIO[Config1, int]](testErr)
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO[Config1, Config2, Context, error, int](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
result := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(Context{contextID: "test"})()
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
@@ -316,7 +316,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO[Config1, Config2, Context, error, string](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
ctx := Context{contextID: "test"}
|
||||
|
||||
@@ -340,7 +340,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO[Config1, Config2, Context, error, int](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
result := sequenced(Config1{value1: 0})(Config2{value2: ""})(Context{contextID: ""})()
|
||||
assert.Equal(t, E.Right[error](0), result)
|
||||
@@ -358,7 +358,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO[Config1, Config2, Context, error, int](original)
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
@@ -389,7 +389,7 @@ func TestTraverse(t *testing.T) {
|
||||
}
|
||||
|
||||
// Apply traverse to swap order and transform
|
||||
traversed := Traverse[Config2, Config1, Context, error, int, string](transform)(original)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
cfg1 := Config1{value1: 100}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -407,7 +407,7 @@ func TestTraverse(t *testing.T) {
|
||||
return Of[Config1, Context, error](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
traversed := Traverse[Config2, Config1, Context, error, int, string](transform)(original)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
result := traversed(Config1{value1: 100})(Config2{value2: "test"})(Context{contextID: "test"})()
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
@@ -426,12 +426,12 @@ func TestTraverse(t *testing.T) {
|
||||
|
||||
// Test with negative value
|
||||
originalNeg := Of[Config2, Context, error](-1)
|
||||
traversedNeg := Traverse[Config2, Config1, Context, error, int, string](transform)(originalNeg)
|
||||
traversedNeg := Traverse[Config2](transform)(originalNeg)
|
||||
resultNeg := traversedNeg(Config1{value1: 100})(Config2{value2: "test"})(Context{contextID: "test"})()
|
||||
assert.Equal(t, E.Left[string](testErr), resultNeg)
|
||||
|
||||
// Test with positive value
|
||||
traversedPos := Traverse[Config2, Config1, Context, error, int, string](transform)(original)
|
||||
traversedPos := Traverse[Config2](transform)(original)
|
||||
resultPos := traversedPos(Config1{value1: 100})(Config2{value2: "test"})(Context{contextID: "test"})()
|
||||
assert.Equal(t, E.Right[error]("42"), resultPos)
|
||||
})
|
||||
@@ -447,7 +447,7 @@ func TestTraverse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Config2, Config1, Context, error, int, int](transform)(original)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
result := traversed(Config1{value1: 5})(Config2{value2: "test"})(Context{contextID: "test"})()
|
||||
assert.Equal(t, E.Right[error](50), result)
|
||||
@@ -462,7 +462,7 @@ func TestTraverse(t *testing.T) {
|
||||
|
||||
result := F.Pipe2(
|
||||
original,
|
||||
Traverse[Config2, Config1, Context, error, int, int](transform),
|
||||
Traverse[Config2](transform),
|
||||
func(k Kleisli[Config2, Context, error, Config1, int]) ReaderReaderIOEither[Config2, Context, error, int] {
|
||||
return k(Config1{value1: 5})
|
||||
},
|
||||
@@ -486,7 +486,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
}
|
||||
|
||||
// Apply traverse to introduce Config1 and swap order
|
||||
traversed := TraverseReader[Config2, Config1, Context, error, int, string](formatWithConfig)(original)
|
||||
traversed := TraverseReader[Config2, Config1, Context, error](formatWithConfig)(original)
|
||||
|
||||
cfg1 := Config1{value1: 5}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -504,7 +504,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
return R.Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2, Config1, Context, error, int, string](transform)(original)
|
||||
traversed := TraverseReader[Config2, Config1, Context, error](transform)(original)
|
||||
|
||||
result := traversed(Config1{value1: 5})(Config2{value2: "test"})(Context{contextID: "test"})()
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
@@ -520,7 +520,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2, Config1, Context, error, int, int](double)(original)
|
||||
traversed := TraverseReader[Config2, Config1, Context, error](double)(original)
|
||||
|
||||
result := traversed(Config1{value1: 3})(Config2{value2: "test"})(Context{contextID: "test"})()
|
||||
assert.Equal(t, E.Right[error](126), result)
|
||||
@@ -535,7 +535,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2, Config1, Context, error, int, int](transform)(original)
|
||||
traversed := TraverseReader[Config2, Config1, Context, error](transform)(original)
|
||||
|
||||
result := traversed(Config1{value1: 0})(Config2{value2: ""})(Context{contextID: ""})()
|
||||
assert.Equal(t, E.Right[error](0), result)
|
||||
@@ -550,7 +550,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2, Config1, Context, error, int, int](transform)(original)
|
||||
traversed := TraverseReader[Config2, Config1, Context, error](transform)(original)
|
||||
|
||||
cfg1 := Config1{value1: 5}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
@@ -574,7 +574,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
result := F.Pipe2(
|
||||
original,
|
||||
TraverseReader[Config2, Config1, Context, error, int, int](multiply),
|
||||
TraverseReader[Config2, Config1, Context, error](multiply),
|
||||
func(k Kleisli[Config2, Context, error, Config1, int]) ReaderReaderIOEither[Config2, Context, error, int] {
|
||||
return k(Config1{value1: 3})
|
||||
},
|
||||
@@ -593,7 +593,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sequence it
|
||||
sequenced := Sequence[Config1, Config2, Context, error, int](nested)
|
||||
sequenced := Sequence(nested)
|
||||
|
||||
// Then traverse with a transformation
|
||||
transform := func(n int) ReaderReaderIOEither[Config1, Context, error, string] {
|
||||
@@ -611,7 +611,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
|
||||
// Then apply traverse on a new computation
|
||||
original := Of[Config2, Context, error](5)
|
||||
traversed := Traverse[Config2, Config1, Context, error, int, string](transform)(original)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
result := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, E.Right[error]("length=5"), result)
|
||||
})
|
||||
@@ -626,21 +626,21 @@ func TestFlipIntegration(t *testing.T) {
|
||||
seqErr := func(cfg2 Config2) RIOE.ReaderIOEither[Context, error, ReaderReaderIOEither[Config1, Context, error, int]] {
|
||||
return RIOE.Left[Context, ReaderReaderIOEither[Config1, Context, error, int]](testErr)
|
||||
}
|
||||
seqResult := Sequence[Config1, Config2, Context, error, int](seqErr)(cfg1)(cfg2)(ctx)()
|
||||
seqResult := Sequence(seqErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, E.IsLeft(seqResult))
|
||||
|
||||
// Test SequenceReader with error
|
||||
seqReaderErr := func(cfg2 Config2) RIOE.ReaderIOEither[Context, error, R.Reader[Config1, int]] {
|
||||
return RIOE.Left[Context, R.Reader[Config1, int]](testErr)
|
||||
}
|
||||
seqReaderResult := SequenceReader[Config1, Config2, Context, error, int](seqReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
seqReaderResult := SequenceReader(seqReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, E.IsLeft(seqReaderResult))
|
||||
|
||||
// Test SequenceReaderIO with error
|
||||
seqReaderIOErr := func(cfg2 Config2) RIOE.ReaderIOEither[Context, error, readerio.ReaderIO[Config1, int]] {
|
||||
return RIOE.Left[Context, readerio.ReaderIO[Config1, int]](testErr)
|
||||
}
|
||||
seqReaderIOResult := SequenceReaderIO[Config1, Config2, Context, error, int](seqReaderIOErr)(cfg1)(cfg2)(ctx)()
|
||||
seqReaderIOResult := SequenceReaderIO(seqReaderIOErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, E.IsLeft(seqReaderIOResult))
|
||||
|
||||
// Test Traverse with error
|
||||
@@ -648,7 +648,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
travTransform := func(n int) ReaderReaderIOEither[Config1, Context, error, string] {
|
||||
return Of[Config1, Context, error](fmt.Sprintf("%d", n))
|
||||
}
|
||||
travResult := Traverse[Config2, Config1, Context, error, int, string](travTransform)(travErr)(cfg1)(cfg2)(ctx)()
|
||||
travResult := Traverse[Config2](travTransform)(travErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, E.IsLeft(travResult))
|
||||
|
||||
// Test TraverseReader with error
|
||||
@@ -656,7 +656,7 @@ func TestFlipIntegration(t *testing.T) {
|
||||
travReaderTransform := func(n int) R.Reader[Config1, string] {
|
||||
return R.Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
travReaderResult := TraverseReader[Config2, Config1, Context, error, int, string](travReaderTransform)(travReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
travReaderResult := TraverseReader[Config2, Config1, Context, error](travReaderTransform)(travReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, E.IsLeft(travReaderResult))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func FromReaderOption[R, C, A, E any](onNone Lazy[E]) Kleisli[R, C, E, ReaderOpt
|
||||
|
||||
//go:inline
|
||||
func FromReaderIOEither[C, E, R, A any](ma ReaderIOEither[R, E, A]) ReaderReaderIOEither[R, C, E, A] {
|
||||
return reader.MonadMap[R](ma, RIOE.FromIOEither[C])
|
||||
return reader.MonadMap(ma, RIOE.FromIOEither[C])
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -55,12 +55,12 @@ func FromReaderIO[C, E, R, A any](ma ReaderIO[R, A]) ReaderReaderIOEither[R, C,
|
||||
|
||||
//go:inline
|
||||
func RightReaderIO[C, E, R, A any](ma ReaderIO[R, A]) ReaderReaderIOEither[R, C, E, A] {
|
||||
return reader.MonadMap[R](ma, RIOE.RightIO[C, E, A])
|
||||
return reader.MonadMap(ma, RIOE.RightIO[C, E, A])
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LeftReaderIO[C, A, R, E any](me ReaderIO[R, E]) ReaderReaderIOEither[R, C, E, A] {
|
||||
return reader.MonadMap[R](me, RIOE.LeftIO[C, A, E])
|
||||
return reader.MonadMap(me, RIOE.LeftIO[C, A, E])
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -297,7 +297,7 @@ func ChainFirstReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operat
|
||||
|
||||
//go:inline
|
||||
func TapReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, A] {
|
||||
return ChainFirstReaderEitherK[C, E](f)
|
||||
return ChainFirstReaderEitherK[C](f)
|
||||
}
|
||||
|
||||
func ChainReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
|
||||
@@ -538,7 +538,7 @@ func FromIOEither[R, C, E, A any](ma IOEither[E, A]) ReaderReaderIOEither[R, C,
|
||||
|
||||
//go:inline
|
||||
func FromReaderEither[R, C, E, A any](ma RE.ReaderEither[R, E, A]) ReaderReaderIOEither[R, C, E, A] {
|
||||
return reader.MonadMap[R](ma, RIOE.FromEither[C])
|
||||
return reader.MonadMap(ma, RIOE.FromEither[C])
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -587,12 +587,12 @@ func Flap[R, C, E, B, A any](a A) Operator[R, C, E, func(A) B, B] {
|
||||
|
||||
//go:inline
|
||||
func MonadMapLeft[R, C, E1, E2, A any](fa ReaderReaderIOEither[R, C, E1, A], f func(E1) E2) ReaderReaderIOEither[R, C, E2, A] {
|
||||
return reader.MonadMap[R](fa, RIOE.MapLeft[C, A, E1, E2](f))
|
||||
return reader.MonadMap(fa, RIOE.MapLeft[C, A](f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MapLeft[R, C, A, E1, E2 any](f func(E1) E2) func(ReaderReaderIOEither[R, C, E1, A]) ReaderReaderIOEither[R, C, E2, A] {
|
||||
return reader.Map[R](RIOE.MapLeft[C, A, E1, E2](f))
|
||||
return reader.Map[R](RIOE.MapLeft[C, A](f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestMonadChain(t *testing.T) {
|
||||
func TestChainFirst(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[OuterConfig, InnerConfig, error](1),
|
||||
ChainFirst[OuterConfig, InnerConfig, error](func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
|
||||
ChainFirst(func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
|
||||
return Of[OuterConfig, InnerConfig, error](fmt.Sprintf("%d", v))
|
||||
}),
|
||||
)
|
||||
@@ -128,7 +128,7 @@ func TestTap(t *testing.T) {
|
||||
sideEffect := 0
|
||||
g := F.Pipe1(
|
||||
Of[OuterConfig, InnerConfig, error](1),
|
||||
Tap[OuterConfig, InnerConfig, error](func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
|
||||
Tap(func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
|
||||
sideEffect = v * 2
|
||||
return Of[OuterConfig, InnerConfig, error]("ignored")
|
||||
}),
|
||||
@@ -187,7 +187,7 @@ func TestMonadApPar(t *testing.T) {
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
t.Run("Right", func(t *testing.T) {
|
||||
result := FromEither[OuterConfig, InnerConfig, error](E.Right[error](42))
|
||||
result := FromEither[OuterConfig, InnerConfig](E.Right[error](42))
|
||||
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
|
||||
@@ -246,7 +246,7 @@ func TestFromIOEither(t *testing.T) {
|
||||
t.Run("Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
ioe := IOE.Left[int](err)
|
||||
result := FromIOEither[OuterConfig, InnerConfig, error, int](ioe)
|
||||
result := FromIOEither[OuterConfig, InnerConfig](ioe)
|
||||
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
}
|
||||
@@ -273,14 +273,14 @@ func TestLeftReaderIO(t *testing.T) {
|
||||
func TestFromReaderEither(t *testing.T) {
|
||||
t.Run("Right", func(t *testing.T) {
|
||||
re := RE.Right[OuterConfig, error](42)
|
||||
result := FromReaderEither[OuterConfig, InnerConfig, error](re)
|
||||
result := FromReaderEither[OuterConfig, InnerConfig](re)
|
||||
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
|
||||
t.Run("Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
re := RE.Left[OuterConfig, int](err)
|
||||
result := FromReaderEither[OuterConfig, InnerConfig, error, int](re)
|
||||
result := FromReaderEither[OuterConfig, InnerConfig](re)
|
||||
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
}
|
||||
@@ -288,14 +288,14 @@ func TestFromReaderEither(t *testing.T) {
|
||||
func TestFromReaderIOEither(t *testing.T) {
|
||||
t.Run("Right", func(t *testing.T) {
|
||||
rioe := RIOE.Right[OuterConfig, error](42)
|
||||
result := FromReaderIOEither[InnerConfig, error](rioe)
|
||||
result := FromReaderIOEither[InnerConfig](rioe)
|
||||
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
|
||||
t.Run("Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
rioe := RIOE.Left[OuterConfig, int](err)
|
||||
result := FromReaderIOEither[InnerConfig, error, OuterConfig, int](rioe)
|
||||
result := FromReaderIOEither[InnerConfig](rioe)
|
||||
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
}
|
||||
@@ -339,7 +339,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
onFalse := func(n int) error { return fmt.Errorf("not positive: %d", n) }
|
||||
|
||||
t.Run("Predicate true", func(t *testing.T) {
|
||||
result := FromPredicate[OuterConfig, InnerConfig, error](isPositive, onFalse)(5)
|
||||
result := FromPredicate[OuterConfig, InnerConfig](isPositive, onFalse)(5)
|
||||
assert.Equal(t, E.Right[error](5), result(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
|
||||
@@ -409,7 +409,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
func TestChainFirstEitherK(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[OuterConfig, InnerConfig, error](1),
|
||||
ChainFirstEitherK[OuterConfig, InnerConfig, error](func(v int) E.Either[error, string] {
|
||||
ChainFirstEitherK[OuterConfig, InnerConfig](func(v int) E.Either[error, string] {
|
||||
return E.Right[error](fmt.Sprintf("%d", v))
|
||||
}),
|
||||
)
|
||||
@@ -428,7 +428,7 @@ func TestTapEitherK(t *testing.T) {
|
||||
sideEffect := ""
|
||||
g := F.Pipe1(
|
||||
Of[OuterConfig, InnerConfig, error](1),
|
||||
TapEitherK[OuterConfig, InnerConfig, error](func(v int) E.Either[error, string] {
|
||||
TapEitherK[OuterConfig, InnerConfig](func(v int) E.Either[error, string] {
|
||||
sideEffect = fmt.Sprintf("%d", v)
|
||||
return E.Right[error](sideEffect)
|
||||
}),
|
||||
@@ -577,7 +577,7 @@ func TestMonadTapReaderIOK(t *testing.T) {
|
||||
func TestChainReaderEitherK(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[OuterConfig, InnerConfig, error](1),
|
||||
ChainReaderEitherK[InnerConfig, error](func(v int) RE.ReaderEither[OuterConfig, error, string] {
|
||||
ChainReaderEitherK[InnerConfig](func(v int) RE.ReaderEither[OuterConfig, error, string] {
|
||||
return RE.Right[OuterConfig, error](fmt.Sprintf("%d", v))
|
||||
}),
|
||||
)
|
||||
@@ -595,7 +595,7 @@ func TestMonadChainReaderEitherK(t *testing.T) {
|
||||
func TestChainFirstReaderEitherK(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[OuterConfig, InnerConfig, error](1),
|
||||
ChainFirstReaderEitherK[InnerConfig, error](func(v int) RE.ReaderEither[OuterConfig, error, string] {
|
||||
ChainFirstReaderEitherK[InnerConfig](func(v int) RE.ReaderEither[OuterConfig, error, string] {
|
||||
return RE.Right[OuterConfig, error](fmt.Sprintf("%d", v))
|
||||
}),
|
||||
)
|
||||
@@ -614,7 +614,7 @@ func TestTapReaderEitherK(t *testing.T) {
|
||||
sideEffect := ""
|
||||
g := F.Pipe1(
|
||||
Of[OuterConfig, InnerConfig, error](1),
|
||||
TapReaderEitherK[InnerConfig, error](func(v int) RE.ReaderEither[OuterConfig, error, string] {
|
||||
TapReaderEitherK[InnerConfig](func(v int) RE.ReaderEither[OuterConfig, error, string] {
|
||||
sideEffect = fmt.Sprintf("%d", v)
|
||||
return RE.Right[OuterConfig, error](sideEffect)
|
||||
}),
|
||||
@@ -709,7 +709,7 @@ func TestTapReaderOptionK(t *testing.T) {
|
||||
func TestChainIOEitherK(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[OuterConfig, InnerConfig, error](1),
|
||||
ChainIOEitherK[OuterConfig, InnerConfig, error](func(v int) IOE.IOEither[error, string] {
|
||||
ChainIOEitherK[OuterConfig, InnerConfig](func(v int) IOE.IOEither[error, string] {
|
||||
return IOE.Right[error](fmt.Sprintf("%d", v))
|
||||
}),
|
||||
)
|
||||
@@ -850,7 +850,7 @@ func TestAlt(t *testing.T) {
|
||||
}
|
||||
g := F.Pipe1(
|
||||
Right[OuterConfig, InnerConfig, error](42),
|
||||
Alt[OuterConfig, InnerConfig, error, int](second),
|
||||
Alt(second),
|
||||
)
|
||||
assert.Equal(t, E.Right[error](42), g(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
@@ -862,7 +862,7 @@ func TestAlt(t *testing.T) {
|
||||
}
|
||||
g := F.Pipe1(
|
||||
Left[OuterConfig, InnerConfig, int](err),
|
||||
Alt[OuterConfig, InnerConfig, error, int](second),
|
||||
Alt(second),
|
||||
)
|
||||
assert.Equal(t, E.Right[error](99), g(OuterConfig{})(InnerConfig{})())
|
||||
})
|
||||
@@ -939,7 +939,7 @@ func TestCompositionWithBothContexts(t *testing.T) {
|
||||
Map[OuterConfig, InnerConfig, error](func(cfg OuterConfig) string {
|
||||
return cfg.database
|
||||
}),
|
||||
Chain[OuterConfig, InnerConfig, error](func(db string) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
|
||||
Chain(func(db string) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
|
||||
return func(r OuterConfig) RIOE.ReaderIOEither[InnerConfig, error, string] {
|
||||
return func(c InnerConfig) IOE.IOEither[error, string] {
|
||||
return IOE.Right[error](fmt.Sprintf("%s:%s", db, c.apiKey))
|
||||
|
||||
240
v2/samples/builder/builder.go
Normal file
240
v2/samples/builder/builder.go
Normal file
@@ -0,0 +1,240 @@
|
||||
// Package builder demonstrates a functional builder pattern using fp-go.
|
||||
// It shows how to construct and validate Person objects using lenses, prisms,
|
||||
// and functional composition, separating the building phase (PartialPerson)
|
||||
// from the validated result (Person).
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
|
||||
var (
|
||||
// partialPersonLenses provides lens accessors for PartialPerson fields.
|
||||
// Generated by the fp-go:Lens directive in types.go.
|
||||
partialPersonLenses = MakePartialPersonRefLenses()
|
||||
|
||||
// personLenses provides lens accessors for Person fields.
|
||||
// Generated by the fp-go:Lens directive in types.go.
|
||||
personLenses = MakePersonRefLenses()
|
||||
|
||||
// emptyPartialPerson is a zero-value PartialPerson used as a starting point for building.
|
||||
emptyPartialPerson = &PartialPerson{}
|
||||
|
||||
// emptyPerson is a zero-value Person used as a starting point for validation.
|
||||
emptyPerson = &Person{}
|
||||
|
||||
// monoidPartialPerson is a monoid for composing endomorphisms on PartialPerson.
|
||||
// Allows combining multiple builder operations.
|
||||
monoidPartialPerson = endomorphism.Monoid[*PartialPerson]()
|
||||
|
||||
// monoidPerson is a monoid for composing endomorphisms on Person.
|
||||
monoidPerson = endomorphism.Monoid[*Person]()
|
||||
|
||||
// allOfPartialPerson combines multiple PartialPerson endomorphisms into one.
|
||||
allOfPartialPerson = monoid.ConcatAll(monoidPartialPerson)
|
||||
|
||||
// foldPartialPersons folds an array of PartialPerson operations into a single ReaderOption.
|
||||
foldPartialPersons = array.Fold(readeroption.ApplicativeMonoid[*PartialPerson](monoidPerson))
|
||||
|
||||
// foldPersons folds an array of Person operations into a single Reader.
|
||||
foldPersons = array.Fold(reader.ApplicativeMonoid[*Person](monoidPartialPerson))
|
||||
|
||||
// namePrism is a prism that validates and converts between string and NonEmptyString.
|
||||
// It ensures the name is not empty, returning None if validation fails.
|
||||
//
|
||||
// Forward direction: string -> Option[NonEmptyString] (validates non-empty)
|
||||
// Reverse direction: NonEmptyString -> string (always succeeds)
|
||||
namePrism = prism.MakePrismWithName(
|
||||
func(s string) Option[NonEmptyString] {
|
||||
if S.IsEmpty(s) {
|
||||
return option.None[NonEmptyString]()
|
||||
}
|
||||
return option.Of(NonEmptyString(s))
|
||||
},
|
||||
func(ns NonEmptyString) string {
|
||||
return string(ns)
|
||||
},
|
||||
"NonEmptyString",
|
||||
)
|
||||
|
||||
// agePrism is a prism that validates and converts between int and AdultAge.
|
||||
// It ensures the age is at least 18, returning None if validation fails.
|
||||
//
|
||||
// Forward direction: int -> Option[AdultAge] (validates >= 18)
|
||||
// Reverse direction: AdultAge -> int (always succeeds)
|
||||
agePrism = prism.MakePrismWithName(
|
||||
func(a int) Option[AdultAge] {
|
||||
if a < 18 {
|
||||
return option.None[AdultAge]()
|
||||
}
|
||||
return option.Of(AdultAge(a))
|
||||
},
|
||||
func(aa AdultAge) int {
|
||||
return int(aa)
|
||||
},
|
||||
"AdultAge",
|
||||
)
|
||||
|
||||
// WithName is a builder function that sets the Name field of a PartialPerson.
|
||||
// It returns an endomorphism that can be composed with other builder operations.
|
||||
//
|
||||
// Example:
|
||||
// builder := WithName("Alice")
|
||||
// person := builder(&PartialPerson{})
|
||||
WithName = partialPersonLenses.Name.Set
|
||||
|
||||
// WithAge is a builder function that sets the Age field of a PartialPerson.
|
||||
// It returns an endomorphism that can be composed with other builder operations.
|
||||
//
|
||||
// Example:
|
||||
// builder := WithAge(25)
|
||||
// person := builder(&PartialPerson{})
|
||||
WithAge = partialPersonLenses.Age.Set
|
||||
|
||||
// PersonPrism is a prism that converts between a builder pattern (Endomorphism[*PartialPerson])
|
||||
// and a validated Person in both directions.
|
||||
//
|
||||
// Forward direction (buildPerson): Endomorphism[*PartialPerson] -> Option[*Person]
|
||||
// - Applies the builder to an empty PartialPerson
|
||||
// - Validates all fields using namePrism and agePrism
|
||||
// - Returns Some(*Person) if all validations pass, None otherwise
|
||||
//
|
||||
// Reverse direction (buildEndomorphism): *Person -> Endomorphism[*PartialPerson]
|
||||
// - Extracts validated fields from Person
|
||||
// - Converts them back to raw types
|
||||
// - Returns a builder that reconstructs the PartialPerson
|
||||
//
|
||||
// This enables bidirectional conversion with validation in the forward direction.
|
||||
PersonPrism = prism.MakePrismWithName(buildPerson(), buildEndomorphism(), "Person")
|
||||
)
|
||||
|
||||
// MakePerson creates a builder (endomorphism) that sets both name and age fields
|
||||
// on a PartialPerson. This is a convenience function that combines WithName and
|
||||
// WithAge into a single builder operation.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The name to set (will be validated later when converting to Person)
|
||||
// - age: The age to set (will be validated later when converting to Person)
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An endomorphism that applies both field setters to a PartialPerson
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// builder := MakePerson("Alice", 25)
|
||||
// partial := builder(&PartialPerson{})
|
||||
// // partial now has Name="Alice" and Age=25
|
||||
func MakePerson(name string, age int) Endomorphism[*PartialPerson] {
|
||||
return F.Pipe1(
|
||||
A.From(
|
||||
WithName(name),
|
||||
WithAge(age),
|
||||
),
|
||||
allOfPartialPerson)
|
||||
}
|
||||
|
||||
func buildGeneric[S, A, T any](
|
||||
src Prism[Endomorphism[S], Endomorphism[A]],
|
||||
) Prism[Endomorphism[S], A] {
|
||||
var emptyA A
|
||||
|
||||
x := F.Pipe1(
|
||||
src.GetOption,
|
||||
readeroption.Map[Endomorphism[S]](reader.Read[A](emptyA)),
|
||||
)
|
||||
|
||||
y := F.Pipe1(
|
||||
src.ReverseGet,
|
||||
reader.Local[Endomorphism[S]](reader.Of[A, A]),
|
||||
)
|
||||
|
||||
return prism.MakePrism(
|
||||
x,
|
||||
y,
|
||||
)
|
||||
}
|
||||
|
||||
// buildPerson constructs the forward direction of PersonPrism.
|
||||
// It takes a builder (Endomorphism[*PartialPerson]) and attempts to create
|
||||
// a validated Person by:
|
||||
// 1. Applying the builder to an empty PartialPerson
|
||||
// 2. Extracting and validating the Name field using namePrism
|
||||
// 3. Extracting and validating the Age field using agePrism
|
||||
// 4. Combining the validated fields into a Person
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A ReaderOption that produces Some(*Person) if all validations pass,
|
||||
// or None if any validation fails.
|
||||
func buildPerson() ReaderOption[Endomorphism[*PartialPerson], *Person] {
|
||||
|
||||
// maybeName extracts the name from PartialPerson, validates it,
|
||||
// and creates a setter for the Person's Name field if valid
|
||||
maybeName := F.Flow3(
|
||||
partialPersonLenses.Name.Get,
|
||||
namePrism.GetOption,
|
||||
option.Map(personLenses.Name.Set),
|
||||
)
|
||||
|
||||
// maybeAge extracts the age from PartialPerson, validates it,
|
||||
// and creates a setter for the Person's Age field if valid
|
||||
maybeAge := F.Flow3(
|
||||
partialPersonLenses.Age.Get,
|
||||
agePrism.GetOption,
|
||||
option.Map(personLenses.Age.Set),
|
||||
)
|
||||
|
||||
// Combine the field validators and apply them to build a Person
|
||||
return F.Pipe2(
|
||||
A.From(maybeName, maybeAge),
|
||||
foldPartialPersons,
|
||||
readeroption.Promap(reader.Read[*PartialPerson](emptyPartialPerson), reader.Read[*Person](emptyPerson)),
|
||||
)
|
||||
}
|
||||
|
||||
// buildEndomorphism constructs the reverse direction of PersonPrism.
|
||||
// It takes a validated Person and creates a builder (Endomorphism[*PartialPerson])
|
||||
// that can reconstruct the equivalent PartialPerson by:
|
||||
// 1. Extracting the validated Name field and converting it back to string
|
||||
// 2. Extracting the validated Age field and converting it back to int
|
||||
// 3. Creating setters for the PartialPerson fields
|
||||
//
|
||||
// This reverse direction always succeeds because Person contains only valid data.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Reader that produces an endomorphism for reconstructing a PartialPerson
|
||||
func buildEndomorphism() Reader[*Person, Endomorphism[*PartialPerson]] {
|
||||
|
||||
// name extracts the validated name, converts it to string,
|
||||
// and creates a setter for PartialPerson's Name field
|
||||
name := F.Flow3(
|
||||
personLenses.Name.Get,
|
||||
namePrism.ReverseGet,
|
||||
partialPersonLenses.Name.Set,
|
||||
)
|
||||
|
||||
// age extracts the validated age, converts it to int,
|
||||
// and creates a setter for PartialPerson's Age field
|
||||
age := F.Flow3(
|
||||
personLenses.Age.Get,
|
||||
agePrism.ReverseGet,
|
||||
partialPersonLenses.Age.Set,
|
||||
)
|
||||
|
||||
// Combine the field extractors into a single builder
|
||||
return F.Pipe1(
|
||||
A.From(name, age),
|
||||
foldPersons,
|
||||
)
|
||||
}
|
||||
30
v2/samples/builder/builder_test.go
Normal file
30
v2/samples/builder/builder_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuilderPrism(t *testing.T) {
|
||||
b1 := MakePerson("Carsten", 55)
|
||||
|
||||
// this should be a valid person
|
||||
p1, ok := option.Unwrap(PersonPrism.GetOption(b1))
|
||||
assert.True(t, ok)
|
||||
|
||||
// convert back to a builder
|
||||
b2 := PersonPrism.ReverseGet(p1)
|
||||
|
||||
// change the name
|
||||
b3 := endomorphism.Chain(WithName("Jan"))(b1)
|
||||
|
||||
p2 := PersonPrism.GetOption(b2)
|
||||
p3 := PersonPrism.GetOption(b3)
|
||||
|
||||
assert.Equal(t, p2, option.Of(p1))
|
||||
assert.NotEqual(t, p3, option.Of(p1))
|
||||
|
||||
}
|
||||
223
v2/samples/builder/gen_lens.go
Normal file
223
v2/samples/builder/gen_lens.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package builder
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2026-01-21 17:40:56.5217758 +0100 CET m=+0.003738101
|
||||
|
||||
import (
|
||||
__lens "github.com/IBM/fp-go/v2/optics/lens"
|
||||
__option "github.com/IBM/fp-go/v2/option"
|
||||
__prism "github.com/IBM/fp-go/v2/optics/prism"
|
||||
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
|
||||
)
|
||||
|
||||
// PartialPersonLenses provides lenses for accessing fields of PartialPerson
|
||||
type PartialPersonLenses struct {
|
||||
// mandatory fields
|
||||
Name __lens.Lens[PartialPerson, string]
|
||||
Age __lens.Lens[PartialPerson, int]
|
||||
// optional fields
|
||||
NameO __lens_option.LensO[PartialPerson, string]
|
||||
AgeO __lens_option.LensO[PartialPerson, int]
|
||||
}
|
||||
|
||||
// PartialPersonRefLenses provides lenses for accessing fields of PartialPerson via a reference to PartialPerson
|
||||
type PartialPersonRefLenses struct {
|
||||
// mandatory fields
|
||||
Name __lens.Lens[*PartialPerson, string]
|
||||
Age __lens.Lens[*PartialPerson, int]
|
||||
// optional fields
|
||||
NameO __lens_option.LensO[*PartialPerson, string]
|
||||
AgeO __lens_option.LensO[*PartialPerson, int]
|
||||
// prisms
|
||||
NameP __prism.Prism[*PartialPerson, string]
|
||||
AgeP __prism.Prism[*PartialPerson, int]
|
||||
}
|
||||
|
||||
// PartialPersonPrisms provides prisms for accessing fields of PartialPerson
|
||||
type PartialPersonPrisms struct {
|
||||
Name __prism.Prism[PartialPerson, string]
|
||||
Age __prism.Prism[PartialPerson, int]
|
||||
}
|
||||
|
||||
// MakePartialPersonLenses creates a new PartialPersonLenses with lenses for all fields
|
||||
func MakePartialPersonLenses() PartialPersonLenses {
|
||||
// mandatory lenses
|
||||
lensName := __lens.MakeLensWithName(
|
||||
func(s PartialPerson) string { return s.Name },
|
||||
func(s PartialPerson, v string) PartialPerson { s.Name = v; return s },
|
||||
"PartialPerson.Name",
|
||||
)
|
||||
lensAge := __lens.MakeLensWithName(
|
||||
func(s PartialPerson) int { return s.Age },
|
||||
func(s PartialPerson, v int) PartialPerson { s.Age = v; return s },
|
||||
"PartialPerson.Age",
|
||||
)
|
||||
// optional lenses
|
||||
lensNameO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[string]())(lensName)
|
||||
lensAgeO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[int]())(lensAge)
|
||||
return PartialPersonLenses{
|
||||
// mandatory lenses
|
||||
Name: lensName,
|
||||
Age: lensAge,
|
||||
// optional lenses
|
||||
NameO: lensNameO,
|
||||
AgeO: lensAgeO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakePartialPersonRefLenses creates a new PartialPersonRefLenses with lenses for all fields
|
||||
func MakePartialPersonRefLenses() PartialPersonRefLenses {
|
||||
// mandatory lenses
|
||||
lensName := __lens.MakeLensStrictWithName(
|
||||
func(s *PartialPerson) string { return s.Name },
|
||||
func(s *PartialPerson, v string) *PartialPerson { s.Name = v; return s },
|
||||
"(*PartialPerson).Name",
|
||||
)
|
||||
lensAge := __lens.MakeLensStrictWithName(
|
||||
func(s *PartialPerson) int { return s.Age },
|
||||
func(s *PartialPerson, v int) *PartialPerson { s.Age = v; return s },
|
||||
"(*PartialPerson).Age",
|
||||
)
|
||||
// optional lenses
|
||||
lensNameO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[string]())(lensName)
|
||||
lensAgeO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[int]())(lensAge)
|
||||
return PartialPersonRefLenses{
|
||||
// mandatory lenses
|
||||
Name: lensName,
|
||||
Age: lensAge,
|
||||
// optional lenses
|
||||
NameO: lensNameO,
|
||||
AgeO: lensAgeO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakePartialPersonPrisms creates a new PartialPersonPrisms with prisms for all fields
|
||||
func MakePartialPersonPrisms() PartialPersonPrisms {
|
||||
_fromNonZeroName := __option.FromNonZero[string]()
|
||||
_prismName := __prism.MakePrismWithName(
|
||||
func(s PartialPerson) __option.Option[string] { return _fromNonZeroName(s.Name) },
|
||||
func(v string) PartialPerson {
|
||||
return PartialPerson{ Name: v }
|
||||
},
|
||||
"PartialPerson.Name",
|
||||
)
|
||||
_fromNonZeroAge := __option.FromNonZero[int]()
|
||||
_prismAge := __prism.MakePrismWithName(
|
||||
func(s PartialPerson) __option.Option[int] { return _fromNonZeroAge(s.Age) },
|
||||
func(v int) PartialPerson {
|
||||
return PartialPerson{ Age: v }
|
||||
},
|
||||
"PartialPerson.Age",
|
||||
)
|
||||
return PartialPersonPrisms {
|
||||
Name: _prismName,
|
||||
Age: _prismAge,
|
||||
}
|
||||
}
|
||||
|
||||
// PersonLenses provides lenses for accessing fields of Person
|
||||
type PersonLenses struct {
|
||||
// mandatory fields
|
||||
Name __lens.Lens[Person, NonEmptyString]
|
||||
Age __lens.Lens[Person, AdultAge]
|
||||
// optional fields
|
||||
NameO __lens_option.LensO[Person, NonEmptyString]
|
||||
AgeO __lens_option.LensO[Person, AdultAge]
|
||||
}
|
||||
|
||||
// PersonRefLenses provides lenses for accessing fields of Person via a reference to Person
|
||||
type PersonRefLenses struct {
|
||||
// mandatory fields
|
||||
Name __lens.Lens[*Person, NonEmptyString]
|
||||
Age __lens.Lens[*Person, AdultAge]
|
||||
// optional fields
|
||||
NameO __lens_option.LensO[*Person, NonEmptyString]
|
||||
AgeO __lens_option.LensO[*Person, AdultAge]
|
||||
// prisms
|
||||
NameP __prism.Prism[*Person, NonEmptyString]
|
||||
AgeP __prism.Prism[*Person, AdultAge]
|
||||
}
|
||||
|
||||
// PersonPrisms provides prisms for accessing fields of Person
|
||||
type PersonPrisms struct {
|
||||
Name __prism.Prism[Person, NonEmptyString]
|
||||
Age __prism.Prism[Person, AdultAge]
|
||||
}
|
||||
|
||||
// MakePersonLenses creates a new PersonLenses with lenses for all fields
|
||||
func MakePersonLenses() PersonLenses {
|
||||
// mandatory lenses
|
||||
lensName := __lens.MakeLensWithName(
|
||||
func(s Person) NonEmptyString { return s.Name },
|
||||
func(s Person, v NonEmptyString) Person { s.Name = v; return s },
|
||||
"Person.Name",
|
||||
)
|
||||
lensAge := __lens.MakeLensWithName(
|
||||
func(s Person) AdultAge { return s.Age },
|
||||
func(s Person, v AdultAge) Person { s.Age = v; return s },
|
||||
"Person.Age",
|
||||
)
|
||||
// optional lenses
|
||||
lensNameO := __lens_option.FromIso[Person](__iso_option.FromZero[NonEmptyString]())(lensName)
|
||||
lensAgeO := __lens_option.FromIso[Person](__iso_option.FromZero[AdultAge]())(lensAge)
|
||||
return PersonLenses{
|
||||
// mandatory lenses
|
||||
Name: lensName,
|
||||
Age: lensAge,
|
||||
// optional lenses
|
||||
NameO: lensNameO,
|
||||
AgeO: lensAgeO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakePersonRefLenses creates a new PersonRefLenses with lenses for all fields
|
||||
func MakePersonRefLenses() PersonRefLenses {
|
||||
// mandatory lenses
|
||||
lensName := __lens.MakeLensStrictWithName(
|
||||
func(s *Person) NonEmptyString { return s.Name },
|
||||
func(s *Person, v NonEmptyString) *Person { s.Name = v; return s },
|
||||
"(*Person).Name",
|
||||
)
|
||||
lensAge := __lens.MakeLensStrictWithName(
|
||||
func(s *Person) AdultAge { return s.Age },
|
||||
func(s *Person, v AdultAge) *Person { s.Age = v; return s },
|
||||
"(*Person).Age",
|
||||
)
|
||||
// optional lenses
|
||||
lensNameO := __lens_option.FromIso[*Person](__iso_option.FromZero[NonEmptyString]())(lensName)
|
||||
lensAgeO := __lens_option.FromIso[*Person](__iso_option.FromZero[AdultAge]())(lensAge)
|
||||
return PersonRefLenses{
|
||||
// mandatory lenses
|
||||
Name: lensName,
|
||||
Age: lensAge,
|
||||
// optional lenses
|
||||
NameO: lensNameO,
|
||||
AgeO: lensAgeO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakePersonPrisms creates a new PersonPrisms with prisms for all fields
|
||||
func MakePersonPrisms() PersonPrisms {
|
||||
_fromNonZeroName := __option.FromNonZero[NonEmptyString]()
|
||||
_prismName := __prism.MakePrismWithName(
|
||||
func(s Person) __option.Option[NonEmptyString] { return _fromNonZeroName(s.Name) },
|
||||
func(v NonEmptyString) Person {
|
||||
return Person{ Name: v }
|
||||
},
|
||||
"Person.Name",
|
||||
)
|
||||
_fromNonZeroAge := __option.FromNonZero[AdultAge]()
|
||||
_prismAge := __prism.MakePrismWithName(
|
||||
func(s Person) __option.Option[AdultAge] { return _fromNonZeroAge(s.Age) },
|
||||
func(v AdultAge) Person {
|
||||
return Person{ Age: v }
|
||||
},
|
||||
"Person.Age",
|
||||
)
|
||||
return PersonPrisms {
|
||||
Name: _prismName,
|
||||
Age: _prismAge,
|
||||
}
|
||||
}
|
||||
80
v2/samples/builder/types.go
Normal file
80
v2/samples/builder/types.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Package builder demonstrates the builder pattern using functional programming concepts
|
||||
// from fp-go, including validation and transformation of data structures.
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
//go:generate go run ../../main.go lens --dir . --filename gen_lens.go
|
||||
|
||||
type (
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
// It is an alias for endomorphism.Endomorphism[A].
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Result represents a computation that may succeed with a value of type A or fail with an error.
|
||||
// It is an alias for result.Result[A].
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Option represents an optional value of type A that may or may not be present.
|
||||
// It is an alias for option.Option[A].
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// ReaderOption represents a computation that depends on an environment R and produces
|
||||
// an optional value of type A. It is an alias for readeroption.ReaderOption[R, A].
|
||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces
|
||||
// a value of type A. It is an alias for reader.Reader[R, A].
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
// NonEmptyString is a string type that represents a validated non-empty string.
|
||||
// It is used to ensure that string fields contain meaningful data.
|
||||
NonEmptyString string
|
||||
|
||||
// AdultAge is an unsigned integer type that represents a validated age
|
||||
// that meets adult criteria (typically >= 18).
|
||||
AdultAge uint
|
||||
)
|
||||
|
||||
// PartialPerson represents a person record with unvalidated fields.
|
||||
// This type is typically used as an intermediate representation before
|
||||
// validation is applied to create a Person instance.
|
||||
//
|
||||
// The fp-go:Lens directive generates lens functions for accessing and
|
||||
// modifying the fields of this struct in a functional way.
|
||||
//
|
||||
// fp-go:Lens
|
||||
type PartialPerson struct {
|
||||
// Name is the person's name as a raw string, which may be empty or invalid.
|
||||
Name string
|
||||
|
||||
// Age is the person's age as a raw integer, which may be negative or otherwise invalid.
|
||||
Age int
|
||||
}
|
||||
|
||||
// Person represents a person record with validated fields.
|
||||
// All fields in this type have been validated and are guaranteed to meet
|
||||
// specific business rules (non-empty name, adult age).
|
||||
//
|
||||
// The fp-go:Lens directive generates lens functions for accessing and
|
||||
// modifying the fields of this struct in a functional way.
|
||||
//
|
||||
// fp-go:Lens
|
||||
type Person struct {
|
||||
// Name is the person's validated name, guaranteed to be non-empty.
|
||||
Name NonEmptyString
|
||||
|
||||
// Age is the person's validated age, guaranteed to meet adult criteria.
|
||||
Age AdultAge
|
||||
}
|
||||
Reference in New Issue
Block a user