1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-07 23:03:15 +02:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Carsten Leue
f652a94c3a fix: add template based logger
Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
2025-11-28 10:11:08 +01:00
Dr. Carsten Leue
774db88ca5 fix: add name to prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-27 13:26:36 +01:00
Dr. Carsten Leue
62a3365b20 fix: add conversion prisms for numbers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-27 13:12:18 +01:00
Dr. Carsten Leue
d9a16a6771 fix: add reduce operations to readerioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 17:00:10 +01:00
Dr. Carsten Leue
8949cc7dca fix: expose stats
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 13:44:40 +01:00
Dr. Carsten Leue
fa6b6caf22 fix: generic order for reader.Flap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:53:13 +01:00
Dr. Carsten Leue
a1e8d397c3 fix: better doc and some helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:06:09 +01:00
Dr. Carsten Leue
dbe7102e43 fix: better doc and some helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:05:31 +01:00
Dr. Carsten Leue
09aeb996e2 fix: add GetOrElseOf
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 18:57:30 +01:00
Dr. Carsten Leue
7cd575d95a fix: improve Prism and Optional
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 18:22:52 +01:00
90 changed files with 4906 additions and 358 deletions

View File

@@ -314,7 +314,7 @@ if err != nil {
```go
// Map transforms the success value
double := result.Map(func(x int) int { return x * 2 })
double := result.Map(N.Mul(2))
result := double(result.Right[error](21)) // Right(42)
// Chain sequences operations
@@ -330,7 +330,7 @@ validate := result.Chain(func(x int) result.Result[int] {
```go
// Map transforms the success value
double := result.Map(func(x int) int { return x * 2 })
double := result.Map(N.Mul(2))
value, err := double(21, nil) // (42, nil)
// Chain sequences operations

View File

@@ -205,7 +205,7 @@ The `Compose` function for endomorphisms now follows **mathematical function com
```go
// Compose executed left-to-right
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
increment := N.Add(1)
composed := Compose(double, increment)
result := composed(5) // (5 * 2) + 1 = 11
```
@@ -214,7 +214,7 @@ result := composed(5) // (5 * 2) + 1 = 11
```go
// Compose executes RIGHT-TO-LEFT (mathematical composition)
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
increment := N.Add(1)
composed := Compose(double, increment)
result := composed(5) // (5 + 1) * 2 = 12

View File

@@ -35,7 +35,7 @@ func TestReplicate(t *testing.T) {
func TestMonadMap(t *testing.T) {
src := []int{1, 2, 3}
result := MonadMap(src, func(x int) int { return x * 2 })
result := MonadMap(src, N.Mul(2))
assert.Equal(t, []int{2, 4, 6}, result)
}
@@ -173,8 +173,8 @@ func TestChain(t *testing.T) {
func TestMonadAp(t *testing.T) {
fns := []func(int) int{
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
N.Mul(2),
N.Add(10),
}
values := []int{1, 2}
result := MonadAp(fns, values)
@@ -268,7 +268,7 @@ func TestCopy(t *testing.T) {
func TestClone(t *testing.T) {
src := []int{1, 2, 3}
cloner := Clone(func(x int) int { return x * 2 })
cloner := Clone(N.Mul(2))
result := cloner(src)
assert.Equal(t, []int{2, 4, 6}, result)
}

View File

@@ -50,6 +50,7 @@ import (
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
@@ -106,6 +107,13 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
}
}
// StringNotEmpty checks if a string is not empty
func StringNotEmpty(s string) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, s)
}
}
// ArrayLength tests if an array has the expected length
func ArrayLength[T any](expected int) Kleisli[[]T] {
return func(actual []T) Reader {
@@ -266,3 +274,197 @@ func RunAll(testcases map[string]Reader) Reader {
return current
}
}
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
// by providing a function that converts R2 to R1. This allows you to focus a test on a
// specific property or subset of a larger data structure.
//
// This is particularly useful when you have an assertion that operates on a specific field
// or property, and you want to apply it to a complete object. Instead of extracting the
// property and then asserting on it, you can transform the assertion to work directly
// on the whole object.
//
// Parameters:
// - f: A function that extracts or transforms R2 into R1
//
// Returns:
// - A function that transforms a Reader[R1, Reader] into a Reader[R2, Reader]
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
//
// // Create an assertion that checks if age is positive
// ageIsPositive := assert.That(func(age int) bool { return age > 0 })
//
// // Focus this assertion on the Age field of User
// userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive)
//
// // Now we can test the whole User object
// user := User{Name: "Alice", Age: 30}
// userAgeIsPositive(user)(t)
//
//go:inline
func Local[R1, R2 any](f func(R2) R1) func(Kleisli[R1]) Kleisli[R2] {
return reader.Local[Reader](f)
}
// LocalL is similar to Local but uses a Lens to focus on a specific property.
// A Lens is a functional programming construct that provides a composable way to
// focus on a part of a data structure.
//
// This function is particularly useful when you want to focus a test on a specific
// field of a struct using a lens, making the code more declarative and composable.
// Lenses are often code-generated or predefined for common data structures.
//
// Parameters:
// - l: A Lens that focuses from type S to type T
//
// Returns:
// - A function that transforms a Reader[T, Reader] into a Reader[S, Reader]
//
// Example:
//
// type Person struct {
// Name string
// Email string
// }
//
// // Assume we have a lens that focuses on the Email field
// var emailLens = lens.Prop[Person, string]("Email")
//
// // Create an assertion for email format
// validEmail := assert.That(func(email string) bool {
// return strings.Contains(email, "@")
// })
//
// // Focus this assertion on the Email property using a lens
// validPersonEmail := assert.LocalL(emailLens)(validEmail)
//
// // Test a Person object
// person := Person{Name: "Bob", Email: "bob@example.com"}
// validPersonEmail(person)(t)
//
//go:inline
func LocalL[S, T any](l Lens[S, T]) func(Kleisli[T]) Kleisli[S] {
return reader.Local[Reader](l.Get)
}
// fromOptionalGetter is an internal helper that creates an assertion Reader from
// an optional getter function. It asserts that the optional value is present (Some).
func fromOptionalGetter[S, T any](getter func(S) option.Option[T], msgAndArgs ...any) Kleisli[S] {
return func(s S) Reader {
return func(t *testing.T) bool {
return assert.True(t, option.IsSome(getter(s)), msgAndArgs...)
}
}
}
// FromOptional creates an assertion that checks if an Optional can successfully extract a value.
// An Optional is an optic that represents an optional reference to a subpart of a data structure.
//
// This function is useful when you have an Optional optic and want to assert that the optional
// value is present (Some) rather than absent (None). The assertion passes if the Optional's
// GetOption returns Some, and fails if it returns None.
//
// This enables property-focused testing where you verify that a particular optional field or
// sub-structure exists and is accessible.
//
// Parameters:
// - opt: An Optional optic that focuses from type S to type T
//
// Returns:
// - A Reader that asserts the optional value is present when applied to a value of type S
//
// Example:
//
// type Config struct {
// Database *DatabaseConfig // Optional field
// }
//
// type DatabaseConfig struct {
// Host string
// Port int
// }
//
// // Create an Optional that focuses on the Database field
// dbOptional := optional.MakeOptional(
// func(c Config) option.Option[*DatabaseConfig] {
// if c.Database != nil {
// return option.Some(c.Database)
// }
// return option.None[*DatabaseConfig]()
// },
// func(c Config, db *DatabaseConfig) Config {
// c.Database = db
// return c
// },
// )
//
// // Assert that the database config is present
// hasDatabaseConfig := assert.FromOptional(dbOptional)
//
// config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
// hasDatabaseConfig(config)(t) // Passes
//
// emptyConfig := Config{Database: nil}
// hasDatabaseConfig(emptyConfig)(t) // Fails
//
//go:inline
func FromOptional[S, T any](opt Optional[S, T]) reader.Reader[S, Reader] {
return fromOptionalGetter(opt.GetOption, "Optional: %s", opt)
}
// FromPrism creates an assertion that checks if a Prism can successfully extract a value.
// A Prism is an optic used to select part of a sum type (tagged union or variant).
//
// This function is useful when you have a Prism optic and want to assert that a value
// matches a specific variant of a sum type. The assertion passes if the Prism's GetOption
// returns Some (meaning the value is of the expected variant), and fails if it returns None
// (meaning the value is a different variant).
//
// This enables variant-focused testing where you verify that a value is of a particular
// type or matches a specific condition within a sum type.
//
// Parameters:
// - p: A Prism optic that focuses from type S to type T
//
// Returns:
// - A Reader that asserts the prism successfully extracts when applied to a value of type S
//
// Example:
//
// type Result interface{ isResult() }
// type Success struct{ Value int }
// type Failure struct{ Error string }
//
// func (Success) isResult() {}
// func (Failure) isResult() {}
//
// // Create a Prism that focuses on Success variant
// successPrism := prism.MakePrism(
// func(r Result) option.Option[int] {
// if s, ok := r.(Success); ok {
// return option.Some(s.Value)
// }
// return option.None[int]()
// },
// func(v int) Result { return Success{Value: v} },
// )
//
// // Assert that the result is a Success
// isSuccess := assert.FromPrism(successPrism)
//
// result1 := Success{Value: 42}
// isSuccess(result1)(t) // Passes
//
// result2 := Failure{Error: "something went wrong"}
// isSuccess(result2)(t) // Fails
//
//go:inline
func FromPrism[S, T any](p Prism[S, T]) reader.Reader[S, Reader] {
return fromOptionalGetter(p.GetOption, "Prism: %s", p)
}

View File

@@ -19,6 +19,8 @@ import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
@@ -212,7 +214,7 @@ func TestError(t *testing.T) {
func TestSuccess(t *testing.T) {
t.Run("should pass for successful result", func(t *testing.T) {
res := result.Of[int](42)
res := result.Of(42)
result := Success(res)(t)
if !result {
t.Error("Expected Success to pass for successful result")
@@ -240,7 +242,7 @@ func TestFailure(t *testing.T) {
t.Run("should fail for successful result", func(t *testing.T) {
mockT := &testing.T{}
res := result.Of[int](42)
res := result.Of(42)
result := Failure(res)(mockT)
if result {
t.Error("Expected Failure to fail for successful result")
@@ -445,41 +447,245 @@ func TestEq(t *testing.T) {
})
}
func TestIntegration(t *testing.T) {
t.Run("complex assertion composition", func(t *testing.T) {
type User struct {
Name string
Age int
Email string
func TestLocal(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("should focus assertion on a property", func(t *testing.T) {
// Create an assertion that checks if age is positive
ageIsPositive := That(func(age int) bool { return age > 0 })
// Focus this assertion on the Age field of User
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
// Test with a user who has a positive age
user := User{Name: "Alice", Age: 30}
result := userAgeIsPositive(user)(t)
if !result {
t.Error("Expected focused assertion to pass for positive age")
}
})
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
t.Run("should fail when focused property doesn't match", func(t *testing.T) {
mockT := &testing.T{}
ageIsPositive := That(func(age int) bool { return age > 0 })
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
// Test with a user who has zero age
user := User{Name: "Bob", Age: 0}
result := userAgeIsPositive(user)(mockT)
if result {
t.Error("Expected focused assertion to fail for zero age")
}
})
t.Run("should compose with other assertions", func(t *testing.T) {
// Create multiple focused assertions
nameNotEmpty := Local(func(u User) string { return u.Name })(
That(func(name string) bool { return len(name) > 0 }),
)
ageInRange := Local(func(u User) int { return u.Age })(
That(func(age int) bool { return age >= 18 && age <= 100 }),
)
user := User{Name: "Charlie", Age: 25}
assertions := AllOf([]Reader{
Equal("Alice")(user.Name),
Equal(30)(user.Age),
That(func(s string) bool { return len(s) > 0 })(user.Email),
nameNotEmpty(user),
ageInRange(user),
})
result := assertions(t)
if !result {
t.Error("Expected complex assertion composition to pass")
t.Error("Expected composed focused assertions to pass")
}
})
t.Run("test suite with RunAll", func(t *testing.T) {
data := []int{1, 2, 3, 4, 5}
t.Run("should work with Equal assertion", func(t *testing.T) {
// Focus Equal assertion on Name field
nameIsAlice := Local(func(u User) string { return u.Name })(Equal("Alice"))
suite := RunAll(map[string]Reader{
"not_empty": ArrayNotEmpty(data),
"correct_size": ArrayLength[int](5)(data),
"contains_one": ArrayContains(1)(data),
"contains_five": ArrayContains(5)(data),
})
result := suite(t)
user := User{Name: "Alice", Age: 30}
result := nameIsAlice(user)(t)
if !result {
t.Error("Expected test suite to pass")
t.Error("Expected focused Equal assertion to pass")
}
})
}
func TestLocalL(t *testing.T) {
// Note: LocalL requires lens package which provides lens operations.
// This test demonstrates the concept, but actual usage would require
// proper lens definitions.
t.Run("conceptual test for LocalL", func(t *testing.T) {
// LocalL is similar to Local but uses lenses for focusing.
// It would be used like:
// validEmail := That(func(email string) bool { return strings.Contains(email, "@") })
// validPersonEmail := LocalL(emailLens)(validEmail)
//
// The actual implementation would require lens definitions from the lens package.
// This test serves as documentation for the intended usage.
})
}
func TestFromOptional(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
}
type Config struct {
Database *DatabaseConfig
}
// Create an Optional that focuses on the Database field
dbOptional := Optional[Config, *DatabaseConfig]{
GetOption: func(c Config) option.Option[*DatabaseConfig] {
if c.Database != nil {
return option.Of(c.Database)
}
return option.None[*DatabaseConfig]()
},
Set: func(db *DatabaseConfig) func(Config) Config {
return func(c Config) Config {
c.Database = db
return c
}
},
}
t.Run("should pass when optional value is present", func(t *testing.T) {
config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
hasDatabaseConfig := FromOptional(dbOptional)
result := hasDatabaseConfig(config)(t)
if !result {
t.Error("Expected FromOptional to pass when optional value is present")
}
})
t.Run("should fail when optional value is absent", func(t *testing.T) {
mockT := &testing.T{}
emptyConfig := Config{Database: nil}
hasDatabaseConfig := FromOptional(dbOptional)
result := hasDatabaseConfig(emptyConfig)(mockT)
if result {
t.Error("Expected FromOptional to fail when optional value is absent")
}
})
t.Run("should work with nested optionals", func(t *testing.T) {
type AdvancedSettings struct {
Cache bool
}
type Settings struct {
Advanced *AdvancedSettings
}
advancedOptional := Optional[Settings, *AdvancedSettings]{
GetOption: func(s Settings) option.Option[*AdvancedSettings] {
if s.Advanced != nil {
return option.Of(s.Advanced)
}
return option.None[*AdvancedSettings]()
},
Set: func(adv *AdvancedSettings) func(Settings) Settings {
return func(s Settings) Settings {
s.Advanced = adv
return s
}
},
}
settings := Settings{Advanced: &AdvancedSettings{Cache: true}}
hasAdvanced := FromOptional(advancedOptional)
result := hasAdvanced(settings)(t)
if !result {
t.Error("Expected FromOptional to pass for nested optional")
}
})
}
// Helper types for Prism testing
type PrismTestResult interface {
isPrismTestResult()
}
type PrismTestSuccess struct {
Value int
}
type PrismTestFailure struct {
Error string
}
func (PrismTestSuccess) isPrismTestResult() {}
func (PrismTestFailure) isPrismTestResult() {}
func TestFromPrism(t *testing.T) {
// Create a Prism that focuses on Success variant using prism.MakePrism
successPrism := prism.MakePrism(
func(r PrismTestResult) option.Option[int] {
if s, ok := r.(PrismTestSuccess); ok {
return option.Of(s.Value)
}
return option.None[int]()
},
func(v int) PrismTestResult {
return PrismTestSuccess{Value: v}
},
)
// Create a Prism that focuses on Failure variant
failurePrism := prism.MakePrism(
func(r PrismTestResult) option.Option[string] {
if f, ok := r.(PrismTestFailure); ok {
return option.Of(f.Error)
}
return option.None[string]()
},
func(err string) PrismTestResult {
return PrismTestFailure{Error: err}
},
)
t.Run("should pass when prism successfully extracts", func(t *testing.T) {
result := PrismTestSuccess{Value: 42}
isSuccess := FromPrism(successPrism)
testResult := isSuccess(result)(t)
if !testResult {
t.Error("Expected FromPrism to pass when prism extracts successfully")
}
})
t.Run("should fail when prism cannot extract", func(t *testing.T) {
mockT := &testing.T{}
result := PrismTestFailure{Error: "something went wrong"}
isSuccess := FromPrism(successPrism)
testResult := isSuccess(result)(mockT)
if testResult {
t.Error("Expected FromPrism to fail when prism cannot extract")
}
})
t.Run("should work with failure prism", func(t *testing.T) {
result := PrismTestFailure{Error: "test error"}
isFailure := FromPrism(failurePrism)
testResult := isFailure(result)(t)
if !testResult {
t.Error("Expected FromPrism to pass for failure prism on failure result")
}
})
t.Run("should fail with failure prism on success result", func(t *testing.T) {
mockT := &testing.T{}
result := PrismTestSuccess{Value: 100}
isFailure := FromPrism(failurePrism)
testResult := isFailure(result)(mockT)
if testResult {
t.Error("Expected FromPrism to fail for failure prism on success result")
}
})
}

View File

@@ -3,14 +3,20 @@ package assert
import (
"testing"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
Result[T any] = result.Result[T]
Reader = reader.Reader[*testing.T, bool]
Kleisli[T any] = reader.Reader[T, Reader]
Predicate[T any] = predicate.Predicate[T]
Result[T any] = result.Result[T]
Reader = reader.Reader[*testing.T, bool]
Kleisli[T any] = reader.Reader[T, Reader]
Predicate[T any] = predicate.Predicate[T]
Lens[S, T any] = lens.Lens[S, T]
Optional[S, T any] = optional.Optional[S, T]
Prism[S, T any] = prism.Prism[S, T]
)

View File

@@ -8,5 +8,5 @@ import (
// BuilderPrism createa a [Prism] that converts between a builder and its type
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
return prism.MakePrism(F.Flow2(B.Build, result.ToOption[T]), creator)
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
}

View File

@@ -27,9 +27,9 @@ import (
// resourceState tracks the lifecycle of resources for testing
type resourceState struct {
resourcesCreated int
resourcesCreated int
resourcesReleased int
lastError error
lastError error
}
// mockResource represents a test resource

View File

@@ -68,7 +68,7 @@ func Of[S, A any](a A) StateReaderIOResult[S, A] {
//
// result := statereaderioresult.MonadMap(
// statereaderioresult.Of[AppState](21),
// func(x int) int { return x * 2 },
// N.Mul(2),
// ) // Result contains 42
func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReaderIOResult[S, B] {
return statet.MonadMap[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
@@ -83,7 +83,7 @@ func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReade
//
// Example:
//
// double := statereaderioresult.Map[AppState](func(x int) int { return x * 2 })
// double := statereaderioresult.Map[AppState](N.Mul(2))
// result := function.Pipe1(statereaderioresult.Of[AppState](21), double)
func Map[S, A, B any](f func(A) B) Operator[S, A, B] {
return statet.Map[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
@@ -135,7 +135,7 @@ func Chain[S, A, B any](f Kleisli[S, A, B]) Operator[S, A, B] {
//
// Example:
//
// fab := statereaderioresult.Of[AppState](func(x int) int { return x * 2 })
// fab := statereaderioresult.Of[AppState](N.Mul(2))
// fa := statereaderioresult.Of[AppState](21)
// result := statereaderioresult.MonadAp(fab, fa) // Result contains 42
func MonadAp[B, S, A any](fab StateReaderIOResult[S, func(A) B], fa StateReaderIOResult[S, A]) StateReaderIOResult[S, B] {

View File

@@ -215,7 +215,7 @@ func TestFromState(t *testing.T) {
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 11, P.Tail(p)) // Incremented value
assert.Equal(t, 11, P.Tail(p)) // Incremented value
assert.Equal(t, 11, P.Head(p).counter) // State updated
return p
})(res)
@@ -473,7 +473,7 @@ func TestStatefulComputation(t *testing.T) {
res := result(initialState)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
assert.Equal(t, 3, P.Head(p).counter) // State updated three times
return p
})(res)

View File

@@ -19,12 +19,12 @@ import (
"context"
"testing"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
ST "github.com/IBM/fp-go/v2/context/statereaderioresult"
EQ "github.com/IBM/fp-go/v2/eq"
L "github.com/IBM/fp-go/v2/internal/monad/testing"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
ST "github.com/IBM/fp-go/v2/context/statereaderioresult"
)
// AssertLaws asserts the monad laws for the StateReaderIOResult monad

View File

@@ -95,11 +95,11 @@ func (o *eitherMonad[E, A, B]) Chain(f Kleisli[E, A, B]) Operator[E, A, B] {
// m := either.Monad[error, int, int]()
//
// // Map transforms the value
// value := m.Map(func(x int) int { return x * 2 })(either.Right[error](21))
// value := m.Map(N.Mul(2))(either.Right[error](21))
// // value is Right(42)
//
// // Ap applies wrapped functions (also fails fast)
// fn := either.Right[error](func(x int) int { return x + 1 })
// fn := either.Right[error](N.Add(1))
// result := m.Ap(value)(fn)
// // result is Right(43)
//

406
v2/endomorphism/builder.go Normal file
View File

@@ -0,0 +1,406 @@
// 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 endomorphism
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/array"
)
// Build applies an endomorphism to the zero value of type A, effectively using
// the endomorphism as a builder pattern.
//
// # Endomorphism as Builder Pattern
//
// An endomorphism (a function from type A to type A) can be viewed as a builder pattern
// because it transforms a value of a type into another value of the same type. When you
// compose multiple endomorphisms together, you create a pipeline of transformations that
// build up a final value step by step.
//
// The Build function starts with the zero value of type A and applies the endomorphism
// to it, making it particularly useful for building complex values from scratch using
// a functional composition of transformations.
//
// # Builder Pattern Characteristics
//
// Traditional builder patterns have these characteristics:
// 1. Start with an initial (often empty) state
// 2. Apply a series of transformations/configurations
// 3. Return the final built object
//
// Endomorphisms provide the same pattern functionally:
// 1. Start with zero value: var a A
// 2. Apply composed endomorphisms: e(a)
// 3. Return the transformed value
//
// # Type Parameters
//
// - A: The type being built/transformed
//
// # Parameters
//
// - e: An endomorphism (or composition of endomorphisms) that transforms type A
//
// # Returns
//
// The result of applying the endomorphism to the zero value of type A
//
// # Example - Building a Configuration Object
//
// type Config struct {
// Host string
// Port int
// Timeout time.Duration
// Debug bool
// }
//
// // Define builder functions as endomorphisms
// withHost := func(host string) Endomorphism[Config] {
// return func(c Config) Config {
// c.Host = host
// return c
// }
// }
//
// withPort := func(port int) Endomorphism[Config] {
// return func(c Config) Config {
// c.Port = port
// return c
// }
// }
//
// withTimeout := func(d time.Duration) Endomorphism[Config] {
// return func(c Config) Config {
// c.Timeout = d
// return c
// }
// }
//
// withDebug := func(debug bool) Endomorphism[Config] {
// return func(c Config) Config {
// c.Debug = debug
// return c
// }
// }
//
// // Compose builders using monoid operations
// import M "github.com/IBM/fp-go/v2/monoid"
//
// configBuilder := M.ConcatAll(Monoid[Config]())(
// withHost("localhost"),
// withPort(8080),
// withTimeout(30 * time.Second),
// withDebug(true),
// )
//
// // Build the final configuration
// config := Build(configBuilder)
// // Result: Config{Host: "localhost", Port: 8080, Timeout: 30s, Debug: true}
//
// # Example - Building a String with Transformations
//
// import (
// "strings"
// M "github.com/IBM/fp-go/v2/monoid"
// )
//
// // Define string transformation endomorphisms
// appendHello := func(s string) string { return s + "Hello" }
// appendSpace := func(s string) string { return s + " " }
// appendWorld := func(s string) string { return s + "World" }
// toUpper := strings.ToUpper
//
// // Compose transformations
// stringBuilder := M.ConcatAll(Monoid[string]())(
// appendHello,
// appendSpace,
// appendWorld,
// toUpper,
// )
//
// // Build the final string from empty string
// result := Build(stringBuilder)
// // Result: "HELLO WORLD"
//
// # Example - Building a Slice with Operations
//
// type IntSlice []int
//
// appendValue := func(v int) Endomorphism[IntSlice] {
// return func(s IntSlice) IntSlice {
// return append(s, v)
// }
// }
//
// sortSlice := func(s IntSlice) IntSlice {
// sorted := make(IntSlice, len(s))
// copy(sorted, s)
// sort.Ints(sorted)
// return sorted
// }
//
// // Build a sorted slice
// sliceBuilder := M.ConcatAll(Monoid[IntSlice]())(
// appendValue(5),
// appendValue(2),
// appendValue(8),
// appendValue(1),
// sortSlice,
// )
//
// result := Build(sliceBuilder)
// // Result: IntSlice{1, 2, 5, 8}
//
// # Advantages of Endomorphism Builder Pattern
//
// 1. **Composability**: Builders can be composed using monoid operations
// 2. **Immutability**: Each transformation returns a new value (if implemented immutably)
// 3. **Type Safety**: The type system ensures all transformations work on the same type
// 4. **Reusability**: Individual builder functions can be reused and combined differently
// 5. **Testability**: Each transformation can be tested independently
// 6. **Declarative**: The composition clearly expresses the building process
//
// # Comparison with Traditional Builder Pattern
//
// Traditional OOP Builder:
//
// config := NewConfigBuilder().
// WithHost("localhost").
// WithPort(8080).
// WithTimeout(30 * time.Second).
// Build()
//
// Endomorphism Builder:
//
// config := Build(M.ConcatAll(Monoid[Config]())(
// withHost("localhost"),
// withPort(8080),
// withTimeout(30 * time.Second),
// ))
//
// Both achieve the same goal, but the endomorphism approach:
// - Uses pure functions instead of methods
// - Leverages algebraic properties (monoid) for composition
// - Allows for more flexible composition patterns
// - Integrates naturally with other functional programming constructs
func Build[A any](e Endomorphism[A]) A {
var a A
return e(a)
}
// ConcatAll combines multiple endomorphisms into a single endomorphism using composition.
//
// This function takes a slice of endomorphisms and combines them using the monoid's
// concat operation (which is composition). The resulting endomorphism, when applied,
// will execute all the input endomorphisms in RIGHT-TO-LEFT order (mathematical composition order).
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - ConcatAll([]Endomorphism{f, g, h}) creates an endomorphism that applies h, then g, then f
// - This is equivalent to f ∘ g ∘ h in mathematical notation
// - The last endomorphism in the slice is applied first
//
// If the slice is empty, returns the identity endomorphism.
//
// # Type Parameters
//
// - T: The type that the endomorphisms operate on
//
// # Parameters
//
// - es: A slice of endomorphisms to combine
//
// # Returns
//
// A single endomorphism that represents the composition of all input endomorphisms
//
// # Example - Basic Composition
//
// double := N.Mul(2)
// increment := N.Add(1)
// square := func(x int) int { return x * x }
//
// // Combine endomorphisms (RIGHT-TO-LEFT execution)
// combined := ConcatAll([]Endomorphism[int]{double, increment, square})
// result := combined(5)
// // Execution: square(5) = 25, increment(25) = 26, double(26) = 52
// // Result: 52
//
// # Example - Building with ConcatAll
//
// type Config struct {
// Host string
// Port int
// }
//
// withHost := func(host string) Endomorphism[Config] {
// return func(c Config) Config {
// c.Host = host
// return c
// }
// }
//
// withPort := func(port int) Endomorphism[Config] {
// return func(c Config) Config {
// c.Port = port
// return c
// }
// }
//
// // Combine configuration builders
// configBuilder := ConcatAll([]Endomorphism[Config]{
// withHost("localhost"),
// withPort(8080),
// })
//
// // Apply to zero value
// config := Build(configBuilder)
// // Result: Config{Host: "localhost", Port: 8080}
//
// # Example - Empty Slice
//
// // Empty slice returns identity
// identity := ConcatAll([]Endomorphism[int]{})
// result := identity(42) // Returns: 42
//
// # Relationship to Monoid
//
// ConcatAll is equivalent to using M.ConcatAll with the endomorphism Monoid:
//
// import M "github.com/IBM/fp-go/v2/monoid"
//
// // These are equivalent:
// result1 := ConcatAll(endomorphisms)
// result2 := M.ConcatAll(Monoid[T]())(endomorphisms)
//
// # Use Cases
//
// 1. **Pipeline Construction**: Build transformation pipelines from individual steps
// 2. **Configuration Building**: Combine multiple configuration setters
// 3. **Data Transformation**: Chain multiple data transformations
// 4. **Middleware Composition**: Combine middleware functions
// 5. **Validation Chains**: Compose multiple validation functions
func ConcatAll[T any](es []Endomorphism[T]) Endomorphism[T] {
return A.Reduce(es, MonadCompose[T], function.Identity[T])
}
// Reduce applies a slice of endomorphisms to the zero value of type T in LEFT-TO-RIGHT order.
//
// This function is a convenience wrapper that:
// 1. Starts with the zero value of type T
// 2. Applies each endomorphism in the slice from left to right
// 3. Returns the final transformed value
//
// IMPORTANT: Execution order is LEFT-TO-RIGHT:
// - Reduce([]Endomorphism{f, g, h}) applies f first, then g, then h
// - This is the opposite of ConcatAll's RIGHT-TO-LEFT order
// - Each endomorphism receives the result of the previous one
//
// This is equivalent to: Build(ConcatAll(reverse(es))) but more efficient and clearer
// for left-to-right sequential application.
//
// # Type Parameters
//
// - T: The type being transformed
//
// # Parameters
//
// - es: A slice of endomorphisms to apply sequentially
//
// # Returns
//
// The final value after applying all endomorphisms to the zero value
//
// # Example - Sequential Transformations
//
// double := N.Mul(2)
// increment := N.Add(1)
// square := func(x int) int { return x * x }
//
// // Apply transformations LEFT-TO-RIGHT
// result := Reduce([]Endomorphism[int]{double, increment, square})
// // Execution: 0 -> double(0) = 0 -> increment(0) = 1 -> square(1) = 1
// // Result: 1
//
// // With a non-zero starting point, use a custom initial value:
// addTen := N.Add(10)
// result2 := Reduce([]Endomorphism[int]{addTen, double, increment})
// // Execution: 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
// // Result: 21
//
// # Example - Building a String
//
// appendHello := func(s string) string { return s + "Hello" }
// appendSpace := func(s string) string { return s + " " }
// appendWorld := func(s string) string { return s + "World" }
//
// // Build string LEFT-TO-RIGHT
// result := Reduce([]Endomorphism[string]{
// appendHello,
// appendSpace,
// appendWorld,
// })
// // Execution: "" -> "Hello" -> "Hello " -> "Hello World"
// // Result: "Hello World"
//
// # Example - Configuration Building
//
// type Settings struct {
// Theme string
// FontSize int
// }
//
// withTheme := func(theme string) Endomorphism[Settings] {
// return func(s Settings) Settings {
// s.Theme = theme
// return s
// }
// }
//
// withFontSize := func(size int) Endomorphism[Settings] {
// return func(s Settings) Settings {
// s.FontSize = size
// return s
// }
// }
//
// // Build settings LEFT-TO-RIGHT
// settings := Reduce([]Endomorphism[Settings]{
// withTheme("dark"),
// withFontSize(14),
// })
// // Result: Settings{Theme: "dark", FontSize: 14}
//
// # Comparison with ConcatAll
//
// // ConcatAll: RIGHT-TO-LEFT composition, returns endomorphism
// endo := ConcatAll([]Endomorphism[int]{f, g, h})
// result1 := endo(value) // Applies h, then g, then f
//
// // Reduce: LEFT-TO-RIGHT application, returns final value
// result2 := Reduce([]Endomorphism[int]{f, g, h})
// // Applies f to zero, then g, then h
//
// # Use Cases
//
// 1. **Sequential Processing**: Apply transformations in order
// 2. **Pipeline Execution**: Execute a pipeline from start to finish
// 3. **Builder Pattern**: Build objects step by step
// 4. **State Machines**: Apply state transitions in sequence
// 5. **Data Flow**: Transform data through multiple stages
func Reduce[T any](es []Endomorphism[T]) T {
var t T
return A.Reduce(es, func(t T, e Endomorphism[T]) T { return e(t) }, t)
}

View File

@@ -0,0 +1,254 @@
// 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 endomorphism_test
import (
"fmt"
"time"
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/endomorphism"
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
)
// Example_build_basicUsage demonstrates basic usage of the Build function
// to construct a value from the zero value using endomorphisms.
func Example_build_basicUsage() {
// Define simple endomorphisms
addTen := N.Add(10)
double := N.Mul(2)
// Compose them using monoid (RIGHT-TO-LEFT execution)
// double is applied first, then addTen
builder := M.ConcatAll(endomorphism.Monoid[int]())(A.From(
addTen,
double,
))
// Build from zero value: 0 * 2 = 0, 0 + 10 = 10
result := endomorphism.Build(builder)
fmt.Println(result)
// Output: 10
}
// Example_build_configBuilder demonstrates using Build as a configuration builder pattern.
func Example_build_configBuilder() {
type Config struct {
Host string
Port int
Timeout time.Duration
Debug bool
}
// Define builder functions as endomorphisms
withHost := func(host string) endomorphism.Endomorphism[Config] {
return func(c Config) Config {
c.Host = host
return c
}
}
withPort := func(port int) endomorphism.Endomorphism[Config] {
return func(c Config) Config {
c.Port = port
return c
}
}
withTimeout := func(d time.Duration) endomorphism.Endomorphism[Config] {
return func(c Config) Config {
c.Timeout = d
return c
}
}
withDebug := func(debug bool) endomorphism.Endomorphism[Config] {
return func(c Config) Config {
c.Debug = debug
return c
}
}
// Compose builders using monoid
configBuilder := M.ConcatAll(endomorphism.Monoid[Config]())([]endomorphism.Endomorphism[Config]{
withHost("localhost"),
withPort(8080),
withTimeout(30 * time.Second),
withDebug(true),
})
// Build the configuration from zero value
config := endomorphism.Build(configBuilder)
fmt.Printf("Host: %s\n", config.Host)
fmt.Printf("Port: %d\n", config.Port)
fmt.Printf("Timeout: %v\n", config.Timeout)
fmt.Printf("Debug: %v\n", config.Debug)
// Output:
// Host: localhost
// Port: 8080
// Timeout: 30s
// Debug: true
}
// Example_build_stringBuilder demonstrates building a string using endomorphisms.
func Example_build_stringBuilder() {
// Define string transformation endomorphisms
appendHello := func(s string) string { return s + "Hello" }
appendSpace := func(s string) string { return s + " " }
appendWorld := func(s string) string { return s + "World" }
appendExclamation := func(s string) string { return s + "!" }
// Compose transformations (RIGHT-TO-LEFT execution)
stringBuilder := M.ConcatAll(endomorphism.Monoid[string]())([]endomorphism.Endomorphism[string]{
appendHello,
appendSpace,
appendWorld,
appendExclamation,
})
// Build the string from empty string
result := endomorphism.Build(stringBuilder)
fmt.Println(result)
// Output: !World Hello
}
// Example_build_personBuilder demonstrates building a complex struct using the builder pattern.
func Example_build_personBuilder() {
type Person struct {
FirstName string
LastName string
Age int
Email string
}
// Define builder functions
withFirstName := func(name string) endomorphism.Endomorphism[Person] {
return func(p Person) Person {
p.FirstName = name
return p
}
}
withLastName := func(name string) endomorphism.Endomorphism[Person] {
return func(p Person) Person {
p.LastName = name
return p
}
}
withAge := func(age int) endomorphism.Endomorphism[Person] {
return func(p Person) Person {
p.Age = age
return p
}
}
withEmail := func(email string) endomorphism.Endomorphism[Person] {
return func(p Person) Person {
p.Email = email
return p
}
}
// Build a person
personBuilder := M.ConcatAll(endomorphism.Monoid[Person]())([]endomorphism.Endomorphism[Person]{
withFirstName("Alice"),
withLastName("Smith"),
withAge(30),
withEmail("alice.smith@example.com"),
})
person := endomorphism.Build(personBuilder)
fmt.Printf("%s %s, Age: %d, Email: %s\n",
person.FirstName, person.LastName, person.Age, person.Email)
// Output: Alice Smith, Age: 30, Email: alice.smith@example.com
}
// Example_build_conditionalBuilder demonstrates conditional building using endomorphisms.
func Example_build_conditionalBuilder() {
type Settings struct {
Theme string
FontSize int
AutoSave bool
Animations bool
}
withTheme := func(theme string) endomorphism.Endomorphism[Settings] {
return func(s Settings) Settings {
s.Theme = theme
return s
}
}
withFontSize := func(size int) endomorphism.Endomorphism[Settings] {
return func(s Settings) Settings {
s.FontSize = size
return s
}
}
withAutoSave := func(enabled bool) endomorphism.Endomorphism[Settings] {
return func(s Settings) Settings {
s.AutoSave = enabled
return s
}
}
withAnimations := func(enabled bool) endomorphism.Endomorphism[Settings] {
return func(s Settings) Settings {
s.Animations = enabled
return s
}
}
// Build settings conditionally
isDarkMode := true
isAccessibilityMode := true
// Note: Monoid executes RIGHT-TO-LEFT, so later items in the slice are applied first
// We need to add items in reverse order for the desired effect
builders := []endomorphism.Endomorphism[Settings]{}
if isAccessibilityMode {
builders = append(builders, withFontSize(18)) // Will be applied last (overrides)
builders = append(builders, withAnimations(false))
}
if isDarkMode {
builders = append(builders, withTheme("dark"))
} else {
builders = append(builders, withTheme("light"))
}
builders = append(builders, withAutoSave(true))
builders = append(builders, withFontSize(14)) // Will be applied first
settingsBuilder := M.ConcatAll(endomorphism.Monoid[Settings]())(builders)
settings := endomorphism.Build(settingsBuilder)
fmt.Printf("Theme: %s\n", settings.Theme)
fmt.Printf("FontSize: %d\n", settings.FontSize)
fmt.Printf("AutoSave: %v\n", settings.AutoSave)
fmt.Printf("Animations: %v\n", settings.Animations)
// Output:
// Theme: dark
// FontSize: 18
// AutoSave: true
// Animations: false
}

View File

@@ -37,7 +37,7 @@
//
// // Define some endomorphisms
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
//
// // Compose them (RIGHT-TO-LEFT execution)
// composed := endomorphism.Compose(double, increment)
@@ -63,8 +63,8 @@
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(
// N.Mul(2), // applied third
// func(x int) int { return x + 1 }, // applied second
// func(x int) int { return x * 3 }, // applied first
// N.Add(1), // applied second
// N.Mul(3), // applied first
// )
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
//
@@ -75,7 +75,7 @@
//
// // Chain allows sequencing of endomorphisms (LEFT-TO-RIGHT)
// f := N.Mul(2)
// g := func(x int) int { return x + 1 }
// g := N.Add(1)
// chained := endomorphism.MonadChain(f, g) // f first, then g
// result := chained(5) // (5 * 2) + 1 = 11
//
@@ -84,7 +84,7 @@
// The key difference between Compose and Chain/MonadChain is execution order:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
//
// // Compose: RIGHT-TO-LEFT (mathematical composition)
// composed := endomorphism.Compose(double, increment)

View File

@@ -38,7 +38,7 @@ import (
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
// // result(5) = double(increment(5)) = double(6) = 12
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
@@ -62,7 +62,7 @@ func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// applyIncrement := endomorphism.Ap(increment)
// double := N.Mul(2)
// composed := applyIncrement(double) // double ∘ increment
@@ -92,7 +92,7 @@ func Ap[A any](fa Endomorphism[A]) Operator[A] {
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
//
// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double
// composed := endomorphism.MonadCompose(double, increment)
@@ -124,7 +124,7 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// mapped := endomorphism.MonadMap(double, increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
@@ -151,7 +151,7 @@ func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// composeWithIncrement := endomorphism.Compose(increment)
// double := N.Mul(2)
//
@@ -188,7 +188,7 @@ func Compose[A any](g Endomorphism[A]) Operator[A] {
//
// double := N.Mul(2)
// mapDouble := endomorphism.Map(double)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// mapped := mapDouble(increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func Map[A any](f Endomorphism[A]) Operator[A] {
@@ -216,7 +216,7 @@ func Map[A any](f Endomorphism[A]) Operator[A] {
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
//
// // MonadChain executes LEFT-TO-RIGHT: double first, then increment
// chained := endomorphism.MonadChain(double, increment)
@@ -294,7 +294,7 @@ func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// chainWithIncrement := endomorphism.Chain(increment)
// double := N.Mul(2)
//

View File

@@ -206,7 +206,7 @@ func TestCompose(t *testing.T) {
// TestMonadComposeVsCompose demonstrates the relationship between MonadCompose and Compose
func TestMonadComposeVsCompose(t *testing.T) {
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
increment := N.Add(1)
// MonadCompose takes both functions at once
monadComposed := MonadCompose(double, increment)
@@ -458,7 +458,7 @@ func BenchmarkCompose(b *testing.B) {
// TestComposeVsChain demonstrates the key difference between Compose and Chain
func TestComposeVsChain(t *testing.T) {
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
increment := N.Add(1)
// Compose executes RIGHT-TO-LEFT
// Compose(double, increment) means: increment first, then double
@@ -722,3 +722,352 @@ func TestChainFirst(t *testing.T) {
// But side effect should have been executed with double's result
assert.Equal(t, 10, sideEffect, "ChainFirst should execute second function for effect")
}
// TestBuild tests the Build function
func TestBuild(t *testing.T) {
t.Run("build with single transformation", func(t *testing.T) {
// Build applies endomorphism to zero value
result := Build(double)
assert.Equal(t, 0, result, "Build(double) on zero value should be 0")
})
t.Run("build with composed transformations", func(t *testing.T) {
// Create a builder that starts from zero and applies transformations
builder := M.ConcatAll(Monoid[int]())([]Endomorphism[int]{
N.Add(10),
N.Mul(2),
N.Add(5),
})
result := Build(builder)
// RIGHT-TO-LEFT: 0 + 5 = 5, 5 * 2 = 10, 10 + 10 = 20
assert.Equal(t, 20, result, "Build should apply composed transformations to zero value")
})
t.Run("build with identity", func(t *testing.T) {
result := Build(Identity[int]())
assert.Equal(t, 0, result, "Build(identity) should return zero value")
})
t.Run("build string from empty", func(t *testing.T) {
builder := M.ConcatAll(Monoid[string]())([]Endomorphism[string]{
func(s string) string { return s + "Hello" },
func(s string) string { return s + " " },
func(s string) string { return s + "World" },
})
result := Build(builder)
// RIGHT-TO-LEFT: "" + "World" = "World", "World" + " " = "World ", "World " + "Hello" = "World Hello"
assert.Equal(t, "World Hello", result, "Build should work with strings")
})
t.Run("build struct with builder pattern", func(t *testing.T) {
type Config struct {
Host string
Port int
}
withHost := func(host string) Endomorphism[Config] {
return func(c Config) Config {
c.Host = host
return c
}
}
withPort := func(port int) Endomorphism[Config] {
return func(c Config) Config {
c.Port = port
return c
}
}
builder := M.ConcatAll(Monoid[Config]())([]Endomorphism[Config]{
withHost("localhost"),
withPort(8080),
})
result := Build(builder)
assert.Equal(t, "localhost", result.Host, "Build should set Host")
assert.Equal(t, 8080, result.Port, "Build should set Port")
})
t.Run("build slice with operations", func(t *testing.T) {
type IntSlice []int
appendValue := func(v int) Endomorphism[IntSlice] {
return func(s IntSlice) IntSlice {
return append(s, v)
}
}
builder := M.ConcatAll(Monoid[IntSlice]())([]Endomorphism[IntSlice]{
appendValue(1),
appendValue(2),
appendValue(3),
})
result := Build(builder)
// RIGHT-TO-LEFT: append 3, append 2, append 1
assert.Equal(t, IntSlice{3, 2, 1}, result, "Build should construct slice")
})
}
// TestBuildAsBuilderPattern demonstrates using Build as a builder pattern
func TestBuildAsBuilderPattern(t *testing.T) {
type Person struct {
Name string
Age int
Email string
Active bool
}
// Define builder functions
withName := func(name string) Endomorphism[Person] {
return func(p Person) Person {
p.Name = name
return p
}
}
withAge := func(age int) Endomorphism[Person] {
return func(p Person) Person {
p.Age = age
return p
}
}
withEmail := func(email string) Endomorphism[Person] {
return func(p Person) Person {
p.Email = email
return p
}
}
withActive := func(active bool) Endomorphism[Person] {
return func(p Person) Person {
p.Active = active
return p
}
}
// Build a person using the builder pattern
personBuilder := M.ConcatAll(Monoid[Person]())([]Endomorphism[Person]{
withName("Alice"),
withAge(30),
withEmail("alice@example.com"),
withActive(true),
})
person := Build(personBuilder)
assert.Equal(t, "Alice", person.Name)
assert.Equal(t, 30, person.Age)
assert.Equal(t, "alice@example.com", person.Email)
assert.True(t, person.Active)
}
// TestConcatAll tests the ConcatAll function
func TestConcatAll(t *testing.T) {
t.Run("concat all with multiple endomorphisms", func(t *testing.T) {
// ConcatAll executes RIGHT-TO-LEFT
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
result := combined(5)
// RIGHT-TO-LEFT: square(5) = 25, increment(25) = 26, double(26) = 52
assert.Equal(t, 52, result, "ConcatAll should execute right-to-left")
})
t.Run("concat all with empty slice", func(t *testing.T) {
// Empty slice should return identity
identity := ConcatAll([]Endomorphism[int]{})
result := identity(42)
assert.Equal(t, 42, result, "ConcatAll with empty slice should return identity")
})
t.Run("concat all with single endomorphism", func(t *testing.T) {
combined := ConcatAll([]Endomorphism[int]{double})
result := combined(5)
assert.Equal(t, 10, result, "ConcatAll with single endomorphism should apply it")
})
t.Run("concat all with two endomorphisms", func(t *testing.T) {
// RIGHT-TO-LEFT: increment first, then double
combined := ConcatAll([]Endomorphism[int]{double, increment})
result := combined(5)
assert.Equal(t, 12, result, "ConcatAll should execute right-to-left: (5 + 1) * 2 = 12")
})
t.Run("concat all with strings", func(t *testing.T) {
appendHello := func(s string) string { return s + "Hello" }
appendSpace := func(s string) string { return s + " " }
appendWorld := func(s string) string { return s + "World" }
// RIGHT-TO-LEFT execution
combined := ConcatAll([]Endomorphism[string]{appendHello, appendSpace, appendWorld})
result := combined("")
// RIGHT-TO-LEFT: "" + "World" = "World", "World" + " " = "World ", "World " + "Hello" = "World Hello"
assert.Equal(t, "World Hello", result, "ConcatAll should work with strings")
})
t.Run("concat all for building structs", func(t *testing.T) {
type Config struct {
Host string
Port int
}
withHost := func(host string) Endomorphism[Config] {
return func(c Config) Config {
c.Host = host
return c
}
}
withPort := func(port int) Endomorphism[Config] {
return func(c Config) Config {
c.Port = port
return c
}
}
combined := ConcatAll([]Endomorphism[Config]{
withHost("localhost"),
withPort(8080),
})
result := combined(Config{})
assert.Equal(t, "localhost", result.Host)
assert.Equal(t, 8080, result.Port)
})
t.Run("concat all is equivalent to monoid ConcatAll", func(t *testing.T) {
endos := []Endomorphism[int]{double, increment, square}
result1 := ConcatAll(endos)(5)
result2 := M.ConcatAll(Monoid[int]())(endos)(5)
assert.Equal(t, result1, result2, "ConcatAll should be equivalent to M.ConcatAll(Monoid())")
})
}
// TestReduce tests the Reduce function
func TestReduce(t *testing.T) {
t.Run("reduce with multiple endomorphisms", func(t *testing.T) {
// Reduce executes LEFT-TO-RIGHT starting from zero value
result := Reduce([]Endomorphism[int]{double, increment, square})
// LEFT-TO-RIGHT: 0 -> double(0) = 0 -> increment(0) = 1 -> square(1) = 1
assert.Equal(t, 1, result, "Reduce should execute left-to-right from zero value")
})
t.Run("reduce with empty slice", func(t *testing.T) {
// Empty slice should return zero value
result := Reduce([]Endomorphism[int]{})
assert.Equal(t, 0, result, "Reduce with empty slice should return zero value")
})
t.Run("reduce with single endomorphism", func(t *testing.T) {
addTen := N.Add(10)
result := Reduce([]Endomorphism[int]{addTen})
// 0 + 10 = 10
assert.Equal(t, 10, result, "Reduce with single endomorphism should apply it to zero")
})
t.Run("reduce with sequential transformations", func(t *testing.T) {
addTen := N.Add(10)
// LEFT-TO-RIGHT: 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
result := Reduce([]Endomorphism[int]{addTen, double, increment})
assert.Equal(t, 21, result, "Reduce should apply transformations left-to-right")
})
t.Run("reduce with strings", func(t *testing.T) {
appendHello := func(s string) string { return s + "Hello" }
appendSpace := func(s string) string { return s + " " }
appendWorld := func(s string) string { return s + "World" }
// LEFT-TO-RIGHT execution
result := Reduce([]Endomorphism[string]{appendHello, appendSpace, appendWorld})
// "" -> "Hello" -> "Hello " -> "Hello World"
assert.Equal(t, "Hello World", result, "Reduce should work with strings left-to-right")
})
t.Run("reduce for building structs", func(t *testing.T) {
type Settings struct {
Theme string
FontSize int
}
withTheme := func(theme string) Endomorphism[Settings] {
return func(s Settings) Settings {
s.Theme = theme
return s
}
}
withFontSize := func(size int) Endomorphism[Settings] {
return func(s Settings) Settings {
s.FontSize = size
return s
}
}
// LEFT-TO-RIGHT application
result := Reduce([]Endomorphism[Settings]{
withTheme("dark"),
withFontSize(14),
})
assert.Equal(t, "dark", result.Theme)
assert.Equal(t, 14, result.FontSize)
})
t.Run("reduce is equivalent to Build(ConcatAll(reverse))", func(t *testing.T) {
addTen := N.Add(10)
endos := []Endomorphism[int]{addTen, double, increment}
// Reduce applies left-to-right
result1 := Reduce(endos)
// Reverse and use ConcatAll (which is right-to-left)
reversed := []Endomorphism[int]{increment, double, addTen}
result2 := Build(ConcatAll(reversed))
assert.Equal(t, result1, result2, "Reduce should be equivalent to Build(ConcatAll(reverse))")
})
}
// TestConcatAllVsReduce demonstrates the difference between ConcatAll and Reduce
func TestConcatAllVsReduce(t *testing.T) {
addTen := N.Add(10)
endos := []Endomorphism[int]{addTen, double, increment}
// ConcatAll: RIGHT-TO-LEFT composition, returns endomorphism
concatResult := ConcatAll(endos)(5)
// 5 -> increment(5) = 6 -> double(6) = 12 -> addTen(12) = 22
// Reduce: LEFT-TO-RIGHT application, returns value from zero
reduceResult := Reduce(endos)
// 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
assert.NotEqual(t, concatResult, reduceResult, "ConcatAll and Reduce should produce different results")
assert.Equal(t, 22, concatResult, "ConcatAll should execute right-to-left on input value")
assert.Equal(t, 21, reduceResult, "Reduce should execute left-to-right from zero value")
}
// TestReduceWithBuild demonstrates using Reduce vs Build with ConcatAll
func TestReduceWithBuild(t *testing.T) {
addFive := N.Add(5)
multiplyByThree := N.Mul(3)
endos := []Endomorphism[int]{addFive, multiplyByThree}
// Reduce: LEFT-TO-RIGHT from zero
reduceResult := Reduce(endos)
// 0 -> addFive(0) = 5 -> multiplyByThree(5) = 15
assert.Equal(t, 15, reduceResult)
// Build with ConcatAll: RIGHT-TO-LEFT from zero
buildResult := Build(ConcatAll(endos))
// 0 -> multiplyByThree(0) = 0 -> addFive(0) = 5
assert.Equal(t, 5, buildResult)
assert.NotEqual(t, reduceResult, buildResult, "Reduce and Build(ConcatAll) produce different results due to execution order")
}

View File

@@ -104,7 +104,7 @@ func Identity[A any]() Endomorphism[A] {
//
// sg := endomorphism.Semigroup[int]()
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
//
// // Combine using the semigroup (RIGHT-TO-LEFT execution)
// combined := sg.Concat(double, increment)
@@ -140,7 +140,7 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
//
// monoid := endomorphism.Monoid[int]()
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// square := func(x int) int { return x * x }
//
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)

View File

@@ -30,7 +30,7 @@ type (
//
// // Simple endomorphisms on integers
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
//
// // Both are endomorphisms of type Endomorphism[int]
// var f endomorphism.Endomorphism[int] = double

View File

@@ -0,0 +1,226 @@
# Idiomatic Package Review Summary
**Date:** 2025-11-26
**Reviewer:** Code Review Assistant
## Overview
This document summarizes the comprehensive review of the `idiomatic` package and its subpackages, including documentation fixes, additions, and test coverage analysis.
## Documentation Improvements
### 1. Main Package (`idiomatic/`)
-**Status:** Documentation is comprehensive and well-structured
- **File:** `doc.go` (505 lines)
- **Quality:** Excellent - includes overview, performance comparisons, usage examples, and best practices
### 2. Option Package (`idiomatic/option/`)
-**Fixed:** Added missing copyright headers to `types.go` and `function.go`
-**Fixed:** Added comprehensive documentation for type aliases in `types.go`
-**Fixed:** Enhanced function documentation in `function.go` with examples
-**Fixed:** Added missing documentation for `FromZero`, `FromNonZero`, and `FromEq` functions
- **Files Updated:**
- `types.go` - Added copyright header and type documentation
- `function.go` - Added copyright header and improved function docs
- `option.go` - Enhanced documentation for utility functions
### 3. Result Package (`idiomatic/result/`)
-**Fixed:** Added missing copyright header to `function.go`
-**Fixed:** Enhanced function documentation with examples
- **Files Updated:**
- `function.go` - Added copyright header and improved documentation
- `types.go` - Already had good documentation
### 4. IOResult Package (`idiomatic/ioresult/`)
-**Status:** Documentation is comprehensive
- **File:** `doc.go` (198 lines)
- **Quality:** Excellent - includes detailed explanations of IO operations, lazy evaluation, and side effects
### 5. ReaderIOResult Package (`idiomatic/readerioresult/`)
-**Created:** New `doc.go` file (96 lines)
-**Fixed:** Added comprehensive type documentation to `types.go`
- **New Documentation Includes:**
- Package overview and use cases
- Basic usage examples
- Composition patterns
- Error handling strategies
- Relationship to other monads
### 6. ReaderResult Package (`idiomatic/readerresult/`)
-**Fixed:** Added comprehensive type documentation to `types.go`
- **Existing:** `doc.go` already present (178 lines) with excellent documentation
## Test Coverage Analysis
### Option Package Tests
**File:** `idiomatic/option/option_test.go`
**Existing Coverage:**
-`IsNone` - Tested
-`IsSome` - Tested
-`Map` - Tested
-`Ap` - Tested
-`Chain` - Tested
-`ChainTo` - Comprehensive tests with multiple scenarios
**Missing Tests (Commented Out):**
- ⚠️ `Flatten` - Test commented out
- ⚠️ `Fold` - Test commented out
- ⚠️ `FromPredicate` - Test commented out
- ⚠️ `Alt` - Test commented out
**Recommendations:**
1. Uncomment and fix the commented-out tests
2. Add tests for:
- `FromZero`
- `FromNonZero`
- `FromEq`
- `FromNillable`
- `MapTo`
- `GetOrElse`
- `ChainFirst`
- `Reduce`
- `Filter`
- `Flap`
- `ToString`
### Result Package Tests
**File:** `idiomatic/result/either_test.go`
**Existing Coverage:**
-`IsLeft` - Tested
-`IsRight` - Tested
-`Map` - Tested
-`Ap` - Tested
-`Alt` - Tested
-`ChainFirst` - Tested
-`ChainOptionK` - Tested
-`FromOption` - Tested
-`ToString` - Tested
**Missing Tests:**
- ⚠️ `Of` - Not explicitly tested
- ⚠️ `BiMap` - Not tested
- ⚠️ `MapTo` - Not tested
- ⚠️ `MapLeft` - Not tested
- ⚠️ `Chain` - Not tested
- ⚠️ `ChainTo` - Not tested
- ⚠️ `ToOption` - Not tested
- ⚠️ `FromError` - Not tested
- ⚠️ `ToError` - Not tested
- ⚠️ `Fold` - Not tested
- ⚠️ `FromPredicate` - Not tested
- ⚠️ `FromNillable` - Not tested
- ⚠️ `GetOrElse` - Not tested
- ⚠️ `Reduce` - Not tested
- ⚠️ `OrElse` - Not tested
- ⚠️ `ToType` - Not tested
- ⚠️ `Memoize` - Not tested
- ⚠️ `Flap` - Not tested
### IOResult Package Tests
**File:** `idiomatic/ioresult/monad_test.go`
**Existing Coverage:****EXCELLENT**
- ✅ Comprehensive monad law tests (left identity, right identity, associativity)
- ✅ Functor law tests (composition, identity)
- ✅ Pointed, Functor, and Monad interface tests
- ✅ Parallel vs Sequential execution tests
- ✅ Integration tests with complex pipelines
- ✅ Error handling scenarios
**Status:** This package has exemplary test coverage and can serve as a model for other packages.
### ReaderIOResult Package
**Status:** ⚠️ **NO TESTS FOUND**
**Recommendations:**
Create comprehensive test suite covering:
- Basic construction and execution
- Map, Chain, Ap operations
- Error handling
- Environment dependency injection
- Integration with IOResult
### ReaderResult Package
**Files:** Multiple test files exist
- `array_test.go`
- `bind_test.go`
- `curry_test.go`
- `from_test.go`
- `monoid_test.go`
- `reader_test.go`
- `sequence_test.go`
**Status:** ✅ Good coverage exists
## Subpackages Review
### Packages Requiring Review:
1. **idiomatic/option/number/** - Needs documentation and test review
2. **idiomatic/option/testing/** - Contains disabled test files (`laws_test._go`, `laws._go`)
3. **idiomatic/result/exec/** - Needs review
4. **idiomatic/result/http/** - Needs review
5. **idiomatic/result/testing/** - Contains disabled test files
6. **idiomatic/ioresult/exec/** - Needs review
7. **idiomatic/ioresult/file/** - Needs review
8. **idiomatic/ioresult/http/** - Needs review
9. **idiomatic/ioresult/http/builder/** - Needs review
10. **idiomatic/ioresult/testing/** - Needs review
## Priority Recommendations
### High Priority
1. **Enable Commented Tests:** Uncomment and fix tests in `option/option_test.go`
2. **Add Missing Option Tests:** Create tests for all untested functions in option package
3. **Add Missing Result Tests:** Create comprehensive test suite for result package
4. **Create ReaderIOResult Tests:** This package has no tests at all
### Medium Priority
5. **Review Subpackages:** Systematically review exec, file, http, and testing subpackages
6. **Enable Testing Package Tests:** Investigate why `laws_test._go` files are disabled
### Low Priority
7. **Benchmark Tests:** Consider adding benchmark tests for performance-critical operations
8. **Property-Based Tests:** Consider adding property-based tests using testing/quick
## Files Modified in This Review
1. `idiomatic/option/types.go` - Added copyright and documentation
2. `idiomatic/option/function.go` - Added copyright and enhanced docs
3. `idiomatic/option/option.go` - Enhanced function documentation
4. `idiomatic/result/function.go` - Added copyright and enhanced docs
5. `idiomatic/readerioresult/doc.go` - **CREATED NEW FILE**
6. `idiomatic/readerioresult/types.go` - Added comprehensive type docs
7. `idiomatic/readerresult/types.go` - Added comprehensive type docs
## Summary Statistics
- **Packages Reviewed:** 6 main packages
- **Documentation Files Created:** 1 (readerioresult/doc.go)
- **Files Modified:** 7
- **Lines of Documentation Added:** ~150+
- **Test Coverage Status:**
- ✅ Excellent: ioresult
- ✅ Good: readerresult
- ⚠️ Needs Improvement: option, result
- ⚠️ Missing: readerioresult
## Next Steps
1. Create missing unit tests for option package functions
2. Create missing unit tests for result package functions
3. Create complete test suite for readerioresult package
4. Review and document subpackages (exec, file, http, testing, number)
5. Investigate and potentially enable disabled test files in testing subpackages
6. Consider adding integration tests that demonstrate real-world usage patterns
## Conclusion
The idiomatic package has excellent documentation at the package level, with comprehensive explanations of concepts, usage patterns, and performance characteristics. The main areas for improvement are:
1. **Test Coverage:** Several functions lack unit tests, particularly in option and result packages
2. **Subpackage Documentation:** Some subpackages need documentation review
3. **Disabled Tests:** Some test files are disabled and should be investigated
The IOResult package serves as an excellent example of comprehensive testing, including monad law verification and integration tests. This approach should be replicated across other packages.

View File

@@ -27,21 +27,21 @@
// Unlike the standard fp-go packages (option, either, result) which use struct wrappers,
// the idiomatic package uses Go's native tuple patterns:
//
// Standard either: Either[E, A] (struct wrapper)
// Idiomatic result: (A, error) (native Go tuple)
// Standard either: Either[E, A] (struct wrapper)
// Idiomatic result: (A, error) (native Go tuple)
//
// Standard option: Option[A] (struct wrapper)
// Idiomatic option: (A, bool) (native Go tuple)
// Standard option: Option[A] (struct wrapper)
// Idiomatic option: (A, bool) (native Go tuple)
//
// # Performance Benefits
//
// The idiomatic approach offers several performance advantages:
//
// - Zero allocation for creating values (no heap allocations)
// - Better CPU cache locality (no pointer indirection)
// - Native Go compiler optimizations for tuples
// - Reduced garbage collection pressure
// - Smaller memory footprint
// - Zero allocation for creating values (no heap allocations)
// - Better CPU cache locality (no pointer indirection)
// - Native Go compiler optimizations for tuples
// - Reduced garbage collection pressure
// - Smaller memory footprint
//
// Benchmarks show 2-10x performance improvements for common operations compared to struct-based
// implementations, especially for simple operations like Map, Chain, and Fold.
@@ -74,7 +74,7 @@
// none := option.None[int]() // (0, false)
//
// // Transforming values
// double := option.Map(func(x int) int { return x * 2 })
// double := option.Map(N.Mul(2))
// result := double(some) // (84, true)
// result = double(none) // (0, false)
//
@@ -103,7 +103,7 @@
// failure := result.Left[int](errors.New("oops")) // (0, error)
//
// // Transforming values
// double := result.Map(func(x int) int { return x * 2 })
// double := result.Map(N.Mul(2))
// res := double(success) // (84, nil)
// res = double(failure) // (0, error)
//
@@ -175,11 +175,11 @@
// )()
//
// Key features:
// - Lazy evaluation: Operations are not executed until the IOResult is called
// - Composable: Chain IO operations that may fail
// - Error handling: Automatic error propagation and recovery
// - Resource safety: Bracket ensures proper resource cleanup
// - Parallel execution: ApPar and TraverseArrayPar for concurrent operations
// - Lazy evaluation: Operations are not executed until the IOResult is called
// - Composable: Chain IO operations that may fail
// - Error handling: Automatic error propagation and recovery
// - Resource safety: Bracket ensures proper resource cleanup
// - Parallel execution: ApPar and TraverseArrayPar for concurrent operations
//
// # Type Signatures
//
@@ -204,33 +204,33 @@
// # When to Use Idiomatic vs Standard Packages
//
// Use idiomatic packages when:
// - Performance is critical (hot paths, tight loops)
// - You want zero-allocation functional patterns
// - You prefer Go's native error handling style
// - You're integrating with existing Go code that uses tuples
// - Memory efficiency matters (embedded systems, high-scale services)
// - You need IO operations with error handling (use ioresult)
// - Performance is critical (hot paths, tight loops)
// - You want zero-allocation functional patterns
// - You prefer Go's native error handling style
// - You're integrating with existing Go code that uses tuples
// - Memory efficiency matters (embedded systems, high-scale services)
// - You need IO operations with error handling (use ioresult)
//
// Use standard packages when:
// - You need full algebraic data type semantics
// - You're porting code from other FP languages
// - You want explicit Either[E, A] with custom error types
// - You need the complete suite of FP abstractions
// - Code clarity outweighs performance concerns
// - You need full algebraic data type semantics
// - You're porting code from other FP languages
// - You want explicit Either[E, A] with custom error types
// - You need the complete suite of FP abstractions
// - Code clarity outweighs performance concerns
//
// # Choosing Between result and ioresult
//
// Use result when:
// - Operations are pure (same input always produces same output)
// - No side effects are involved (no IO, no state mutation)
// - You want to represent success/failure without execution delay
// - Operations are pure (same input always produces same output)
// - No side effects are involved (no IO, no state mutation)
// - You want to represent success/failure without execution delay
//
// Use ioresult when:
// - Operations perform IO (file system, network, database)
// - Side effects are part of the computation
// - You need lazy evaluation (defer execution until needed)
// - You want to compose IO operations that may fail
// - Resource management is required (files, connections, locks)
// - Operations perform IO (file system, network, database)
// - Side effects are part of the computation
// - You need lazy evaluation (defer execution until needed)
// - You want to compose IO operations that may fail
// - Resource management is required (files, connections, locks)
//
// # Performance Comparison
//
@@ -473,7 +473,7 @@
// result, err := F.Pipe2(
// input,
// result.Right[int],
// result.Map(func(x int) int { return x * 2 }),
// result.Map(N.Mul(2)),
// )
// assert.NoError(t, err)
// assert.Equal(t, 42, result)
@@ -483,7 +483,7 @@
// value, ok := F.Pipe2(
// 42,
// option.Some[int],
// option.Map(func(x int) int { return x * 2 }),
// option.Map(N.Mul(2)),
// )
// assert.True(t, ok)
// assert.Equal(t, 84, value)

View File

@@ -20,6 +20,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
@@ -147,7 +148,7 @@ func TestApFirst(t *testing.T) {
result := F.Pipe2(
Of(5),
ApFirst[int](Of("ignored")),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
val, err := result()
@@ -298,7 +299,7 @@ func TestApSecond(t *testing.T) {
result := F.Pipe2(
Of(1),
ApSecond[int](Of(5)),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
val, err := result()

View File

@@ -19,6 +19,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
func BenchmarkOf(b *testing.B) {
@@ -29,7 +30,7 @@ func BenchmarkOf(b *testing.B) {
func BenchmarkMap(b *testing.B) {
io := Of(42)
f := func(x int) int { return x * 2 }
f := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -68,9 +69,9 @@ func BenchmarkBind(b *testing.B) {
}
func BenchmarkPipeline(b *testing.B) {
f1 := func(x int) int { return x + 1 }
f2 := func(x int) int { return x * 2 }
f3 := func(x int) int { return x - 3 }
f1 := N.Add(1)
f2 := N.Mul(2)
f3 := N.Sub(3)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -93,9 +94,9 @@ func BenchmarkExecute(b *testing.B) {
}
func BenchmarkExecutePipeline(b *testing.B) {
f1 := func(x int) int { return x + 1 }
f2 := func(x int) int { return x * 2 }
f3 := func(x int) int { return x - 3 }
f1 := N.Add(1)
f2 := N.Mul(2)
f3 := N.Sub(3)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -133,7 +134,7 @@ func BenchmarkLeft(b *testing.B) {
func BenchmarkMapWithError(b *testing.B) {
io := Left[int](F.Constant[error](nil)())
f := func(x int) int { return x * 2 }
f := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -194,7 +195,7 @@ func BenchmarkMonadApSecond(b *testing.B) {
func BenchmarkFunctor(b *testing.B) {
functor := Functor[int, int]()
io := Of(42)
f := func(x int) int { return x * 2 }
f := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {

View File

@@ -45,23 +45,23 @@
//
// IOResult provides two critical benefits:
//
// 1. **Lazy Evaluation**: The side effect doesn't happen when you create the IOResult,
// only when you call it (execute it). This allows you to build complex computations
// as pure data structures and defer execution until needed.
// 1. **Lazy Evaluation**: The side effect doesn't happen when you create the IOResult,
// only when you call it (execute it). This allows you to build complex computations
// as pure data structures and defer execution until needed.
//
// // This doesn't read the file yet, just describes how to read it
// readConfig := func() (Config, error) { return os.ReadFile("config.json") }
// // This doesn't read the file yet, just describes how to read it
// readConfig := func() (Config, error) { return os.ReadFile("config.json") }
//
// // Still hasn't read the file, just composed operations
// parsed := Map(parseJSON)(readConfig)
// // Still hasn't read the file, just composed operations
// parsed := Map(parseJSON)(readConfig)
//
// // NOW it reads the file and parses it
// config, err := parsed()
// // NOW it reads the file and parses it
// config, err := parsed()
//
// 2. **Referential Transparency of the Description**: While the IO operation itself has
// side effects, the IOResult value (the function) is referentially transparent. You can
// pass it around, compose it, and reason about it without triggering the side effect.
// The side effect only occurs when you explicitly call the function.
// 2. **Referential Transparency of the Description**: While the IO operation itself has
// side effects, the IOResult value (the function) is referentially transparent. You can
// pass it around, compose it, and reason about it without triggering the side effect.
// The side effect only occurs when you explicitly call the function.
//
// # Distinguishing Pure from Impure Operations
//
@@ -135,7 +135,7 @@
//
// Transforming values:
//
// doubled := Map(func(x int) int { return x * 2 })(success)
// doubled := Map(N.Mul(2))(success)
//
// Chaining computations:
//

View File

@@ -33,7 +33,7 @@ func ExampleIOResult_extraction() {
// Or more directly using GetOrElse
infallibleIO := GetOrElse(F.Constant1[error](io.Of(0)))(someIOResult) // => io returns 42
valueFromIO := infallibleIO() // => 42
valueFromIO := infallibleIO() // => 42
fmt.Println(value)
fmt.Println(valueFromIO)

View File

@@ -21,6 +21,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
@@ -78,7 +79,7 @@ func TestFunctorMap(t *testing.T) {
t.Run("Maps over successful value", func(t *testing.T) {
functor := Functor[int, int]()
io := Of(5)
mapped := functor.Map(func(x int) int { return x * 2 })(io)
mapped := functor.Map(N.Mul(2))(io)
val, err := mapped()
assert.NoError(t, err)
@@ -88,7 +89,7 @@ func TestFunctorMap(t *testing.T) {
t.Run("Maps over error preserves error", func(t *testing.T) {
functor := Functor[int, int]()
io := Left[int](errors.New("test error"))
mapped := functor.Map(func(x int) int { return x * 2 })(io)
mapped := functor.Map(N.Mul(2))(io)
_, err := mapped()
assert.Error(t, err)
@@ -161,7 +162,7 @@ func TestMonadChain(t *testing.T) {
func TestMonadAp(t *testing.T) {
t.Run("Applies function to value", func(t *testing.T) {
monad := Monad[int, int]()
fn := Of(func(x int) int { return x * 2 })
fn := Of(N.Mul(2))
val := monad.Of(5)
result := monad.Ap(val)(fn)
@@ -183,7 +184,7 @@ func TestMonadAp(t *testing.T) {
t.Run("Error in value", func(t *testing.T) {
monad := Monad[int, int]()
fn := Of(func(x int) int { return x * 2 })
fn := Of(N.Mul(2))
val := Left[int](errors.New("value error"))
result := monad.Ap(val)(fn)
@@ -423,7 +424,7 @@ func TestFunctorComposition(t *testing.T) {
functor2 := Functor[int, string]()
m := Of(5)
f := func(x int) int { return x * 2 }
f := N.Mul(2)
g := func(x int) string { return fmt.Sprintf("value: %d", x) }
// Map(g . f)
@@ -444,7 +445,7 @@ func TestFunctorComposition(t *testing.T) {
functor2 := Functor[int, string]()
m := Left[int](errors.New("test error"))
f := func(x int) int { return x * 2 }
f := N.Mul(2)
g := func(x int) string { return fmt.Sprintf("value: %d", x) }
composed := functor2.Map(F.Flow2(f, g))(m)
@@ -497,7 +498,7 @@ func TestMonadParVsSeq(t *testing.T) {
monadSeq := MonadSeq[int, int]()
io := Of(5)
f := func(x int) int { return x * 2 }
f := N.Mul(2)
par := monadPar.Map(f)(io)
seq := monadSeq.Map(f)(io)
@@ -533,7 +534,7 @@ func TestMonadParVsSeq(t *testing.T) {
monadPar := MonadPar[int, int]()
io := Of(5)
f := func(x int) int { return x * 2 }
f := N.Mul(2)
def := monadDefault.Map(f)(io)
par := monadPar.Map(f)(io)
@@ -555,7 +556,7 @@ func TestMonadIntegration(t *testing.T) {
// Build a pipeline: multiply by 2, add 3, then format
result := F.Pipe2(
monad1.Of(5),
monad1.Map(func(x int) int { return x * 2 }),
monad1.Map(N.Mul(2)),
monad1.Chain(func(x int) IOResult[int] {
return Of(x + 3)
}),
@@ -577,7 +578,7 @@ func TestMonadIntegration(t *testing.T) {
result := F.Pipe2(
monad1.Of(5),
monad1.Map(func(x int) int { return x * 2 }),
monad1.Map(N.Mul(2)),
monad1.Chain(func(x int) IOResult[int] {
if x > 5 {
return Left[int](errors.New("value too large"))

View File

@@ -20,6 +20,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
// Benchmark the closure allocations in Bind
@@ -69,7 +70,7 @@ func BenchmarkBindAllocations(b *testing.B) {
func BenchmarkMapPatterns(b *testing.B) {
b.Run("SimpleFunction", func(b *testing.B) {
io := Of(42)
f := func(x int) int { return x * 2 }
f := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
@@ -85,7 +86,7 @@ func BenchmarkMapPatterns(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := Map(func(x int) int { return x * 2 })(io)
result := Map(N.Mul(2))(io)
_, _ = result()
}
})
@@ -98,9 +99,9 @@ func BenchmarkMapPatterns(b *testing.B) {
for i := 0; i < b.N; i++ {
result := F.Pipe3(
io,
Map(func(x int) int { return x + 1 }),
Map(func(x int) int { return x * 2 }),
Map(func(x int) int { return x - 3 }),
Map(N.Add(1)),
Map(N.Mul(2)),
Map(N.Sub(3)),
)
_, _ = result()
}
@@ -188,7 +189,7 @@ func BenchmarkErrorPaths(b *testing.B) {
for i := 0; i < b.N; i++ {
result := F.Pipe2(
Of(42),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
Chain(func(x int) IOResult[int] { return Of(x + 1) }),
)
_, _ = result()
@@ -201,7 +202,7 @@ func BenchmarkErrorPaths(b *testing.B) {
for i := 0; i < b.N; i++ {
result := F.Pipe2(
Left[int](errors.New("error")),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
Chain(func(x int) IOResult[int] { return Of(x + 1) }),
)
_, _ = result()

View File

@@ -17,6 +17,8 @@ package option
import (
"testing"
N "github.com/IBM/fp-go/v2/number"
)
// Benchmark basic construction
@@ -46,7 +48,7 @@ func BenchmarkIsSome(b *testing.B) {
func BenchmarkMap(b *testing.B) {
v, ok := Some(21)
mapper := Map(func(x int) int { return x * 2 })
mapper := Map(N.Mul(2))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {

View File

@@ -17,6 +17,8 @@ package option
import (
"testing"
N "github.com/IBM/fp-go/v2/number"
)
// Benchmark shallow chain (1 step)
@@ -81,11 +83,11 @@ func BenchmarkMap_5Steps(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Map(func(x int) int { return x + 1 })(v, ok)
v2, ok2 := Map(func(x int) int { return x * 3 })(v1, ok1)
v3, ok3 := Map(func(x int) int { return x + 20 })(v2, ok2)
v4, ok4 := Map(func(x int) int { return x / 2 })(v3, ok3)
_, _ = Map(func(x int) int { return x - 10 })(v4, ok4)
v1, ok1 := Map(N.Add(1))(v, ok)
v2, ok2 := Map(N.Mul(3))(v1, ok1)
v3, ok3 := Map(N.Add(20))(v2, ok2)
v4, ok4 := Map(N.Div(2))(v3, ok3)
_, _ = Map(N.Sub(10))(v4, ok4)
}
}

View File

@@ -58,7 +58,7 @@
//
// Map transforms the contained value:
//
// double := Map(func(x int) int { return x * 2 })
// double := Map(N.Mul(2))
// result := double(Some(21)) // (42, true)
// result := double(None[int]()) // (0, false)
//
@@ -113,7 +113,7 @@
//
// Applicative example:
//
// fab := Some(func(x int) int { return x * 2 })
// fab := Some(N.Mul(2))
// fa := Some(21)
// result := Ap[int](fa)(fab) // (42, true)
//

View File

@@ -1,15 +1,39 @@
// Copyright (c) 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 option
// Pipe1 takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
// Pipe1 takes an initial value t0 and successively applies 1 function where the input of a function is the return value of the previous function.
// The final return value is the result of the last function application.
//
// Example:
//
// result := Pipe1(42, func(x int) (int, bool) { return x * 2, true }) // (84, true)
//
//go:inline
func Pipe1[F1 ~func(T0) (T1, bool), T0, T1 any](t0 T0, f1 F1) (T1, bool) {
return f1(t0)
}
// Flow1 creates a function that takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
// Flow1 creates a function that takes an initial value t0 and successively applies 1 function where the input of a function is the return value of the previous function.
// The final return value is the result of the last function application.
//
// Example:
//
// double := Flow1(func(x int, ok bool) (int, bool) { return x * 2, ok })
// result := double(42, true) // (84, true)
//
//go:inline
func Flow1[F1 ~func(T0, bool) (T1, bool), T0, T1 any](f1 F1) func(T0, bool) (T1, bool) {

View File

@@ -19,6 +19,7 @@ import (
"testing"
"github.com/IBM/fp-go/v2/eq"
N "github.com/IBM/fp-go/v2/number"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
@@ -206,13 +207,13 @@ func TestFlow5(t *testing.T) {
func TestMakeFunctor(t *testing.T) {
t.Run("Map with functor", func(t *testing.T) {
f := MakeFunctor[int, int]()
double := f.Map(func(x int) int { return x * 2 })
double := f.Map(N.Mul(2))
AssertEq(Some(42))(double(Some(21)))(t)
})
t.Run("Map with None", func(t *testing.T) {
f := MakeFunctor[int, int]()
double := f.Map(func(x int) int { return x * 2 })
double := f.Map(N.Mul(2))
AssertEq(None[int]())(double(None[int]()))(t)
})
}

View File

@@ -30,7 +30,7 @@
// result, ok := none // ok == false, result == 0
//
// // Transforming Options
// doubled := Map(func(x int) int { return x * 2 })(some) // (84, true)
// doubled := Map(N.Mul(2))(some) // (84, true)
package option
import (
@@ -56,16 +56,47 @@ func FromPredicate[A any](pred func(A) bool) Kleisli[A, A] {
}
}
// FromZero returns a function that creates an Option based on whether a value is the zero value.
// Returns Some if the value is the zero value, None otherwise.
//
// Example:
//
// checkZero := FromZero[int]()
// result := checkZero(0) // Some(0)
// result := checkZero(5) // None
//
//go:inline
func FromZero[A comparable]() Kleisli[A, A] {
return FromPredicate(P.IsZero[A]())
}
// FromNonZero returns a function that creates an Option based on whether a value is non-zero.
// Returns Some if the value is non-zero, None otherwise.
//
// Example:
//
// checkNonZero := FromNonZero[int]()
// result := checkNonZero(5) // Some(5)
// result := checkNonZero(0) // None
//
//go:inline
func FromNonZero[A comparable]() Kleisli[A, A] {
return FromPredicate(P.IsNonZero[A]())
}
// FromEq returns a function that creates an Option based on equality with a given value.
// The returned function takes a value to compare against and returns a Kleisli function.
//
// Parameters:
// - pred: An equality predicate
//
// Example:
//
// import "github.com/IBM/fp-go/v2/eq"
// equals42 := FromEq(eq.FromStrictEquals[int]())(42)
// result := equals42(42) // Some(42)
// result := equals42(10) // None
//
//go:inline
func FromEq[A any](pred eq.Eq[A]) func(A) Kleisli[A, A] {
return F.Flow2(P.IsEqual(pred), FromPredicate[A])

View File

@@ -1,3 +1,18 @@
// Copyright (c) 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 option
import (
@@ -7,6 +22,10 @@ import (
)
type (
Seq[T any] = iter.Seq[T]
// Seq is an iterator sequence type alias for working with Go 1.23+ iterators.
Seq[T any] = iter.Seq[T]
// Endomorphism represents a function from type T to type T.
// It is commonly used for transformations that preserve the type.
Endomorphism[T any] = endomorphism.Endomorphism[T]
)

View File

@@ -0,0 +1,112 @@
// 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 readerioresult provides a ReaderIOResult monad that combines Reader, IO, and Result monads.
//
// A ReaderIOResult[R, A] represents a computation that:
// - Depends on an environment of type R (Reader aspect)
// - Performs IO operations (IO aspect)
// - May fail with an error (Result aspect, which is Either[error, A])
//
// This is equivalent to Reader[R, IOResult[A]] or Reader[R, func() (A, error)].
//
// # Use Cases
//
// ReaderIOResult is particularly useful for:
//
// 1. Dependency injection with IO and error handling - pass configuration/services through
// computations that perform side effects and may fail
// 2. Functional IO with context - compose IO operations that depend on environment and may error
// 3. Testing - easily mock dependencies and IO operations by changing the environment value
// 4. Resource management - manage resources that depend on configuration
//
// # Basic Example
//
// type Config struct {
// DatabaseURL string
// Timeout time.Duration
// }
//
// // Function that needs config, performs IO, and may fail
// func fetchUser(id int) readerioresult.ReaderIOResult[Config, User] {
// return func(cfg Config) ioresult.IOResult[User] {
// return func() (User, error) {
// // Use cfg.DatabaseURL and cfg.Timeout to fetch user
// return queryDatabase(cfg.DatabaseURL, id, cfg.Timeout)
// }
// }
// }
//
// // Execute by providing the config
// cfg := Config{DatabaseURL: "postgres://...", Timeout: 5 * time.Second}
// ioResult := fetchUser(42)(cfg) // Returns IOResult[User]
// user, err := ioResult() // Execute the IO operation
//
// # Composition
//
// ReaderIOResult provides several ways to compose computations:
//
// 1. Map - transform successful values
// 2. Chain (FlatMap) - sequence dependent IO operations
// 3. Ap - combine independent IO computations
// 4. ChainFirst - perform IO for side effects while keeping original value
//
// # Example with Composition
//
// type AppContext struct {
// DB *sql.DB
// Cache Cache
// Log Logger
// }
//
// getUserWithCache := F.Pipe2(
// getFromCache(userID),
// readerioresult.Alt(func() readerioresult.ReaderIOResult[AppContext, User] {
// return F.Pipe2(
// getFromDB(userID),
// readerioresult.ChainFirst(saveToCache),
// )
// }),
// )
//
// ctx := AppContext{DB: db, Cache: cache, Log: logger}
// user, err := getUserWithCache(ctx)()
//
// # Error Handling
//
// ReaderIOResult provides several functions for error handling:
//
// - Left/Right - create failed/successful values
// - GetOrElse - provide a default value for errors
// - OrElse - recover from errors with an alternative computation
// - Fold - handle both success and failure cases
// - ChainLeft - transform error values into new computations
//
// # Relationship to Other Monads
//
// ReaderIOResult is related to several other monads in this library:
//
// - Reader[R, A] - ReaderIOResult without IO or error handling
// - IOResult[A] - ReaderIOResult without environment dependency
// - ReaderResult[R, A] - ReaderIOResult without IO (pure computations)
// - ReaderIO[R, A] - ReaderIOResult without error handling
// - ReaderIOEither[R, E, A] - like ReaderIOResult but with custom error type E
//
// # Performance Note
//
// ReaderIOResult is a zero-cost abstraction - it compiles to a simple function type
// with no runtime overhead beyond the underlying computation. The IO operations are
// lazy and only executed when the final IOResult is called.
package readerioresult

View File

@@ -27,18 +27,40 @@ import (
)
type (
// Endomorphism represents a function from type A to type A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
Lazy[A any] = lazy.Lazy[A]
Option[A any] = option.Option[A]
Result[A any] = result.Result[A]
Reader[R, A any] = reader.Reader[R, A]
IO[A any] = io.IO[A]
IOResult[A any] = ioresult.IOResult[A]
// Lazy represents a deferred computation that produces a value of type A when evaluated.
Lazy[A any] = lazy.Lazy[A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
Result[A any] = result.Result[A]
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
// IO represents a computation that performs side effects and returns a value of type A.
IO[A any] = io.IO[A]
// IOResult represents a computation that performs IO and may fail with an error.
IOResult[A any] = ioresult.IOResult[A]
// ReaderIOResult represents a computation that depends on an environment R,
// performs IO operations, and may fail with an error.
// It is equivalent to Reader[R, IOResult[A]] or func(R) func() (A, error).
ReaderIOResult[R, A any] = Reader[R, IOResult[A]]
// Monoid represents a monoid structure for ReaderIOResult values.
Monoid[R, A any] = monoid.Monoid[ReaderIOResult[R, A]]
Kleisli[R, A, B any] = Reader[A, ReaderIOResult[R, B]]
// Kleisli represents a function from A to a ReaderIOResult of B.
// It is used for chaining computations that depend on environment, perform IO, and may fail.
Kleisli[R, A, B any] = Reader[A, ReaderIOResult[R, B]]
// Operator represents a transformation from ReaderIOResult[R, A] to ReaderIOResult[R, B].
// It is commonly used in function composition pipelines.
Operator[R, A, B any] = Kleisli[R, ReaderIOResult[R, A], B]
)

View File

@@ -20,6 +20,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
type BenchContext struct {
@@ -50,7 +51,7 @@ func BenchmarkLeft(b *testing.B) {
func BenchmarkMap(b *testing.B) {
ctx := BenchContext{Value: 42}
rr := Of[BenchContext](10)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
mapped := F.Pipe1(rr, Map[BenchContext](double))
@@ -61,7 +62,7 @@ func BenchmarkMap(b *testing.B) {
func BenchmarkMapChain(b *testing.B) {
ctx := BenchContext{Value: 42}
rr := Of[BenchContext](1)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := F.Pipe3(
@@ -109,7 +110,7 @@ func BenchmarkChainDeep(b *testing.B) {
func BenchmarkAp(b *testing.B) {
ctx := BenchContext{Value: 42}
fab := Of[BenchContext](func(x int) int { return x * 2 })
fab := Of[BenchContext](N.Mul(2))
fa := Of[BenchContext](21)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -188,7 +189,7 @@ func BenchmarkErrorPropagation(b *testing.B) {
ctx := BenchContext{Value: 42}
err := testError
rr := Left[BenchContext, int](err)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {

View File

@@ -17,7 +17,6 @@ package readerresult
import (
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/result"
AP "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
@@ -69,7 +68,7 @@ func Do[R, S any](
// ConfigService ConfigService
// }
//
// result := F.Pipe2(
// result := function.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.Bind(
// func(user User) func(State) State {
@@ -173,7 +172,7 @@ func BindTo[R, S1, T any](
// return env.ConfigService.GetConfig()
// })
//
// result := F.Pipe2(
// result := function.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.ApS(
// func(user User) func(State) State {
@@ -228,7 +227,7 @@ func ApS[R, S1, S2, T any](
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfig()
// })
// result := F.Pipe2(
// result := function.Pipe2(
// readereither.Of[Env, error](State{}),
// readereither.ApSL(configLens, getConfig),
// )
@@ -265,7 +264,7 @@ func ApSL[R, S, T any](
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// result := function.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.BindL(userLens, func(user User) readereither.ReaderResult[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
@@ -279,7 +278,7 @@ func BindL[R, S, T any](
lens L.Lens[S, T],
f Kleisli[R, T, T],
) Operator[R, S, S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
@@ -302,7 +301,7 @@ func BindL[R, S, T any](
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// result := F.Pipe2(
// result := function.Pipe2(
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
// readereither.LetL(configLens, func(cfg Config) Config {
// cfg.Port = 8080
@@ -315,7 +314,7 @@ func LetL[R, S, T any](
lens L.Lens[S, T],
f Endomorphism[T],
) Operator[R, S, S] {
return Let[R](lens.Set, F.Flow2(lens.Get, f))
return Let[R](lens.Set, function.Flow2(lens.Get, f))
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
@@ -338,7 +337,7 @@ func LetL[R, S, T any](
// )
//
// newConfig := Config{Host: "localhost", Port: 8080}
// result := F.Pipe2(
// result := function.Pipe2(
// readereither.Do[any, error](State{}),
// readereither.LetToL(configLens, newConfig),
// )
@@ -374,7 +373,7 @@ func LetToL[R, S, T any](
// }
// }
//
// result := F.Pipe2(
// result := function.Pipe2(
// readerresult.Do[Env](State{}),
// readerresult.BindReaderK(
// func(path string) func(State) State {
@@ -435,7 +434,7 @@ func BindEitherK[R, S1, S2, T any](
// return result.Of(s.Value * 2)
// }
//
// result := F.Pipe2(
// result := function.Pipe2(
// readerresult.Do[any](State{Value: 5}),
// readerresult.BindResultK(
// func(parsed int) func(State) State {
@@ -481,7 +480,7 @@ func BindResultK[R, S1, S2, T any](
// return env.ConfigPath
// }
//
// result := F.Pipe1(
// result := function.Pipe1(
// reader.Of[Env](getConfigPath),
// readerresult.BindToReader(func(path string) State {
// return State{Config: path}
@@ -530,7 +529,7 @@ func BindToEither[
// return 42
// })
//
// computation := F.Pipe1(
// computation := function.Pipe1(
// parseResult,
// readerresult.BindToResult[any](func(value int) State {
// return State{Value: value}
@@ -572,7 +571,7 @@ func BindToResult[
// getDefaultPort := func(env Env) int { return env.DefaultPort }
// getDefaultHost := func(env Env) string { return env.DefaultHost }
//
// result := F.Pipe2(
// result := function.Pipe2(
// readerresult.Do[Env](State{}),
// readerresult.ApReaderS(
// func(port int) func(State) State {
@@ -635,7 +634,7 @@ func ApResultS[
// parseValue1 := result.TryCatch(func() int { return 42 })
// parseValue2 := result.TryCatch(func() int { return 100 })
//
// computation := F.Pipe2(
// computation := function.Pipe2(
// readerresult.Do[any](State{}),
// readerresult.ApResultS(
// func(v int) func(State) State {

View File

@@ -116,7 +116,7 @@ func FromReader[R, A any](r Reader[R, A]) ReaderResult[R, A] {
// Example:
//
// rr := readerresult.Of[Config](5)
// doubled := readerresult.MonadMap(rr, func(x int) int { return x * 2 })
// doubled := readerresult.MonadMap(rr, N.Mul(2))
// // doubled(cfg) returns (10, nil)
func MonadMap[R, A, B any](fa ReaderResult[R, A], f func(A) B) ReaderResult[R, B] {
mp := result.Map(f)
@@ -130,7 +130,7 @@ func MonadMap[R, A, B any](fa ReaderResult[R, A], f func(A) B) ReaderResult[R, B
//
// Example:
//
// double := readerresult.Map[Config](func(x int) int { return x * 2 })
// double := readerresult.Map[Config](N.Mul(2))
// result := F.Pipe1(readerresult.Of[Config](5), double)
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
mp := result.Map(f)
@@ -462,7 +462,7 @@ func Flatten[R, A any](mma ReaderResult[R, ReaderResult[R, A]]) ReaderResult[R,
// Example:
//
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// result := readerresult.MonadBiMap(rr, enrichErr, double)
//
//go:inline
@@ -482,7 +482,7 @@ func MonadBiMap[R, A, B any](fa ReaderResult[R, A], f Endomorphism[error], g fun
// Example:
//
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// result := F.Pipe1(rr, readerresult.BiMap[Config](enrichErr, double))
func BiMap[R, A, B any](f Endomorphism[error], g func(A) B) Operator[R, A, B] {
return func(fa ReaderResult[R, A]) ReaderResult[R, B] {
@@ -530,7 +530,7 @@ func Read[A, R any](r R) func(ReaderResult[R, A]) (A, error) {
//
// Example:
//
// fabr := readerresult.Of[Config](func(x int) int { return x * 2 })
// fabr := readerresult.Of[Config](N.Mul(2))
// result := readerresult.MonadFlap(fabr, 5) // Returns (10, nil)
//
//go:inline

View File

@@ -22,6 +22,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
RES "github.com/IBM/fp-go/v2/result"
@@ -117,7 +118,7 @@ func TestMap(t *testing.T) {
func TestMonadMap(t *testing.T) {
rr := Of[MyContext](5)
doubled := MonadMap(rr, func(x int) int { return x * 2 })
doubled := MonadMap(rr, N.Mul(2))
v, err := doubled(defaultContext)
assert.NoError(t, err)
assert.Equal(t, 10, v)
@@ -341,7 +342,7 @@ func TestFlatten(t *testing.T) {
func TestBiMap(t *testing.T) {
enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
double := func(x int) int { return x * 2 }
double := N.Mul(2)
res := F.Pipe1(Of[MyContext](5), BiMap[MyContext](enrichErr, double))
v, err := res(defaultContext)
@@ -376,7 +377,7 @@ func TestRead(t *testing.T) {
}
func TestFlap(t *testing.T) {
fabr := Of[MyContext](func(x int) int { return x * 2 })
fabr := Of[MyContext](N.Mul(2))
flapped := MonadFlap(fabr, 5)
v, err := flapped(defaultContext)
assert.NoError(t, err)

View File

@@ -26,16 +26,37 @@ import (
)
type (
// Endomorphism represents a function from type A to type A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
Lazy[A any] = lazy.Lazy[A]
Option[A any] = option.Option[A]
Either[E, A any] = either.Either[E, A]
Result[A any] = result.Result[A]
Reader[R, A any] = reader.Reader[R, A]
// Lazy represents a deferred computation that produces a value of type A when evaluated.
Lazy[A any] = lazy.Lazy[A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
// Either represents a value that can be one of two types: Left (E) or Right (A).
Either[E, A any] = either.Either[E, A]
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
Result[A any] = result.Result[A]
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
// ReaderResult represents a computation that depends on an environment R and may fail with an error.
// It is equivalent to Reader[R, Result[A]] or func(R) (A, error).
// This combines dependency injection with error handling in a functional style.
ReaderResult[R, A any] = func(R) (A, error)
Monoid[R, A any] = monoid.Monoid[ReaderResult[R, A]]
Kleisli[R, A, B any] = Reader[A, ReaderResult[R, B]]
// Monoid represents a monoid structure for ReaderResult values.
Monoid[R, A any] = monoid.Monoid[ReaderResult[R, A]]
// Kleisli represents a function from A to a ReaderResult of B.
// It is used for chaining computations that depend on environment and may fail.
Kleisli[R, A, B any] = Reader[A, ReaderResult[R, B]]
// Operator represents a transformation from ReaderResult[R, A] to ReaderResult[R, B].
// It is commonly used in function composition pipelines.
Operator[R, A, B any] = Kleisli[R, ReaderResult[R, A], B]
)

View File

@@ -27,7 +27,7 @@ func Example_extraction() {
rightValue, rightErr := Right(10)
// Convert Either[A] to A with a default value
leftWithDefault := GetOrElse(F.Constant1[error](0))(leftValue, leftErr) // 0
leftWithDefault := GetOrElse(F.Constant1[error](0))(leftValue, leftErr) // 0
rightWithDefault := GetOrElse(F.Constant1[error](0))(rightValue, rightErr) // 10
// Apply a different function on Left(...)/Right(...)

View File

@@ -1,7 +1,26 @@
// Copyright (c) 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 result
// Pipe1 takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
// Pipe1 takes an initial value t0 and successively applies 1 function where the input of a function is the return value of the previous function.
// The final return value is the result of the last function application.
//
// Example:
//
// result, err := Pipe1(42, func(x int) (int, error) { return x * 2, nil }) // (84, nil)
//
//go:inline
func Pipe1[F1 ~func(T0) (T1, error), T0, T1 any](t0 T0, f1 F1) (T1, error) {

View File

@@ -67,7 +67,7 @@ import (
// })
//
// // ApV with both function and value having errors
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// apv := result.ApV[int, int](errorSemigroup)
//
// value := result.Left[int](errors.New("invalid value"))

View File

@@ -21,6 +21,7 @@ import (
"strings"
"testing"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
@@ -67,7 +68,7 @@ func TestApV_BothRight(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
value, verr := Right(5)
fn, ferr := Right(double)
@@ -83,7 +84,7 @@ func TestApV_ValueLeft_FunctionRight(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
valueError := errors.New("invalid value")
value, verr := Left[int](valueError)
@@ -345,7 +346,7 @@ func BenchmarkApV_BothRight(b *testing.B) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
value, verr := Right(5)
fn, ferr := Right(double)

View File

@@ -141,6 +141,10 @@ func GetOrElse[E, A, HKTEA, HKTA any](mchain func(HKTEA, func(ET.Either[E, A]) H
return MatchE(mchain, onLeft, mof)
}
func GetOrElseOf[E, A, HKTEA, HKTA any](mchain func(HKTEA, func(ET.Either[E, A]) HKTA) HKTA, mof func(A) HKTA, onLeft func(E) A) func(HKTEA) HKTA {
return MatchE(mchain, F.Flow2(onLeft, mof), mof)
}
func OrElse[E1, E2, A, HKTE1A, HKTE2A any](mchain func(HKTE1A, func(ET.Either[E1, A]) HKTE2A) HKTE2A, mof func(ET.Either[E2, A]) HKTE2A, onLeft func(E1) HKTE2A) func(HKTE1A) HKTE2A {
return MatchE(mchain, onLeft, F.Flow2(ET.Right[E2, A], mof))
}

View File

@@ -34,13 +34,16 @@ import (
// Monads must satisfy the monad laws:
//
// Left Identity:
// Chain(f)(Of(a)) == f(a)
//
// Chain(f)(Of(a)) == f(a)
//
// Right Identity:
// Chain(Of)(m) == m
//
// Chain(Of)(m) == m
//
// Associativity:
// Chain(g)(Chain(f)(m)) == Chain(x => Chain(g)(f(x)))(m)
//
// Chain(g)(Chain(f)(m)) == Chain(x => Chain(g)(f(x)))(m)
//
// Type Parameters:
// - A: The input value type
@@ -50,20 +53,21 @@ import (
// - HKTFAB: The higher-kinded type containing a function from A to B
//
// Example:
// // Given a Monad for Option
// var m Monad[int, string, Option[int], Option[string], Option[func(int) string]]
//
// // Use Of to create a value
// value := m.Of(42) // Some(42)
// // Given a Monad for Option
// var m Monad[int, string, Option[int], Option[string], Option[func(int) string]]
//
// // Use Chain for dependent operations
// chainFn := m.Chain(func(x int) Option[string] {
// if x > 0 {
// return Some(strconv.Itoa(x))
// }
// return None[string]()
// })
// result := chainFn(value) // Some("42")
// // Use Of to create a value
// value := m.Of(42) // Some(42)
//
// // Use Chain for dependent operations
// chainFn := m.Chain(func(x int) Option[string] {
// if x > 0 {
// return Some(strconv.Itoa(x))
// }
// return None[string]()
// })
// result := chainFn(value) // Some("42")
type Monad[A, B, HKTA, HKTB, HKTFAB any] interface {
applicative.Applicative[A, B, HKTA, HKTB, HKTFAB]
chain.Chainable[A, B, HKTA, HKTB, HKTFAB]

View File

@@ -26,9 +26,10 @@ package pointed
// - HKTA: The higher-kinded type containing A (e.g., Option[A], Either[E, A])
//
// Example:
// // Given a pointed functor for Option[int]
// var p Pointed[int, Option[int]]
// result := p.Of(42) // Returns Some(42)
//
// // Given a pointed functor for Option[int]
// var p Pointed[int, Option[int]]
// result := p.Of(42) // Returns Some(42)
type Pointed[A, HKTA any] interface {
// Of lifts a pure value into its higher-kinded type context.
//

View File

@@ -369,7 +369,7 @@ func TestMonadTypeClass(t *testing.T) {
m.Chain(func(x int) IO[int] {
return m.Of(x * 2)
}),
m.Map(func(x int) int { return x + 1 }),
m.Map(N.Add(1)),
)
assert.Equal(t, 43, result())

View File

@@ -18,6 +18,10 @@ package io
import (
"fmt"
"log"
"os"
"strings"
"sync"
"text/template"
L "github.com/IBM/fp-go/v2/logging"
)
@@ -32,13 +36,14 @@ import (
// io.ChainFirst(io.Logger[User]()("Fetched user")),
// processUser,
// )
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, A] {
_, right := L.LoggingCallbacks(loggers...)
return func(prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
return func(prefix string) Kleisli[A, A] {
return func(a A) IO[A] {
return func() A {
right("%s: %v", prefix, a)
})
return a
}
}
}
}
@@ -53,11 +58,12 @@ func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
// io.ChainFirst(io.Logf[User]("User: %+v")),
// processUser,
// )
func Logf[A any](prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
func Logf[A any](prefix string) Kleisli[A, A] {
return func(a A) IO[A] {
return func() A {
log.Printf(prefix, a)
})
return a
}
}
}
@@ -72,10 +78,102 @@ func Logf[A any](prefix string) Kleisli[A, any] {
// io.ChainFirst(io.Printf[User]("User: %+v\n")),
// processUser,
// )
func Printf[A any](prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
func Printf[A any](prefix string) Kleisli[A, A] {
return func(a A) IO[A] {
return func() A {
fmt.Printf(prefix, a)
})
return a
}
}
}
// handleLogging is a helper function that creates a Kleisli arrow for logging/printing
// values using Go template syntax. It lazily compiles the template on first use and
// executes it with the provided value as data.
//
// Parameters:
// - onSuccess: callback function to handle successfully formatted output
// - onError: callback function to handle template parsing or execution errors
// - prefix: Go template string to format the value
//
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
// The function always returns the original value unchanged, making it suitable for
// use with ChainFirst or similar operations.
func handleLogging[A any](onSuccess func(string), onError func(error), prefix string) Kleisli[A, A] {
var tmp *template.Template
var err error
var once sync.Once
init := func() {
tmp, err = template.New("").Parse(prefix)
}
return func(a A) IO[A] {
return func() A {
// make sure to compile lazily
once.Do(init)
if err == nil {
var buffer strings.Builder
tmpErr := tmp.Execute(&buffer, a)
if tmpErr != nil {
onError(tmpErr)
onSuccess(fmt.Sprintf("%v", a))
} else {
onSuccess(buffer.String())
}
} else {
onError(err)
onSuccess(fmt.Sprintf("%v", a))
}
// in any case return the original value
return a
}
}
}
// LogGo constructs a logger function using Go template syntax for formatting.
// The prefix string is parsed as a Go template and executed with the value as data.
// Both successful output and template errors are logged using log.Println.
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
// result := pipe.Pipe2(
// fetchUser(),
// io.ChainFirst(io.LogGo[User]("User: {{.Name}}, Age: {{.Age}}")),
// processUser,
// )
func LogGo[A any](prefix string) Kleisli[A, A] {
return handleLogging[A](func(value string) {
log.Println(value)
}, func(err error) {
log.Println(err)
}, prefix)
}
// PrintGo constructs a printer function using Go template syntax for formatting.
// The prefix string is parsed as a Go template and executed with the value as data.
// Successful output is printed to stdout using fmt.Println, while template errors
// are printed to stderr using fmt.Fprintln.
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
// result := pipe.Pipe2(
// fetchUser(),
// io.ChainFirst(io.PrintGo[User]("User: {{.Name}}, Age: {{.Age}}")),
// processUser,
// )
func PrintGo[A any](prefix string) Kleisli[A, A] {
return handleLogging[A](func(value string) {
fmt.Println(value)
}, func(err error) {
fmt.Fprintln(os.Stderr, err)
}, prefix)
}

View File

@@ -16,25 +16,206 @@
package io
import (
"bytes"
"log"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestLogger(t *testing.T) {
l := Logger[int]()
lio := l("out")
assert.NotPanics(t, func() { lio(10)() })
}
func TestLoggerWithCustomLogger(t *testing.T) {
var buf bytes.Buffer
customLogger := log.New(&buf, "", 0)
l := Logger[int](customLogger)
lio := l("test value")
result := lio(42)()
assert.Equal(t, 42, result)
assert.Contains(t, buf.String(), "test value")
assert.Contains(t, buf.String(), "42")
}
func TestLoggerReturnsOriginalValue(t *testing.T) {
type TestStruct struct {
Name string
Value int
}
l := Logger[TestStruct]()
lio := l("test")
input := TestStruct{Name: "test", Value: 100}
result := lio(input)()
assert.Equal(t, input, result)
}
func TestLogf(t *testing.T) {
l := Logf[int]
lio := l("Value is %d")
assert.NotPanics(t, func() { lio(10)() })
}
func TestLogfReturnsOriginalValue(t *testing.T) {
l := Logf[string]
lio := l("String: %s")
input := "hello"
result := lio(input)()
assert.Equal(t, input, result)
}
func TestPrintfLogger(t *testing.T) {
l := Printf[int]
lio := l("Value: %d\n")
assert.NotPanics(t, func() { lio(10)() })
}
func TestPrintfLoggerReturnsOriginalValue(t *testing.T) {
l := Printf[float64]
lio := l("Number: %.2f\n")
input := 3.14159
result := lio(input)()
assert.Equal(t, input, result)
}
func TestLogGo(t *testing.T) {
type User struct {
Name string
Age int
}
l := LogGo[User]
lio := l("User: {{.Name}}, Age: {{.Age}}")
input := User{Name: "Alice", Age: 30}
assert.NotPanics(t, func() { lio(input)() })
}
func TestLogGoReturnsOriginalValue(t *testing.T) {
type Product struct {
ID int
Name string
Price float64
}
l := LogGo[Product]
lio := l("Product: {{.Name}} ({{.ID}})")
input := Product{ID: 123, Name: "Widget", Price: 19.99}
result := lio(input)()
assert.Equal(t, input, result)
}
func TestLogGoWithInvalidTemplate(t *testing.T) {
l := LogGo[int]
// Invalid template syntax
lio := l("Value: {{.MissingField")
// Should not panic even with invalid template
assert.NotPanics(t, func() { lio(42)() })
}
func TestLogGoWithComplexTemplate(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
l := LogGo[Person]
lio := l("Person: {{.Name}} from {{.Address.City}}")
input := Person{
Name: "Bob",
Address: Address{Street: "Main St", City: "NYC"},
}
result := lio(input)()
assert.Equal(t, input, result)
}
func TestPrintGo(t *testing.T) {
type User struct {
Name string
Age int
}
l := PrintGo[User]
lio := l("User: {{.Name}}, Age: {{.Age}}")
input := User{Name: "Charlie", Age: 25}
assert.NotPanics(t, func() { lio(input)() })
}
func TestPrintGoReturnsOriginalValue(t *testing.T) {
type Score struct {
Player string
Points int
}
l := PrintGo[Score]
lio := l("{{.Player}}: {{.Points}} points")
input := Score{Player: "Alice", Points: 100}
result := lio(input)()
assert.Equal(t, input, result)
}
func TestPrintGoWithInvalidTemplate(t *testing.T) {
l := PrintGo[string]
// Invalid template syntax
lio := l("Value: {{.}")
// Should not panic even with invalid template
assert.NotPanics(t, func() { lio("test")() })
}
func TestLogGoInPipeline(t *testing.T) {
type Data struct {
Value int
}
input := Data{Value: 10}
result := F.Pipe2(
Of(input),
ChainFirst(LogGo[Data]("Processing: {{.Value}}")),
Map(func(d Data) Data {
return Data{Value: d.Value * 2}
}),
)()
assert.Equal(t, 20, result.Value)
}
func TestPrintGoInPipeline(t *testing.T) {
input := "hello"
result := F.Pipe2(
Of(input),
ChainFirst(PrintGo[string]("Input: {{.}}")),
Map(func(s string) string {
return s + " world"
}),
)()
assert.Equal(t, "hello world", result)
}

View File

@@ -29,6 +29,48 @@ var (
Create = ioeither.Eitherize1(os.Create)
// ReadFile reads the context of a file
ReadFile = ioeither.Eitherize1(os.ReadFile)
// Stat returns [FileInfo] object
Stat = ioeither.Eitherize1(os.Stat)
// UserCacheDir returns an [IOEither] that resolves to the default root directory
// to use for user-specific cached data. Users should create their own application-specific
// subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CACHE_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.cache.
// On Darwin, it returns $HOME/Library/Caches.
// On Windows, it returns %LocalAppData%.
// On Plan 9, it returns $home/lib/cache.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [E.Left].
UserCacheDir = ioeither.Eitherize0(os.UserCacheDir)()
// UserConfigDir returns an [IOEither] that resolves to the default root directory
// to use for user-specific configuration data. Users should create their own
// application-specific subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CONFIG_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.config.
// On Darwin, it returns $HOME/Library/Application Support.
// On Windows, it returns %AppData%.
// On Plan 9, it returns $home/lib.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [E.Left].
UserConfigDir = ioeither.Eitherize0(os.UserConfigDir)()
// UserHomeDir returns an [IOEither] that resolves to the current user's home directory.
//
// On Unix, including macOS, it returns the $HOME environment variable.
// On Windows, it returns %USERPROFILE%.
// On Plan 9, it returns the $home environment variable.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [E.Left].
UserHomeDir = ioeither.Eitherize0(os.UserHomeDir)()
)
// WriteFile writes a data blob to a file

80
v2/ioeither/file/read.go Normal file
View File

@@ -0,0 +1,80 @@
// 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 file
import (
"io"
"github.com/IBM/fp-go/v2/ioeither"
)
// Read uses a generator function to create a stream, reads data from it using a provided
// reader function, and ensures the stream is properly closed after reading.
//
// This function provides safe resource management for reading operations by:
// 1. Acquiring a ReadCloser resource using the provided acquire function
// 2. Applying a reader function to extract data from the resource
// 3. Ensuring the resource is closed, even if an error occurs during reading
//
// Type Parameters:
// - R: The type of data to be read from the stream
// - RD: The type of the ReadCloser resource (must implement io.ReadCloser)
//
// Parameters:
// - acquire: An IOEither that produces the ReadCloser resource
//
// Returns:
//
// A Kleisli function that takes a reader function (which transforms RD to R)
// and returns an IOEither that produces the read result R or an error.
//
// Example:
//
// import (
// "os"
// "io"
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/ioeither"
// "github.com/IBM/fp-go/v2/ioeither/file"
// )
//
// // Read first 10 bytes from a file
// readFirst10 := func(f *os.File) ioeither.IOEither[error, []byte] {
// return ioeither.TryCatchError(func() ([]byte, error) {
// buf := make([]byte, 10)
// n, err := f.Read(buf)
// return buf[:n], err
// })
// }
//
// result := F.Pipe1(
// file.Open("data.txt"),
// file.Read[[]byte, *os.File],
// )(readFirst10)
//
// data, err := result()
// if err != nil {
// log.Fatal(err)
// }
// fmt.Printf("Read: %s\n", data)
//
// The Read function ensures that the file is closed even if the reading operation fails,
// providing safe and composable resource management in a functional style.
func Read[R any, RD io.ReadCloser](acquire IOEither[error, RD]) Kleisli[error, Kleisli[error, RD, R], R] {
return ioeither.WithResource[R](
acquire,
Close[RD])
}

View File

@@ -0,0 +1,354 @@
// 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 file
import (
"errors"
"io"
"os"
"strings"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockReadCloser is a mock implementation of io.ReadCloser for testing
type mockReadCloser struct {
data []byte
readErr error
closeErr error
readPos int
closeCalled bool
}
func (m *mockReadCloser) Read(p []byte) (n int, err error) {
if m.readErr != nil {
return 0, m.readErr
}
if m.readPos >= len(m.data) {
return 0, io.EOF
}
n = copy(p, m.data[m.readPos:])
m.readPos += n
if m.readPos >= len(m.data) {
return n, io.EOF
}
return n, nil
}
func (m *mockReadCloser) Close() error {
m.closeCalled = true
return m.closeErr
}
// TestReadSuccessfulRead tests reading data successfully from a ReadCloser
func TestReadSuccessfulRead(t *testing.T) {
testData := []byte("Hello, World!")
mock := &mockReadCloser{data: testData}
// Create an acquire function that returns our mock
acquire := ioeither.Of[error](mock)
// Create a reader function that reads all data
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
return io.ReadAll(rc)
})
}
// Execute the Read operation
result := Read[[]byte](acquire)(reader)
either := result()
// Assertions
assert.True(t, E.IsRight(either))
data := E.GetOrElse(func(error) []byte { return nil })(either)
assert.Equal(t, testData, data)
assert.True(t, mock.closeCalled, "Close should have been called")
}
// TestReadWithRealFile tests reading from an actual file
func TestReadWithRealFile(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "read_test_*.txt")
require.NoError(t, err)
defer os.Remove(tmpFile.Name())
testContent := []byte("Test file content for Read function")
_, err = tmpFile.Write(testContent)
require.NoError(t, err)
tmpFile.Close()
// Use Read to read the file
reader := func(f *os.File) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
return io.ReadAll(f)
})
}
result := Read[[]byte](Open(tmpFile.Name()))(reader)
either := result()
assert.True(t, E.IsRight(either))
data := E.GetOrElse(func(error) []byte { return nil })(either)
assert.Equal(t, testContent, data)
}
// TestReadPartialRead tests reading only part of the data
func TestReadPartialRead(t *testing.T) {
testData := []byte("Hello, World! This is a longer message.")
mock := &mockReadCloser{data: testData}
acquire := ioeither.Of[error](mock)
// Reader that only reads first 13 bytes
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
buf := make([]byte, 13)
n, err := rc.Read(buf)
if err != nil && err != io.EOF {
return nil, err
}
return buf[:n], nil
})
}
result := Read[[]byte](acquire)(reader)
either := result()
assert.True(t, E.IsRight(either))
data := E.GetOrElse(func(error) []byte { return nil })(either)
assert.Equal(t, []byte("Hello, World!"), data)
assert.True(t, mock.closeCalled, "Close should have been called")
}
// TestReadErrorDuringRead tests that errors during reading are propagated
func TestReadErrorDuringRead(t *testing.T) {
readError := errors.New("read error")
mock := &mockReadCloser{
data: []byte("data"),
readErr: readError,
}
acquire := ioeither.Of[error](mock)
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
return io.ReadAll(rc)
})
}
result := Read[[]byte](acquire)(reader)
either := result()
assert.True(t, E.IsLeft(either))
err := E.Fold(func(e error) error { return e }, func([]byte) error { return nil })(either)
assert.Equal(t, readError, err)
assert.True(t, mock.closeCalled, "Close should be called even on read error")
}
// TestReadErrorDuringClose tests that errors during close are handled
func TestReadErrorDuringClose(t *testing.T) {
closeError := errors.New("close error")
mock := &mockReadCloser{
data: []byte("Hello"),
closeErr: closeError,
}
acquire := ioeither.Of[error](mock)
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
return io.ReadAll(rc)
})
}
result := Read[[]byte](acquire)(reader)
either := result()
// The close error should be propagated
assert.True(t, E.IsLeft(either))
assert.True(t, mock.closeCalled, "Close should have been called")
}
// TestReadErrorDuringAcquire tests that errors during resource acquisition are propagated
func TestReadErrorDuringAcquire(t *testing.T) {
acquireError := errors.New("acquire error")
acquire := ioeither.Left[*mockReadCloser](acquireError)
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
return io.ReadAll(rc)
})
}
result := Read[[]byte](acquire)(reader)
either := result()
assert.True(t, E.IsLeft(either))
err := E.Fold(func(e error) error { return e }, func([]byte) error { return nil })(either)
assert.Equal(t, acquireError, err)
}
// TestReadWithStringReader tests reading and transforming to a different type
func TestReadWithStringReader(t *testing.T) {
testData := []byte("Hello, World!")
mock := &mockReadCloser{data: testData}
acquire := ioeither.Of[error](mock)
// Reader that converts bytes to uppercase string
reader := func(rc *mockReadCloser) IOEither[error, string] {
return ioeither.TryCatchError(func() (string, error) {
data, err := io.ReadAll(rc)
if err != nil {
return "", err
}
return strings.ToUpper(string(data)), nil
})
}
result := Read[string](acquire)(reader)
either := result()
assert.True(t, E.IsRight(either))
str := E.GetOrElse(func(error) string { return "" })(either)
assert.Equal(t, "HELLO, WORLD!", str)
assert.True(t, mock.closeCalled, "Close should have been called")
}
// TestReadComposition tests composing Read with other operations
func TestReadComposition(t *testing.T) {
testData := []byte("42")
mock := &mockReadCloser{data: testData}
acquire := ioeither.Of[error](mock)
// Reader that parses the content as an integer
reader := func(rc *mockReadCloser) IOEither[error, int] {
return ioeither.TryCatchError(func() (int, error) {
data, err := io.ReadAll(rc)
if err != nil {
return 0, err
}
var num int
// Simple parsing
num = int(data[0]-'0')*10 + int(data[1]-'0')
return num, nil
})
}
result := F.Pipe1(
acquire,
Read[int],
)(reader)
either := result()
assert.True(t, E.IsRight(either))
num := E.GetOrElse(func(error) int { return 0 })(either)
assert.Equal(t, 42, num)
assert.True(t, mock.closeCalled, "Close should have been called")
}
// TestReadMultipleOperations tests that Read can be used multiple times
func TestReadMultipleOperations(t *testing.T) {
// Create a function that creates a new mock each time
createMock := func() *mockReadCloser {
return &mockReadCloser{data: []byte("test data")}
}
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
return io.ReadAll(rc)
})
}
// First read
mock1 := createMock()
result1 := Read[[]byte](ioeither.Of[error](mock1))(reader)
either1 := result1()
assert.True(t, E.IsRight(either1))
data1 := E.GetOrElse(func(error) []byte { return nil })(either1)
assert.Equal(t, []byte("test data"), data1)
assert.True(t, mock1.closeCalled)
// Second read with a new mock
mock2 := createMock()
result2 := Read[[]byte](ioeither.Of[error](mock2))(reader)
either2 := result2()
assert.True(t, E.IsRight(either2))
data2 := E.GetOrElse(func(error) []byte { return nil })(either2)
assert.Equal(t, []byte("test data"), data2)
assert.True(t, mock2.closeCalled)
}
// TestReadEnsuresCloseOnPanic tests that Close is called even if reader panics
// Note: This is more of a conceptual test as the actual panic handling depends on
// the implementation of WithResource
func TestReadWithEmptyData(t *testing.T) {
mock := &mockReadCloser{data: []byte{}}
acquire := ioeither.Of[error](mock)
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
return io.ReadAll(rc)
})
}
result := Read[[]byte](acquire)(reader)
either := result()
assert.True(t, E.IsRight(either))
data := E.GetOrElse(func(error) []byte { return nil })(either)
assert.Empty(t, data)
assert.True(t, mock.closeCalled, "Close should be called even with empty data")
}
// TestReadIntegrationWithEither tests integration with Either operations
func TestReadIntegrationWithEither(t *testing.T) {
testData := []byte("integration test")
mock := &mockReadCloser{data: testData}
acquire := ioeither.Of[error](mock)
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
return ioeither.TryCatchError(func() ([]byte, error) {
return io.ReadAll(rc)
})
}
result := Read[[]byte](acquire)(reader)
either := result()
// Test with Either operations
assert.True(t, E.IsRight(either))
folded := E.Fold(
func(err error) string { return "error: " + err.Error() },
func(data []byte) string { return "success: " + string(data) },
)(either)
assert.Equal(t, "success: integration test", folded)
assert.True(t, mock.closeCalled)
}

View File

@@ -31,7 +31,7 @@ import (
func TestBuilderWithQuery(t *testing.T) {
// add some query
withLimit := R.WithQueryArg("limit")("10")
withURL := R.WithUrl("http://www.example.org?a=b")
withURL := R.WithURL("http://www.example.org?a=b")
b := F.Pipe2(
R.Default,

View File

@@ -264,6 +264,11 @@ func GetOrElse[E, A any](onLeft func(E) IO[A]) func(IOEither[E, A]) IO[A] {
return eithert.GetOrElse(io.MonadChain[Either[E, A], A], io.MonadOf[A], onLeft)
}
// GetOrElseOf extracts the value or maps the error
func GetOrElseOf[E, A any](onLeft func(E) A) func(IOEither[E, A]) IO[A] {
return eithert.GetOrElseOf(io.MonadChain[Either[E, A], A], io.MonadOf[A], onLeft)
}
// MonadChainTo composes to the second monad ignoring the return value of the first
func MonadChainTo[A, E, B any](fa IOEither[E, A], fb IOEither[E, B]) IOEither[E, B] {
return MonadChain(fa, function.Constant1[A](fb))

View File

@@ -19,6 +19,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
func BenchmarkOf(b *testing.B) {
@@ -29,7 +30,7 @@ func BenchmarkOf(b *testing.B) {
func BenchmarkMap(b *testing.B) {
io := Of(42)
f := func(x int) int { return x * 2 }
f := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -68,9 +69,9 @@ func BenchmarkBind(b *testing.B) {
}
func BenchmarkPipeline(b *testing.B) {
f1 := func(x int) int { return x + 1 }
f2 := func(x int) int { return x * 2 }
f3 := func(x int) int { return x - 3 }
f1 := N.Add(1)
f2 := N.Mul(2)
f3 := N.Sub(3)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -93,9 +94,9 @@ func BenchmarkExecute(b *testing.B) {
}
func BenchmarkExecutePipeline(b *testing.B) {
f1 := func(x int) int { return x + 1 }
f2 := func(x int) int { return x * 2 }
f3 := func(x int) int { return x - 3 }
f1 := N.Add(1)
f2 := N.Mul(2)
f3 := N.Sub(3)
b.ResetTimer()
for i := 0; i < b.N; i++ {

View File

@@ -29,6 +29,48 @@ var (
Create = file.Create
// ReadFile reads the context of a file
ReadFile = file.ReadFile
// Stat returns [FileInfo] object
Stat = file.Stat
// UserCacheDir returns an [IOResult] that resolves to the default root directory
// to use for user-specific cached data. Users should create their own application-specific
// subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CACHE_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.cache.
// On Darwin, it returns $HOME/Library/Caches.
// On Windows, it returns %LocalAppData%.
// On Plan 9, it returns $home/lib/cache.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [Err].
UserCacheDir = file.UserCacheDir
// UserConfigDir returns an [IOResult] that resolves to the default root directory
// to use for user-specific configuration data. Users should create their own
// application-specific subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CONFIG_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.config.
// On Darwin, it returns $HOME/Library/Application Support.
// On Windows, it returns %AppData%.
// On Plan 9, it returns $home/lib.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [Err].
UserConfigDir = file.UserConfigDir
// UserHomeDir returns an [IOResult] that resolves to the current user's home directory.
//
// On Unix, including macOS, it returns the $HOME environment variable.
// On Windows, it returns %USERPROFILE%.
// On Plan 9, it returns the $home environment variable.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [Err].
UserHomeDir = file.UserHomeDir
)
// WriteFile writes a data blob to a file

111
v2/ioresult/file/read.go Normal file
View File

@@ -0,0 +1,111 @@
// 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 file
import (
"io"
"github.com/IBM/fp-go/v2/ioeither/file"
)
// Read uses a generator function to create a stream, reads data from it using a provided
// reader function, and ensures the stream is properly closed after reading.
//
// This function provides safe resource management for reading operations by:
// 1. Acquiring a ReadCloser resource using the provided acquire function
// 2. Applying a reader function to extract data from the resource
// 3. Ensuring the resource is closed, even if an error occurs during reading
//
// Type Parameters:
// - R: The type of data to be read from the stream
// - RD: The type of the ReadCloser resource (must implement io.ReadCloser)
//
// Parameters:
// - acquire: An IOResult that produces the ReadCloser resource
//
// Returns:
//
// A Kleisli function that takes a reader function (which transforms RD to R)
// and returns an IOResult that produces the read result R or an error.
//
// The key difference from ioeither.Read is that this returns IOResult[R] which is
// IO[Result[R]], representing a computation that returns a Result type (tuple of value and error)
// rather than an Either type.
//
// Example - Reading first N bytes from a file:
//
// import (
// "os"
// "io"
// F "github.com/IBM/fp-go/v2/function"
// R "github.com/IBM/fp-go/v2/result"
// "github.com/IBM/fp-go/v2/ioresult"
// "github.com/IBM/fp-go/v2/ioresult/file"
// )
//
// // Read first 10 bytes from a file
// readFirst10 := func(f *os.File) ioresult.IOResult[[]byte] {
// return ioresult.TryCatch(func() ([]byte, error) {
// buf := make([]byte, 10)
// n, err := f.Read(buf)
// return buf[:n], err
// })
// }
//
// result := F.Pipe1(
// file.Open("data.txt"),
// file.Read[[]byte, *os.File],
// )(readFirst10)
//
// // Execute the IO operation to get the Result
// res := result()
// data, err := res() // Result is a tuple function
// if err != nil {
// log.Fatal(err)
// }
// fmt.Printf("Read: %s\n", data)
//
// Example - Using with Result combinators:
//
// result := F.Pipe1(
// file.Open("config.json"),
// file.Read[[]byte, *os.File],
// )(readFirst10)
//
// // Chain operations using Result combinators
// processed := F.Pipe2(
// result,
// ioresult.Map(func(data []byte) string {
// return string(data)
// }),
// ioresult.ChainFirst(func(s string) ioresult.IOResult[any] {
// return ioresult.Of[any](fmt.Printf("Read: %s\n", s))
// }),
// )
//
// res := processed()
// str, err := res()
// if err != nil {
// log.Fatal(err)
// }
//
// The Read function ensures that the file is closed even if the reading operation fails,
// providing safe and composable resource management in a functional style.
//
//go:inline
func Read[R any, RD io.ReadCloser](acquire IOResult[RD]) Kleisli[Kleisli[RD, R], R] {
return file.Read[R](acquire)
}

View File

@@ -0,0 +1,64 @@
// 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 file_test
import (
"fmt"
"io"
"os"
FL "github.com/IBM/fp-go/v2/file"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/ioresult/file"
)
// Example_read_basicUsage demonstrates basic usage of the Read function
// to read data from a file with automatic resource cleanup.
func Example_read_basicUsage() {
// Create a temporary file for demonstration
tmpFile, err := os.CreateTemp("", "example-*.txt")
if err != nil {
fmt.Printf("Error creating temp file: %v\n", err)
return
}
defer os.Remove(tmpFile.Name())
// Write some test data
testData := "Hello, World! This is a test file."
if _, err := tmpFile.WriteString(testData); err != nil {
fmt.Printf("Error writing to temp file: %v\n", err)
return
}
tmpFile.Close()
// Define a reader function that reads the full file content
readAll := F.Flow2(
FL.ToReader[*os.File],
ioresult.Eitherize1(io.ReadAll),
)
content := F.Pipe2(
readAll,
file.Read[[]byte](file.Open(tmpFile.Name())),
ioresult.TapIOK(I.Printf[[]byte]("%s\n")),
)
content()
// Output: Hello, World! This is a test file.
}

View File

@@ -22,11 +22,110 @@ import (
)
var (
// CreateTemp created a temp file with proper parametrization
// CreateTemp creates a temporary file with proper parametrization.
// It is an alias for ioeither.file.CreateTemp which wraps os.CreateTemp
// in an IOResult context for functional composition.
//
// This function takes a directory and pattern parameter and returns an IOResult
// that produces a temporary file handle when executed.
//
// Parameters:
// - dir: directory where the temporary file should be created (empty string uses default temp dir)
// - pattern: filename pattern with optional '*' placeholder for random suffix
//
// Returns:
// IOResult[*os.File] that when executed creates and returns a temporary file handle
//
// Example:
// tempFile := CreateTemp("", "myapp-*.tmp")
// result := tempFile()
// file, err := E.UnwrapError(result)
// if err != nil {
// log.Fatal(err)
// }
// defer file.Close()
CreateTemp = file.CreateTemp
)
// WithTempFile creates a temporary filthen invokes a callback to create a resource based on the filthen close and remove the temp file
// WithTempFile creates a temporary file, then invokes a callback to create a resource
// based on the file, then automatically closes and removes the temp file.
//
// This function provides safe temporary file management by:
// 1. Creating a temporary file with sensible defaults
// 2. Passing the file handle to the provided callback function
// 3. Ensuring the file is closed and removed, even if the callback fails
//
// Type Parameters:
// - A: The type of result produced by the callback function
//
// Parameters:
// - f: A Kleisli function that takes a *os.File and returns an IOResult[A]
//
// Returns:
//
// IOResult[A] that when executed creates a temp file, runs the callback,
// and cleans up the file regardless of success or failure
//
// Example - Writing and reading from a temporary file:
//
// import (
// "io"
// "os"
// E "github.com/IBM/fp-go/v2/either"
// "github.com/IBM/fp-go/v2/ioresult"
// "github.com/IBM/fp-go/v2/ioresult/file"
// )
//
// // Write data to temp file and return the number of bytes written
// writeToTemp := func(f *os.File) ioresult.IOResult[int] {
// return ioresult.TryCatchError(func() (int, error) {
// data := []byte("Hello, temporary world!")
// return f.Write(data)
// })
// }
//
// result := file.WithTempFile(writeToTemp)
// bytesWritten, err := E.UnwrapError(result())
// if err != nil {
// log.Fatal(err)
// }
// fmt.Printf("Wrote %d bytes to temporary file\n", bytesWritten)
//
// Example - Processing data through a temporary file:
//
// processData := func(data []byte) ioresult.IOResult[string] {
// return file.WithTempFile(func(f *os.File) ioresult.IOResult[string] {
// return ioresult.TryCatchError(func() (string, error) {
// // Write data to temp file
// if _, err := f.Write(data); err != nil {
// return "", err
// }
//
// // Seek back to beginning
// if _, err := f.Seek(0, 0); err != nil {
// return "", err
// }
//
// // Read and process
// processed, err := io.ReadAll(f)
// if err != nil {
// return "", err
// }
//
// return strings.ToUpper(string(processed)), nil
// })
// })
// }
//
// result := processData([]byte("hello world"))
// output, err := E.UnwrapError(result())
// if err != nil {
// log.Fatal(err)
// }
// fmt.Println(output) // "HELLO WORLD"
//
// The temporary file is guaranteed to be cleaned up even if the callback function
// panics or returns an error, providing safe resource management in a functional style.
//
//go:inline
func WithTempFile[A any](f Kleisli[*os.File, A]) IOResult[A] {

View File

@@ -32,7 +32,6 @@ func TestCreateTemp(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, file)
tmpPath := file.Name()
@@ -49,7 +48,6 @@ func TestCreateTemp(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, file)
tmpPath := file.Name()
@@ -68,7 +66,6 @@ func TestCreateTemp(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, file)
tmpPath := file.Name()
@@ -95,7 +92,6 @@ func TestWithTempFile(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, testData, returnedData)
})
@@ -115,7 +111,6 @@ func TestWithTempFile(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, tmpPath, path)
_, statErr := os.Stat(tmpPath)
@@ -165,7 +160,6 @@ func TestWithTempFile(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, testContent, content)
})
@@ -182,9 +176,8 @@ func TestWithTempFile(t *testing.T) {
result := WithTempFile(useFile)()
path, err := E.UnwrapError(result)
assert.NoError(t, err)
assert.NoError(t, err)
paths = append(paths, path)
}
@@ -239,7 +232,6 @@ func TestWithTempFile(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, testData, returnedData)
})

View File

@@ -1,9 +1,88 @@
// 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 file
import "github.com/IBM/fp-go/v2/ioresult"
type (
IOResult[T any] = ioresult.IOResult[T]
Kleisli[A, B any] = ioresult.Kleisli[A, B]
// IOResult represents a synchronous computation that may fail, returning a Result type.
// It is an alias for ioresult.IOResult[T] which is equivalent to IO[Result[T]].
//
// IOResult[T] is a function that when executed returns Result[T], which is Either[error, T].
// This provides a functional approach to handling IO operations that may fail, with
// automatic resource management and composable error handling.
//
// Example:
// var readFile IOResult[[]byte] = func() Result[[]byte] {
// data, err := os.ReadFile("config.json")
// return result.TryCatchError(data, err)
// }
//
// // Execute the IO operation
// result := readFile()
// data, err := E.UnwrapError(result)
IOResult[T any] = ioresult.IOResult[T]
// Kleisli represents a function that takes a value of type A and returns an IOResult[B].
// It is an alias for ioresult.Kleisli[A, B] which is equivalent to Reader[A, IOResult[B]].
//
// Kleisli functions are the building blocks of monadic composition in the IOResult context.
// They allow for chaining operations that may fail while maintaining functional purity.
//
// Example:
// // A Kleisli function that reads from a file handle
// var readAll Kleisli[*os.File, []byte] = func(f *os.File) IOResult[[]byte] {
// return TryCatchError(func() ([]byte, error) {
// return io.ReadAll(f)
// })
// }
//
// // Can be composed with other Kleisli functions
// var processFile = F.Pipe1(
// file.Open("data.txt"),
// file.Read[[]byte, *os.File],
// )(readAll)
Kleisli[A, B any] = ioresult.Kleisli[A, B]
// Operator represents a function that transforms one IOResult into another.
// It is an alias for ioresult.Operator[A, B] which is equivalent to Kleisli[IOResult[A], B].
//
// Operators are used for transforming and composing IOResult values, providing a way
// to build complex data processing pipelines while maintaining error handling semantics.
//
// Example:
// // An operator that converts bytes to string
// var bytesToString Operator[[]byte, string] = Map(func(data []byte) string {
// return string(data)
// })
//
// // An operator that validates JSON
// var validateJSON Operator[string, map[string]interface{}] = ChainEitherK(
// func(s string) Result[map[string]interface{}] {
// var result map[string]interface{}
// err := json.Unmarshal([]byte(s), &result)
// return result.TryCatchError(result, err)
// },
// )
//
// // Compose operators in a pipeline
// var processJSON = F.Pipe2(
// readFileOperation,
// bytesToString,
// validateJSON,
// )
Operator[A, B any] = ioresult.Operator[A, B]
)

View File

@@ -286,6 +286,11 @@ func GetOrElse[A any](onLeft func(error) IO[A]) func(IOResult[A]) IO[A] {
return ioeither.GetOrElse(onLeft)
}
//go:inline
func GetOrElseOf[A any](onLeft func(error) A) func(IOResult[A]) IO[A] {
return ioeither.GetOrElseOf(onLeft)
}
// MonadChainTo composes to the second monad ignoring the return value of the first
//
//go:inline

View File

@@ -476,7 +476,7 @@ func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
//
// Example:
//
// fns := From(N.Mul(2), func(x int) int { return x + 10 })
// fns := From(N.Mul(2), N.Add(10))
// vals := From(5, 3)
// result := MonadAp(fns, vals)
// // yields: 10, 6, 15, 13 (each function applied to each value)
@@ -492,7 +492,7 @@ func MonadAp[B, A any](fab Seq[func(A) B], fa Seq[A]) Seq[B] {
// Example:
//
// applyTo5 := Ap(From(5))
// fns := From(N.Mul(2), func(x int) int { return x + 10 })
// fns := From(N.Mul(2), N.Add(10))
// result := applyTo5(fns)
// // yields: 10, 15
//
@@ -799,7 +799,7 @@ func FoldMapWithKey[K, A, B any](m M.Monoid[B]) func(func(K, A) B) func(Seq2[K,
//
// Example:
//
// fns := From(N.Mul(2), func(x int) int { return x + 10 })
// fns := From(N.Mul(2), N.Add(10))
// result := MonadFlap(fns, 5)
// // yields: 10, 15
//

View File

@@ -251,7 +251,7 @@ func TestFlatten(t *testing.T) {
func TestMonadAp(t *testing.T) {
fns := From(
N.Mul(2),
func(x int) int { return x + 10 },
N.Add(10),
)
vals := From(1, 2)
result := MonadAp(fns, vals)
@@ -261,7 +261,7 @@ func TestMonadAp(t *testing.T) {
func TestAp(t *testing.T) {
fns := From(
N.Mul(2),
func(x int) int { return x + 10 },
N.Add(10),
)
vals := From(1, 2)
applier := Ap[int](vals)
@@ -425,7 +425,7 @@ func TestFoldMapWithKey(t *testing.T) {
func TestMonadFlap(t *testing.T) {
fns := From(
N.Mul(2),
func(x int) int { return x + 10 },
N.Add(10),
)
result := MonadFlap(fns, 5)
assert.Equal(t, []int{10, 15}, toSlice(result))
@@ -434,7 +434,7 @@ func TestMonadFlap(t *testing.T) {
func TestFlap(t *testing.T) {
fns := From(
N.Mul(2),
func(x int) int { return x + 10 },
N.Add(10),
)
flapper := Flap[int](5)
result := toSlice(flapper(fns))
@@ -525,7 +525,7 @@ func TestPipelineComposition(t *testing.T) {
result := F.Pipe4(
From(1, 2, 3, 4, 5, 6),
Filter(func(x int) bool { return x%2 == 0 }),
Map(func(x int) int { return x * 10 }),
Map(N.Mul(10)),
Prepend(0),
toSlice[int],
)

View File

@@ -496,7 +496,7 @@ func TestMapComposition(t *testing.T) {
result := F.Pipe3(
Of(5),
Map(N.Mul(2)),
Map(func(x int) int { return x + 10 }),
Map(N.Add(10)),
Map(func(x int) int { return x }),
)

View File

@@ -239,7 +239,7 @@ func TestFromZeroWithCompose(t *testing.T) {
return O.MonadMap(opt, N.Mul(2))
},
func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x / 2 })
return O.MonadMap(opt, N.Div(2))
},
)

View File

@@ -17,6 +17,8 @@
package lens
import (
"fmt"
"github.com/IBM/fp-go/v2/endomorphism"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
@@ -597,3 +599,11 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[
return MakeLensCurried(F.Flow2(ea.Get, ab), F.Flow2(ba, ea.Set))
}
}
func (l Lens[S, T]) String() string {
return "Lens"
}
func (l Lens[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -557,7 +557,7 @@ func TestModifyLaws(t *testing.T) {
// Modify composition: Modify(f ∘ g) = Modify(f) ∘ Modify(g)
t.Run("ModifyComposition", func(t *testing.T) {
f := N.Mul(2)
g := func(x int) int { return x + 3 }
g := N.Add(3)
// Modify(f ∘ g)
composed := F.Flow2(g, f)

View File

@@ -274,7 +274,7 @@ func TestFromIsoModify(t *testing.T) {
t.Run("ModifyNoneToSome", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
// Map None to Some(10)
modified := L.Modify[Config](O.Map(func(x int) int { return x + 10 }))(optTimeoutLens)(config)
modified := L.Modify[Config](O.Map(N.Add(10)))(optTimeoutLens)(config)
// Since it was None, Map doesn't apply, stays None (0)
assert.Equal(t, 0, modified.timeout)
})

View File

@@ -31,7 +31,7 @@ type (
// by applying a function that takes and returns the same type.
//
// Example:
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// // increment is an Endomorphism[int]
Endomorphism[A any] = endomorphism.Endomorphism[A]

View File

@@ -18,6 +18,8 @@
package optional
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
@@ -27,6 +29,7 @@ import (
type Optional[S, A any] struct {
GetOption func(s S) O.Option[A]
Set func(a A) EM.Endomorphism[S]
name string
}
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
@@ -41,29 +44,42 @@ func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
// MakeOptional creates an Optional based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeOptionalRef`
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
//
//go:inline
func MakeOptional[S, A any](get func(S) O.Option[A], set func(S, A) S) Optional[S, A] {
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set)}
return MakeOptionalWithName(get, set, "GenericOptional")
}
func MakeOptionalWithName[S, A any](get func(S) O.Option[A], set func(S, A) S, name string) Optional[S, A] {
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(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
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
//
//go:inline
func MakeOptionalRef[S, A any](get func(*S) O.Option[A], set func(*S, A) *S) Optional[*S, A] {
return MakeOptional(get, setCopy(set))
}
//go:inline
func MakeOptionalRefWithName[S, A any](get func(*S) O.Option[A], set func(*S, A) *S, name string) Optional[*S, A] {
return MakeOptionalWithName(get, setCopy(set), name)
}
// Id returns am optional implementing the identity operation
func id[S any](creator func(get func(S) O.Option[S], set func(S, S) S) Optional[S, S]) Optional[S, S] {
return creator(O.Some[S], F.Second[S, S])
func idWithName[S any](creator func(get func(S) O.Option[S], set func(S, S) S, name string) Optional[S, S], name string) Optional[S, S] {
return creator(O.Some[S], F.Second[S, S], name)
}
// Id returns am optional implementing the identity operation
func Id[S any]() Optional[S, S] {
return id(MakeOptional[S, S])
return idWithName(MakeOptionalWithName[S, S], "Identity")
}
// Id returns am optional implementing the identity operation
func IdRef[S any]() Optional[*S, *S] {
return id(MakeOptionalRef[S, *S])
return idWithName(MakeOptionalRefWithName[S, *S], "Identity")
}
func optionalModifyOption[S, A any](f func(A) A, optional Optional[S, A], s S) O.Option[S] {
@@ -189,3 +205,11 @@ func IChainAny[S, A any]() func(Optional[S, any]) Optional[S, A] {
return ichain(sa, fromAny, toAny)
}
}
func (l Optional[S, T]) String() string {
return l.name
}
func (l Optional[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -33,7 +33,7 @@ func AsOptional[S, A any](sa P.Prism[S, A]) OPT.Optional[S, A] {
}
func PrismSome[A any]() P.Prism[O.Option[A], A] {
return P.MakePrism(F.Identity[O.Option[A]], O.Some[A])
return P.MakePrismWithName(F.Identity[O.Option[A]], O.Some[A], "PrismSome")
}
// Some returns a `Optional` from a `Optional` focused on the `Some` of a `Option` type.

View File

@@ -16,6 +16,8 @@
package prism
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
@@ -48,33 +50,19 @@ type (
// },
// func(v int) Result { return Success{Value: v} },
// )
Prism[S, A any] interface {
Prism[S, A any] struct {
// GetOption attempts to extract a value of type A from S.
// Returns Some(a) if the extraction succeeds, None otherwise.
GetOption(s S) Option[A]
GetOption O.Kleisli[S, A]
// ReverseGet constructs an S from an A.
// This operation always succeeds.
ReverseGet(a A) S
}
ReverseGet func(A) S
// prismImpl is the internal implementation of the Prism interface.
prismImpl[S, A any] struct {
get func(S) Option[A]
rev func(A) S
name string
}
)
// GetOption implements the Prism interface for prismImpl.
func (prism prismImpl[S, A]) GetOption(s S) Option[A] {
return prism.get(s)
}
// ReverseGet implements the Prism interface for prismImpl.
func (prism prismImpl[S, A]) ReverseGet(a A) S {
return prism.rev(a)
}
// MakePrism constructs a Prism from GetOption and ReverseGet functions.
//
// Parameters:
@@ -90,8 +78,15 @@ func (prism prismImpl[S, A]) ReverseGet(a A) S {
// func(opt Option[int]) Option[int] { return opt },
// func(n int) Option[int] { return Some(n) },
// )
//
//go:inline
func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
return prismImpl[S, A]{get, rev}
return MakePrismWithName(get, rev, "GenericPrism")
}
//go:inline
func MakePrismWithName[S, A any](get func(S) Option[A], rev func(A) S, name string) Prism[S, A] {
return Prism[S, A]{get, rev, name}
}
// Id returns an identity prism that focuses on the entire value.
@@ -106,7 +101,7 @@ func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
// value := idPrism.GetOption(42) // Some(42)
// result := idPrism.ReverseGet(42) // 42
func Id[S any]() Prism[S, S] {
return MakePrism(O.Some[S], F.Identity[S])
return MakePrismWithName(O.Some[S], F.Identity[S], "PrismIdentity")
}
// FromPredicate creates a prism that matches values satisfying a predicate.
@@ -125,7 +120,7 @@ func Id[S any]() Prism[S, S] {
// value := positivePrism.GetOption(42) // Some(42)
// value = positivePrism.GetOption(-5) // None[int]
func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
return MakePrism(O.FromPredicate(pred), F.Identity[S])
return MakePrismWithName(O.FromPredicate(pred), F.Identity[S], "PrismWithPredicate")
}
// Compose composes two prisms to create a prism that focuses deeper into a structure.
@@ -149,13 +144,15 @@ func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
// composed := Compose[Outer](innerPrism)(outerPrism) // Prism[Outer, Value]
func Compose[S, A, B any](ab Prism[A, B]) func(Prism[S, A]) Prism[S, B] {
return func(sa Prism[S, A]) Prism[S, B] {
return MakePrism(F.Flow2(
return MakePrismWithName(F.Flow2(
sa.GetOption,
O.Chain(ab.GetOption),
), F.Flow2(
ab.ReverseGet,
sa.ReverseGet,
))
),
fmt.Sprintf("PrismCompose[%s x %s]", ab, sa),
)
}
}
@@ -213,7 +210,7 @@ func Set[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
// prismSome creates a prism that focuses on the Some variant of an Option.
// This is an internal helper used by the Some function.
func prismSome[A any]() Prism[Option[A], A] {
return MakePrism(F.Identity[Option[A]], O.Some[A])
return MakePrismWithName(F.Identity[Option[A]], O.Some[A], "PrismSome")
}
// Some creates a prism that focuses on the Some variant of an Option within a structure.
@@ -242,9 +239,10 @@ func Some[S, A any](soa Prism[S, Option[A]]) Prism[S, A] {
// imap is an internal helper that bidirectionally maps a prism's focus type.
func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB, ba BA) Prism[S, B] {
return MakePrism(
return MakePrismWithName(
F.Flow2(sa.GetOption, O.Map(ab)),
F.Flow2(ba, sa.ReverseGet),
fmt.Sprintf("PrismIMap[%s]", sa),
)
}
@@ -278,3 +276,11 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Pris
return imap(sa, ab, ba)
}
}
func (l Prism[S, T]) String() string {
return "Prism"
}
func (l Prism[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -17,8 +17,10 @@ package prism
import (
"encoding/base64"
"fmt"
"net/url"
"regexp"
"strconv"
"time"
"github.com/IBM/fp-go/v2/either"
@@ -67,10 +69,12 @@ import (
// - Validating and transforming base64 data in pipelines
// - Using different encodings (Standard, URL-safe, RawStd, RawURL)
func FromEncoding(enc *base64.Encoding) Prism[string, []byte] {
return MakePrism(F.Flow2(
return MakePrismWithName(F.Flow2(
either.Eitherize1(enc.DecodeString),
either.Fold(F.Ignore1of1[error](option.None[[]byte]), option.Some),
), enc.EncodeToString)
), enc.EncodeToString,
"PrismFromEncoding",
)
}
// ParseURL creates a prism for parsing and formatting URLs.
@@ -114,10 +118,12 @@ func FromEncoding(enc *base64.Encoding) Prism[string, []byte] {
// - Transforming URL strings in data pipelines
// - Extracting and modifying URL components safely
func ParseURL() Prism[string, *url.URL] {
return MakePrism(F.Flow2(
return MakePrismWithName(F.Flow2(
either.Eitherize1(url.Parse),
either.Fold(F.Ignore1of1[error](option.None[*url.URL]), option.Some),
), (*url.URL).String)
), (*url.URL).String,
"PrismParseURL",
)
}
// InstanceOf creates a prism for type assertions on interface{}/any values.
@@ -161,7 +167,8 @@ func ParseURL() Prism[string, *url.URL] {
// - Type-safe deserialization and validation
// - Pattern matching on interface{} values
func InstanceOf[T any]() Prism[any, T] {
return MakePrism(option.ToType[T], F.ToAny[T])
var t T
return MakePrismWithName(option.ToType[T], F.ToAny[T], fmt.Sprintf("PrismInstanceOf[%T]", t))
}
// ParseDate creates a prism for parsing and formatting dates with a specific layout.
@@ -212,10 +219,12 @@ func InstanceOf[T any]() Prism[any, T] {
// - Converting between date formats
// - Safely handling user-provided date inputs
func ParseDate(layout string) Prism[string, time.Time] {
return MakePrism(F.Flow2(
return MakePrismWithName(F.Flow2(
F.Bind1st(either.Eitherize2(time.Parse), layout),
either.Fold(F.Ignore1of1[error](option.None[time.Time]), option.Some),
), F.Bind2nd(time.Time.Format, layout))
), F.Bind2nd(time.Time.Format, layout),
"PrismParseDate",
)
}
// Deref creates a prism for safely dereferencing pointers.
@@ -263,7 +272,7 @@ func ParseDate(layout string) Prism[string, time.Time] {
// - Filtering out nil values in data pipelines
// - Working with database nullable columns
func Deref[T any]() Prism[*T, *T] {
return MakePrism(option.FromNillable[T], F.Identity[*T])
return MakePrismWithName(option.FromNillable[T], F.Identity[*T], "PrismDeref")
}
// FromEither creates a prism for extracting Right values from Either types.
@@ -309,7 +318,7 @@ func Deref[T any]() Prism[*T, *T] {
// - Working with fallible operations
// - Composing with other prisms for complex error handling
func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrism(either.ToOption[E, T], either.Of[E, T])
return MakePrismWithName(either.ToOption[E, T], either.Of[E, T], "PrismFromEither")
}
// FromZero creates a prism that matches zero values of comparable types.
@@ -352,11 +361,50 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
// - Working with optional fields that use zero as "not set"
// - Replacing zero values with defaults
func FromZero[T comparable]() Prism[T, T] {
return MakePrism(option.FromZero[T](), F.Identity[T])
return MakePrismWithName(option.FromZero[T](), F.Identity[T], "PrismFromZero")
}
// FromNonZero creates a prism that matches non-zero values of comparable types.
// It provides a safe way to work with non-zero values, handling zero values
// gracefully through the Option type.
//
// The prism's GetOption returns Some(t) if the value is not equal to the zero value
// of type T; otherwise, it returns None.
//
// The prism's ReverseGet is the identity function, returning the value unchanged.
//
// Type Parameters:
// - T: A comparable type (must support == and != operators)
//
// Returns:
// - A Prism[T, T] that matches non-zero values
//
// Example:
//
// // Create a prism for non-zero integers
// nonZeroPrism := FromNonZero[int]()
//
// // Match non-zero value
// result := nonZeroPrism.GetOption(42) // Some(42)
//
// // Zero returns None
// result = nonZeroPrism.GetOption(0) // None[int]()
//
// // ReverseGet is identity
// value := nonZeroPrism.ReverseGet(42) // 42
//
// // Use with Set to update non-zero values
// setter := Set[int, int](100)
// result := setter(nonZeroPrism)(42) // 100
// result = setter(nonZeroPrism)(0) // 0 (unchanged)
//
// Common use cases:
// - Validating that values are non-zero/non-default
// - Filtering non-zero values in data pipelines
// - Working with required fields that shouldn't be zero
// - Replacing non-zero values with new values
func FromNonZero[T comparable]() Prism[T, T] {
return MakePrism(option.FromNonZero[T](), F.Identity[T])
return MakePrismWithName(option.FromNonZero[T](), F.Identity[T], "PrismFromNonZero")
}
// Match represents a regex match result with full reconstruction capability.
@@ -495,7 +543,7 @@ func (m Match) Group(n int) string {
func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
noMatch := option.None[Match]()
return MakePrism(
return MakePrismWithName(
// String -> Option[Match]
func(s string) Option[Match] {
loc := re.FindStringSubmatchIndex(s)
@@ -522,6 +570,7 @@ func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
return option.Some(match)
},
Match.Reconstruct,
fmt.Sprintf("PrismRegex[%s]", re),
)
}
@@ -660,3 +709,259 @@ func RegexNamedMatcher(re *regexp.Regexp) Prism[string, NamedMatch] {
NamedMatch.Reconstruct,
)
}
func getFromEither[A, B any](f func(A) (B, error)) func(A) Option[B] {
return func(a A) Option[B] {
b, err := f(a)
if err != nil {
return option.None[B]()
}
return option.Of(b)
}
}
func atoi64(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
}
func itoa64(i int64) string {
return strconv.FormatInt(i, 10)
}
// ParseInt creates a prism for parsing and formatting integers.
// It provides a safe way to convert between string and int, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into an int.
// If parsing succeeds, it returns Some(int); if it fails (e.g., invalid
// number format), it returns None.
//
// The prism's ReverseGet always succeeds, converting an int to its string representation.
//
// Returns:
// - A Prism[string, int] that safely handles int parsing/formatting
//
// Example:
//
// // Create an int parsing prism
// intPrism := ParseInt()
//
// // Parse valid integer
// parsed := intPrism.GetOption("42") // Some(42)
//
// // Parse invalid integer
// invalid := intPrism.GetOption("not-a-number") // None[int]()
//
// // Format int to string
// str := intPrism.ReverseGet(42) // "42"
//
// // Use with Set to update integer values
// setter := Set[string, int](100)
// result := setter(intPrism)("42") // "100"
//
// Common use cases:
// - Parsing integer configuration values
// - Validating numeric user input
// - Converting between string and int in data pipelines
// - Working with numeric API parameters
//
//go:inline
func ParseInt() Prism[string, int] {
return MakePrismWithName(getFromEither(strconv.Atoi), strconv.Itoa, "PrismParseInt")
}
// ParseInt64 creates a prism for parsing and formatting 64-bit integers.
// It provides a safe way to convert between string and int64, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into an int64.
// If parsing succeeds, it returns Some(int64); if it fails (e.g., invalid
// number format or overflow), it returns None.
//
// The prism's ReverseGet always succeeds, converting an int64 to its string representation.
//
// Returns:
// - A Prism[string, int64] that safely handles int64 parsing/formatting
//
// Example:
//
// // Create an int64 parsing prism
// int64Prism := ParseInt64()
//
// // Parse valid 64-bit integer
// parsed := int64Prism.GetOption("9223372036854775807") // Some(9223372036854775807)
//
// // Parse invalid integer
// invalid := int64Prism.GetOption("not-a-number") // None[int64]()
//
// // Format int64 to string
// str := int64Prism.ReverseGet(int64(42)) // "42"
//
// // Use with Set to update int64 values
// setter := Set[string, int64](int64(100))
// result := setter(int64Prism)("42") // "100"
//
// Common use cases:
// - Parsing large integer values (timestamps, IDs)
// - Working with database integer columns
// - Handling 64-bit numeric API parameters
// - Converting between string and int64 in data pipelines
//
//go:inline
func ParseInt64() Prism[string, int64] {
return MakePrismWithName(getFromEither(atoi64), itoa64, "PrismParseInt64")
}
// ParseBool creates a prism for parsing and formatting boolean values.
// It provides a safe way to convert between string and bool, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into a bool.
// It accepts "1", "t", "T", "TRUE", "true", "True", "0", "f", "F", "FALSE", "false", "False".
// If parsing succeeds, it returns Some(bool); if it fails, it returns None.
//
// The prism's ReverseGet always succeeds, converting a bool to "true" or "false".
//
// Returns:
// - A Prism[string, bool] that safely handles bool parsing/formatting
//
// Example:
//
// // Create a bool parsing prism
// boolPrism := ParseBool()
//
// // Parse valid boolean strings
// parsed := boolPrism.GetOption("true") // Some(true)
// parsed = boolPrism.GetOption("1") // Some(true)
// parsed = boolPrism.GetOption("false") // Some(false)
// parsed = boolPrism.GetOption("0") // Some(false)
//
// // Parse invalid boolean
// invalid := boolPrism.GetOption("maybe") // None[bool]()
//
// // Format bool to string
// str := boolPrism.ReverseGet(true) // "true"
// str = boolPrism.ReverseGet(false) // "false"
//
// // Use with Set to update boolean values
// setter := Set[string, bool](true)
// result := setter(boolPrism)("false") // "true"
//
// Common use cases:
// - Parsing boolean configuration values
// - Validating boolean user input
// - Converting between string and bool in data pipelines
// - Working with boolean API parameters or flags
//
//go:inline
func ParseBool() Prism[string, bool] {
return MakePrismWithName(getFromEither(strconv.ParseBool), strconv.FormatBool, "PrismParseBool")
}
func atof64(s string) (float64, error) {
return strconv.ParseFloat(s, 64)
}
func atof32(s string) (float32, error) {
f32, err := strconv.ParseFloat(s, 32)
if err != nil {
return 0, err
}
return float32(f32), nil
}
func f32toa(f float32) string {
return strconv.FormatFloat(float64(f), 'g', -1, 32)
}
func f64toa(f float64) string {
return strconv.FormatFloat(f, 'g', -1, 64)
}
// ParseFloat32 creates a prism for parsing and formatting 32-bit floating-point numbers.
// It provides a safe way to convert between string and float32, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into a float32.
// If parsing succeeds, it returns Some(float32); if it fails (e.g., invalid
// number format or overflow), it returns None.
//
// The prism's ReverseGet always succeeds, converting a float32 to its string representation
// using the 'g' format (shortest representation).
//
// Returns:
// - A Prism[string, float32] that safely handles float32 parsing/formatting
//
// Example:
//
// // Create a float32 parsing prism
// float32Prism := ParseFloat32()
//
// // Parse valid float
// parsed := float32Prism.GetOption("3.14") // Some(3.14)
// parsed = float32Prism.GetOption("1.5e10") // Some(1.5e10)
//
// // Parse invalid float
// invalid := float32Prism.GetOption("not-a-number") // None[float32]()
//
// // Format float32 to string
// str := float32Prism.ReverseGet(float32(3.14)) // "3.14"
//
// // Use with Set to update float32 values
// setter := Set[string, float32](float32(2.71))
// result := setter(float32Prism)("3.14") // "2.71"
//
// Common use cases:
// - Parsing floating-point configuration values
// - Working with scientific notation
// - Converting between string and float32 in data pipelines
// - Handling numeric API parameters with decimal precision
//
//go:inline
func ParseFloat32() Prism[string, float32] {
return MakePrismWithName(getFromEither(atof32), f32toa, "ParseFloat32")
}
// ParseFloat64 creates a prism for parsing and formatting 64-bit floating-point numbers.
// It provides a safe way to convert between string and float64, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into a float64.
// If parsing succeeds, it returns Some(float64); if it fails (e.g., invalid
// number format or overflow), it returns None.
//
// The prism's ReverseGet always succeeds, converting a float64 to its string representation
// using the 'g' format (shortest representation).
//
// Returns:
// - A Prism[string, float64] that safely handles float64 parsing/formatting
//
// Example:
//
// // Create a float64 parsing prism
// float64Prism := ParseFloat64()
//
// // Parse valid float
// parsed := float64Prism.GetOption("3.141592653589793") // Some(3.141592653589793)
// parsed = float64Prism.GetOption("1.5e100") // Some(1.5e100)
//
// // Parse invalid float
// invalid := float64Prism.GetOption("not-a-number") // None[float64]()
//
// // Format float64 to string
// str := float64Prism.ReverseGet(3.141592653589793) // "3.141592653589793"
//
// // Use with Set to update float64 values
// setter := Set[string, float64](2.718281828459045)
// result := setter(float64Prism)("3.14") // "2.718281828459045"
//
// Common use cases:
// - Parsing high-precision floating-point values
// - Working with scientific notation and large numbers
// - Converting between string and float64 in data pipelines
// - Handling precise numeric API parameters
//
//go:inline
func ParseFloat64() Prism[string, float64] {
return MakePrismWithName(getFromEither(atof64), f64toa, "PrismParseFloat64")
}

View File

@@ -532,3 +532,411 @@ func TestRegexNamedMatcherWithSet(t *testing.T) {
assert.Equal(t, original, result)
})
}
// TestFromNonZero tests the FromNonZero prism with various comparable types
func TestFromNonZero(t *testing.T) {
t.Run("int - match non-zero", func(t *testing.T) {
prism := FromNonZero[int]()
result := prism.GetOption(42)
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("int - zero returns None", func(t *testing.T) {
prism := FromNonZero[int]()
result := prism.GetOption(0)
assert.True(t, O.IsNone(result))
})
t.Run("string - match non-empty string", func(t *testing.T) {
prism := FromNonZero[string]()
result := prism.GetOption("hello")
assert.True(t, O.IsSome(result))
assert.Equal(t, "hello", O.GetOrElse(F.Constant("default"))(result))
})
t.Run("string - empty returns None", func(t *testing.T) {
prism := FromNonZero[string]()
result := prism.GetOption("")
assert.True(t, O.IsNone(result))
})
t.Run("bool - match true", func(t *testing.T) {
prism := FromNonZero[bool]()
result := prism.GetOption(true)
assert.True(t, O.IsSome(result))
assert.True(t, O.GetOrElse(F.Constant(false))(result))
})
t.Run("bool - false returns None", func(t *testing.T) {
prism := FromNonZero[bool]()
result := prism.GetOption(false)
assert.True(t, O.IsNone(result))
})
t.Run("float64 - match non-zero", func(t *testing.T) {
prism := FromNonZero[float64]()
result := prism.GetOption(3.14)
assert.True(t, O.IsSome(result))
assert.Equal(t, 3.14, O.GetOrElse(F.Constant(-1.0))(result))
})
t.Run("float64 - zero returns None", func(t *testing.T) {
prism := FromNonZero[float64]()
result := prism.GetOption(0.0)
assert.True(t, O.IsNone(result))
})
t.Run("pointer - match non-nil", func(t *testing.T) {
prism := FromNonZero[*int]()
value := 42
result := prism.GetOption(&value)
assert.True(t, O.IsSome(result))
})
t.Run("pointer - nil returns None", func(t *testing.T) {
prism := FromNonZero[*int]()
var nilPtr *int
result := prism.GetOption(nilPtr)
assert.True(t, O.IsNone(result))
})
t.Run("reverse get is identity", func(t *testing.T) {
prism := FromNonZero[int]()
assert.Equal(t, 0, prism.ReverseGet(0))
assert.Equal(t, 42, prism.ReverseGet(42))
})
}
// TestFromNonZeroWithSet tests using Set with FromNonZero prism
func TestFromNonZeroWithSet(t *testing.T) {
t.Run("set on non-zero value", func(t *testing.T) {
prism := FromNonZero[int]()
setter := Set[int](100)
result := setter(prism)(42)
assert.Equal(t, 100, result)
})
t.Run("set on zero returns original", func(t *testing.T) {
prism := FromNonZero[int]()
setter := Set[int](100)
result := setter(prism)(0)
assert.Equal(t, 0, result)
})
}
// TestParseInt tests the ParseInt prism
func TestParseInt(t *testing.T) {
prism := ParseInt()
t.Run("parse valid positive integer", func(t *testing.T) {
result := prism.GetOption("42")
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("parse valid negative integer", func(t *testing.T) {
result := prism.GetOption("-123")
assert.True(t, O.IsSome(result))
assert.Equal(t, -123, O.GetOrElse(F.Constant(0))(result))
})
t.Run("parse zero", func(t *testing.T) {
result := prism.GetOption("0")
assert.True(t, O.IsSome(result))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("parse invalid integer", func(t *testing.T) {
result := prism.GetOption("not-a-number")
assert.True(t, O.IsNone(result))
})
t.Run("parse float as integer fails", func(t *testing.T) {
result := prism.GetOption("3.14")
assert.True(t, O.IsNone(result))
})
t.Run("parse empty string fails", func(t *testing.T) {
result := prism.GetOption("")
assert.True(t, O.IsNone(result))
})
t.Run("reverse get formats integer", func(t *testing.T) {
assert.Equal(t, "42", prism.ReverseGet(42))
assert.Equal(t, "-123", prism.ReverseGet(-123))
assert.Equal(t, "0", prism.ReverseGet(0))
})
t.Run("round trip", func(t *testing.T) {
original := "12345"
result := prism.GetOption(original)
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(0))(result)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, original, reconstructed)
}
})
}
// TestParseInt64 tests the ParseInt64 prism
func TestParseInt64(t *testing.T) {
prism := ParseInt64()
t.Run("parse valid int64", func(t *testing.T) {
result := prism.GetOption("9223372036854775807")
assert.True(t, O.IsSome(result))
assert.Equal(t, int64(9223372036854775807), O.GetOrElse(F.Constant(int64(-1)))(result))
})
t.Run("parse negative int64", func(t *testing.T) {
result := prism.GetOption("-9223372036854775808")
assert.True(t, O.IsSome(result))
assert.Equal(t, int64(-9223372036854775808), O.GetOrElse(F.Constant(int64(0)))(result))
})
t.Run("parse invalid int64", func(t *testing.T) {
result := prism.GetOption("not-a-number")
assert.True(t, O.IsNone(result))
})
t.Run("reverse get formats int64", func(t *testing.T) {
assert.Equal(t, "42", prism.ReverseGet(int64(42)))
assert.Equal(t, "9223372036854775807", prism.ReverseGet(int64(9223372036854775807)))
})
t.Run("round trip", func(t *testing.T) {
original := "1234567890123456789"
result := prism.GetOption(original)
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(int64(0)))(result)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, original, reconstructed)
}
})
}
// TestParseBool tests the ParseBool prism
func TestParseBool(t *testing.T) {
prism := ParseBool()
t.Run("parse true variations", func(t *testing.T) {
trueValues := []string{"true", "True", "TRUE", "t", "T", "1"}
for _, val := range trueValues {
result := prism.GetOption(val)
assert.True(t, O.IsSome(result), "Should parse: %s", val)
assert.True(t, O.GetOrElse(F.Constant(false))(result), "Should be true: %s", val)
}
})
t.Run("parse false variations", func(t *testing.T) {
falseValues := []string{"false", "False", "FALSE", "f", "F", "0"}
for _, val := range falseValues {
result := prism.GetOption(val)
assert.True(t, O.IsSome(result), "Should parse: %s", val)
assert.False(t, O.GetOrElse(F.Constant(true))(result), "Should be false: %s", val)
}
})
t.Run("parse invalid bool", func(t *testing.T) {
invalidValues := []string{"maybe", "yes", "no", "2", ""}
for _, val := range invalidValues {
result := prism.GetOption(val)
assert.True(t, O.IsNone(result), "Should not parse: %s", val)
}
})
t.Run("reverse get formats bool", func(t *testing.T) {
assert.Equal(t, "true", prism.ReverseGet(true))
assert.Equal(t, "false", prism.ReverseGet(false))
})
t.Run("round trip with true", func(t *testing.T) {
result := prism.GetOption("true")
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(false))(result)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, "true", reconstructed)
}
})
t.Run("round trip with false", func(t *testing.T) {
result := prism.GetOption("false")
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(true))(result)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, "false", reconstructed)
}
})
}
// TestParseFloat32 tests the ParseFloat32 prism
func TestParseFloat32(t *testing.T) {
prism := ParseFloat32()
t.Run("parse valid float32", func(t *testing.T) {
result := prism.GetOption("3.14")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(float32(0)))(result)
assert.InDelta(t, float32(3.14), value, 0.0001)
})
t.Run("parse negative float32", func(t *testing.T) {
result := prism.GetOption("-2.71")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(float32(0)))(result)
assert.InDelta(t, float32(-2.71), value, 0.0001)
})
t.Run("parse scientific notation", func(t *testing.T) {
result := prism.GetOption("1.5e10")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(float32(0)))(result)
assert.InDelta(t, float32(1.5e10), value, 1e6)
})
t.Run("parse integer as float", func(t *testing.T) {
result := prism.GetOption("42")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(float32(0)))(result)
assert.Equal(t, float32(42), value)
})
t.Run("parse invalid float", func(t *testing.T) {
result := prism.GetOption("not-a-number")
assert.True(t, O.IsNone(result))
})
t.Run("reverse get formats float32", func(t *testing.T) {
str := prism.ReverseGet(float32(3.14))
assert.Contains(t, str, "3.14")
})
t.Run("round trip", func(t *testing.T) {
original := "3.14159"
result := prism.GetOption(original)
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(float32(0)))(result)
reconstructed := prism.ReverseGet(value)
// Parse both to compare as floats due to precision
origFloat := F.Pipe1(original, prism.GetOption)
reconFloat := F.Pipe1(reconstructed, prism.GetOption)
if O.IsSome(origFloat) && O.IsSome(reconFloat) {
assert.InDelta(t,
O.GetOrElse(F.Constant(float32(0)))(origFloat),
O.GetOrElse(F.Constant(float32(0)))(reconFloat),
0.0001)
}
}
})
}
// TestParseFloat64 tests the ParseFloat64 prism
func TestParseFloat64(t *testing.T) {
prism := ParseFloat64()
t.Run("parse valid float64", func(t *testing.T) {
result := prism.GetOption("3.141592653589793")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0.0))(result)
assert.InDelta(t, 3.141592653589793, value, 1e-15)
})
t.Run("parse negative float64", func(t *testing.T) {
result := prism.GetOption("-2.718281828459045")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0.0))(result)
assert.InDelta(t, -2.718281828459045, value, 1e-15)
})
t.Run("parse scientific notation", func(t *testing.T) {
result := prism.GetOption("1.5e100")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0.0))(result)
assert.InDelta(t, 1.5e100, value, 1e85)
})
t.Run("parse integer as float", func(t *testing.T) {
result := prism.GetOption("42")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0.0))(result)
assert.Equal(t, 42.0, value)
})
t.Run("parse invalid float", func(t *testing.T) {
result := prism.GetOption("not-a-number")
assert.True(t, O.IsNone(result))
})
t.Run("reverse get formats float64", func(t *testing.T) {
str := prism.ReverseGet(3.141592653589793)
assert.Contains(t, str, "3.14159")
})
t.Run("round trip", func(t *testing.T) {
original := "3.141592653589793"
result := prism.GetOption(original)
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(0.0))(result)
reconstructed := prism.ReverseGet(value)
// Parse both to compare as floats
origFloat := F.Pipe1(original, prism.GetOption)
reconFloat := F.Pipe1(reconstructed, prism.GetOption)
if O.IsSome(origFloat) && O.IsSome(reconFloat) {
assert.InDelta(t,
O.GetOrElse(F.Constant(0.0))(origFloat),
O.GetOrElse(F.Constant(0.0))(reconFloat),
1e-15)
}
}
})
}
// TestParseIntWithSet tests using Set with ParseInt prism
func TestParseIntWithSet(t *testing.T) {
prism := ParseInt()
t.Run("set on valid integer string", func(t *testing.T) {
setter := Set[string](100)
result := setter(prism)("42")
assert.Equal(t, "100", result)
})
t.Run("set on invalid string returns original", func(t *testing.T) {
setter := Set[string](100)
result := setter(prism)("not-a-number")
assert.Equal(t, "not-a-number", result)
})
}
// TestParseBoolWithSet tests using Set with ParseBool prism
func TestParseBoolWithSet(t *testing.T) {
prism := ParseBool()
t.Run("set on valid bool string", func(t *testing.T) {
setter := Set[string](true)
result := setter(prism)("false")
assert.Equal(t, "true", result)
})
t.Run("set on invalid string returns original", func(t *testing.T) {
setter := Set[string](true)
result := setter(prism)("maybe")
assert.Equal(t, "maybe", result)
})
}

View File

@@ -17,6 +17,8 @@ package option
import (
"testing"
N "github.com/IBM/fp-go/v2/number"
)
// Benchmark basic construction
@@ -46,7 +48,7 @@ func BenchmarkIsSome(b *testing.B) {
func BenchmarkMap(b *testing.B) {
opt := Some(21)
mapper := Map(func(x int) int { return x * 2 })
mapper := Map(N.Mul(2))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {

View File

@@ -17,6 +17,8 @@ package option
import (
"testing"
N "github.com/IBM/fp-go/v2/number"
)
// Benchmark shallow chain (1 step)
@@ -121,11 +123,11 @@ func BenchmarkMap_5Steps(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Map(func(x int) int { return x - 10 })(
Map(func(x int) int { return x / 2 })(
Map(func(x int) int { return x + 20 })(
Map(func(x int) int { return x * 3 })(
Map(func(x int) int { return x + 1 })(opt),
_ = Map(N.Sub(10))(
Map(N.Div(2))(
Map(N.Add(20))(
Map(N.Mul(3))(
Map(N.Add(1))(opt),
),
),
),

View File

@@ -289,11 +289,11 @@ func Read[A, E any](e E) func(Reader[E, A]) A {
//
// type Config struct { Multiplier int }
// getMultiplier := func(c Config) func(int) int {
// return func(x int) int { return x * c.Multiplier }
// return N.Mul(c.Multiplier)
// }
// r := reader.MonadFlap(getMultiplier, 5)
// result := r(Config{Multiplier: 3}) // 15
func MonadFlap[R, A, B any](fab Reader[R, func(A) B], a A) Reader[R, B] {
func MonadFlap[R, B, A any](fab Reader[R, func(A) B], a A) Reader[R, B] {
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
}
@@ -304,11 +304,11 @@ func MonadFlap[R, A, B any](fab Reader[R, func(A) B], a A) Reader[R, B] {
//
// type Config struct { Multiplier int }
// getMultiplier := reader.Asks(func(c Config) func(int) int {
// return func(x int) int { return x * c.Multiplier }
// return N.Mul(c.Multiplier)
// })
// applyTo5 := reader.Flap[Config](5)
// r := applyTo5(getMultiplier)
// result := r(Config{Multiplier: 3}) // 15
func Flap[R, A, B any](a A) Operator[R, func(A) B, B] {
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
return functor.Flap(Map[R, func(A) B, B], a)
}

View File

@@ -184,7 +184,7 @@ func TestRead(t *testing.T) {
func TestMonadFlap(t *testing.T) {
config := Config{Multiplier: 3}
getMultiplier := func(c Config) func(int) int {
return func(x int) int { return x * c.Multiplier }
return N.Mul(c.Multiplier)
}
r := MonadFlap(getMultiplier, 5)
result := r(config)
@@ -194,9 +194,9 @@ func TestMonadFlap(t *testing.T) {
func TestFlap(t *testing.T) {
config := Config{Multiplier: 3}
getMultiplier := Asks(func(c Config) func(int) int {
return func(x int) int { return x * c.Multiplier }
return N.Mul(c.Multiplier)
})
applyTo5 := Flap[Config, int, int](5)
applyTo5 := Flap[Config, int](5)
r := applyTo5(getMultiplier)
result := r(config)
assert.Equal(t, 15, result)

View File

@@ -0,0 +1,83 @@
package readerioeither
import (
"github.com/IBM/fp-go/v2/function"
RA "github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/monoid"
)
//go:inline
func MonadReduceArray[R, E, A, B any](as []ReaderIOEither[R, E, A], reduce func(B, A) B, initial B) ReaderIOEither[R, E, B] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
function.Identity[ReaderIOEither[R, E, A]],
reduce,
initial,
)
}
//go:inline
func ReduceArray[R, E, A, B any](reduce func(B, A) B, initial B) Kleisli[R, E, []ReaderIOEither[R, E, A], B] {
return RA.TraverseReduce[[]ReaderIOEither[R, E, A]](
Of,
Map,
Ap,
function.Identity[ReaderIOEither[R, E, A]],
reduce,
initial,
)
}
//go:inline
func MonadReduceArrayM[R, E, A any](as []ReaderIOEither[R, E, A], m monoid.Monoid[A]) ReaderIOEither[R, E, A] {
return MonadReduceArray(as, m.Concat, m.Empty())
}
//go:inline
func ReduceArrayM[R, E, A any](m monoid.Monoid[A]) Kleisli[R, E, []ReaderIOEither[R, E, A], A] {
return ReduceArray[R, E](m.Concat, m.Empty())
}
//go:inline
func MonadTraverseReduceArray[R, E, A, B, C any](as []A, trfrm Kleisli[R, E, A, B], reduce func(C, B) C, initial C) ReaderIOEither[R, E, C] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
trfrm,
reduce,
initial,
)
}
//go:inline
func TraverseReduceArray[R, E, A, B, C any](trfrm Kleisli[R, E, A, B], reduce func(C, B) C, initial C) Kleisli[R, E, []A, C] {
return RA.TraverseReduce[[]A](
Of,
Map,
Ap,
trfrm,
reduce,
initial,
)
}
//go:inline
func MonadTraverseReduceArrayM[R, E, A, B any](as []A, trfrm Kleisli[R, E, A, B], m monoid.Monoid[B]) ReaderIOEither[R, E, B] {
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
}
//go:inline
func TraverseReduceArrayM[R, E, A, B any](trfrm Kleisli[R, E, A, B], m monoid.Monoid[B]) Kleisli[R, E, []A, B] {
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
}

296
v2/readerioresult/array.go Normal file
View File

@@ -0,0 +1,296 @@
// 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 readerioresult
import (
"github.com/IBM/fp-go/v2/function"
RA "github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/monoid"
)
// MonadReduceArray reduces an array of ReaderIOResults to a single ReaderIOResult by applying a reduction function.
// This is the monadic version that takes the array of ReaderIOResults as the first parameter.
//
// Each ReaderIOResult is evaluated with the same environment R, and the results are accumulated using
// the provided reduce function starting from the initial value. If any ReaderIOResult fails, the entire
// operation fails with that error.
//
// Parameters:
// - as: Array of ReaderIOResults to reduce
// - reduce: Binary function that combines accumulated value with each ReaderIOResult's result
// - initial: Starting value for the reduction
//
// Example:
//
// type Config struct { Base int }
// readers := []readerioresult.ReaderIOResult[Config, int]{
// readerioresult.Of[Config](func(c Config) int { return c.Base + 1 }),
// readerioresult.Of[Config](func(c Config) int { return c.Base + 2 }),
// readerioresult.Of[Config](func(c Config) int { return c.Base + 3 }),
// }
// sum := func(acc, val int) int { return acc + val }
// r := readerioresult.MonadReduceArray(readers, sum, 0)
// result := r(Config{Base: 10})() // result.Of(36) (11 + 12 + 13)
//
//go:inline
func MonadReduceArray[R, A, B any](as []ReaderIOResult[R, A], reduce func(B, A) B, initial B) ReaderIOResult[R, B] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
function.Identity[ReaderIOResult[R, A]],
reduce,
initial,
)
}
// ReduceArray returns a curried function that reduces an array of ReaderIOResults to a single ReaderIOResult.
// This is the curried version where the reduction function and initial value are provided first,
// returning a function that takes the array of ReaderIOResults.
//
// Parameters:
// - reduce: Binary function that combines accumulated value with each ReaderIOResult's result
// - initial: Starting value for the reduction
//
// Returns:
// - A function that takes an array of ReaderIOResults and returns a ReaderIOResult of the reduced result
//
// Example:
//
// type Config struct { Multiplier int }
// product := func(acc, val int) int { return acc * val }
// reducer := readerioresult.ReduceArray[Config](product, 1)
// readers := []readerioresult.ReaderIOResult[Config, int]{
// readerioresult.Of[Config](func(c Config) int { return c.Multiplier * 2 }),
// readerioresult.Of[Config](func(c Config) int { return c.Multiplier * 3 }),
// }
// r := reducer(readers)
// result := r(Config{Multiplier: 5})() // result.Of(150) (10 * 15)
//
//go:inline
func ReduceArray[R, A, B any](reduce func(B, A) B, initial B) Kleisli[R, []ReaderIOResult[R, A], B] {
return RA.TraverseReduce[[]ReaderIOResult[R, A]](
Of,
Map,
Ap,
function.Identity[ReaderIOResult[R, A]],
reduce,
initial,
)
}
// MonadReduceArrayM reduces an array of ReaderIOResults using a Monoid to combine the results.
// This is the monadic version that takes the array of ReaderIOResults as the first parameter.
//
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
// for the reduction, making it convenient when working with monoidal types. If any ReaderIOResult
// fails, the entire operation fails with that error.
//
// Parameters:
// - as: Array of ReaderIOResults to reduce
// - m: Monoid that defines how to combine the ReaderIOResult results
//
// Example:
//
// type Config struct { Factor int }
// readers := []readerioresult.ReaderIOResult[Config, int]{
// readerioresult.Of[Config](func(c Config) int { return c.Factor }),
// readerioresult.Of[Config](func(c Config) int { return c.Factor * 2 }),
// readerioresult.Of[Config](func(c Config) int { return c.Factor * 3 }),
// }
// intAddMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
// r := readerioresult.MonadReduceArrayM(readers, intAddMonoid)
// result := r(Config{Factor: 5})() // result.Of(30) (5 + 10 + 15)
//
//go:inline
func MonadReduceArrayM[R, A any](as []ReaderIOResult[R, A], m monoid.Monoid[A]) ReaderIOResult[R, A] {
return MonadReduceArray(as, m.Concat, m.Empty())
}
// ReduceArrayM returns a curried function that reduces an array of ReaderIOResults using a Monoid.
// This is the curried version where the Monoid is provided first, returning a function
// that takes the array of ReaderIOResults.
//
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
// for the reduction.
//
// Parameters:
// - m: Monoid that defines how to combine the ReaderIOResult results
//
// Returns:
// - A function that takes an array of ReaderIOResults and returns a ReaderIOResult of the reduced result
//
// Example:
//
// type Config struct { Scale int }
// intMultMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
// reducer := readerioresult.ReduceArrayM[Config](intMultMonoid)
// readers := []readerioresult.ReaderIOResult[Config, int]{
// readerioresult.Of[Config](func(c Config) int { return c.Scale }),
// readerioresult.Of[Config](func(c Config) int { return c.Scale * 2 }),
// }
// r := reducer(readers)
// result := r(Config{Scale: 3})() // result.Of(18) (3 * 6)
//
//go:inline
func ReduceArrayM[R, A any](m monoid.Monoid[A]) Kleisli[R, []ReaderIOResult[R, A], A] {
return ReduceArray[R](m.Concat, m.Empty())
}
// MonadTraverseReduceArray transforms and reduces an array in one operation.
// This is the monadic version that takes the array as the first parameter.
//
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
// Then, the ReaderIOResult results are reduced using the provided reduction function.
// If any transformation fails, the entire operation fails with that error.
//
// This is more efficient than calling TraverseArray followed by a separate reduce operation,
// as it combines both operations into a single traversal.
//
// Parameters:
// - as: Array of elements to transform and reduce
// - trfrm: Function that transforms each element into a ReaderIOResult
// - reduce: Binary function that combines accumulated value with each transformed result
// - initial: Starting value for the reduction
//
// Example:
//
// type Config struct { Multiplier int }
// numbers := []int{1, 2, 3, 4}
// multiply := func(n int) readerioresult.ReaderIOResult[Config, int] {
// return readerioresult.Of[Config](func(c Config) int { return n * c.Multiplier })
// }
// sum := func(acc, val int) int { return acc + val }
// r := readerioresult.MonadTraverseReduceArray(numbers, multiply, sum, 0)
// result := r(Config{Multiplier: 10})() // result.Of(100) (10 + 20 + 30 + 40)
//
//go:inline
func MonadTraverseReduceArray[R, A, B, C any](as []A, trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) ReaderIOResult[R, C] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
trfrm,
reduce,
initial,
)
}
// TraverseReduceArray returns a curried function that transforms and reduces an array.
// This is the curried version where the transformation function, reduce function, and initial value
// are provided first, returning a function that takes the array.
//
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
// Then, the ReaderIOResult results are reduced using the provided reduction function.
//
// Parameters:
// - trfrm: Function that transforms each element into a ReaderIOResult
// - reduce: Binary function that combines accumulated value with each transformed result
// - initial: Starting value for the reduction
//
// Returns:
// - A function that takes an array and returns a ReaderIOResult of the reduced result
//
// Example:
//
// type Config struct { Base int }
// addBase := func(n int) readerioresult.ReaderIOResult[Config, int] {
// return readerioresult.Of[Config](func(c Config) int { return n + c.Base })
// }
// product := func(acc, val int) int { return acc * val }
// transformer := readerioresult.TraverseReduceArray(addBase, product, 1)
// r := transformer([]int{2, 3, 4})
// result := r(Config{Base: 10})() // result.Of(2184) (12 * 13 * 14)
//
//go:inline
func TraverseReduceArray[R, A, B, C any](trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Kleisli[R, []A, C] {
return RA.TraverseReduce[[]A](
Of,
Map,
Ap,
trfrm,
reduce,
initial,
)
}
// MonadTraverseReduceArrayM transforms and reduces an array using a Monoid.
// This is the monadic version that takes the array as the first parameter.
//
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
// Then, the ReaderIOResult results are reduced using the Monoid's binary operation and identity element.
// If any transformation fails, the entire operation fails with that error.
//
// This combines transformation and monoidal reduction in a single efficient operation.
//
// Parameters:
// - as: Array of elements to transform and reduce
// - trfrm: Function that transforms each element into a ReaderIOResult
// - m: Monoid that defines how to combine the transformed results
//
// Example:
//
// type Config struct { Offset int }
// numbers := []int{1, 2, 3}
// addOffset := func(n int) readerioresult.ReaderIOResult[Config, int] {
// return readerioresult.Of[Config](func(c Config) int { return n + c.Offset })
// }
// intSumMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
// r := readerioresult.MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
// result := r(Config{Offset: 100})() // result.Of(306) (101 + 102 + 103)
//
//go:inline
func MonadTraverseReduceArrayM[R, A, B any](as []A, trfrm Kleisli[R, A, B], m monoid.Monoid[B]) ReaderIOResult[R, B] {
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
}
// TraverseReduceArrayM returns a curried function that transforms and reduces an array using a Monoid.
// This is the curried version where the transformation function and Monoid are provided first,
// returning a function that takes the array.
//
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
// Then, the ReaderIOResult results are reduced using the Monoid's binary operation and identity element.
//
// Parameters:
// - trfrm: Function that transforms each element into a ReaderIOResult
// - m: Monoid that defines how to combine the transformed results
//
// Returns:
// - A function that takes an array and returns a ReaderIOResult of the reduced result
//
// Example:
//
// type Config struct { Factor int }
// scale := func(n int) readerioresult.ReaderIOResult[Config, int] {
// return readerioresult.Of[Config](func(c Config) int { return n * c.Factor })
// }
// intProdMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
// transformer := readerioresult.TraverseReduceArrayM(scale, intProdMonoid)
// r := transformer([]int{2, 3, 4})
// result := r(Config{Factor: 5})() // result.Of(3000) (10 * 15 * 20)
//
//go:inline
func TraverseReduceArrayM[R, A, B any](trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Kleisli[R, []A, B] {
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
}

View File

@@ -24,6 +24,7 @@ import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
TST "github.com/IBM/fp-go/v2/internal/testing"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -73,3 +74,341 @@ func TestSequenceArrayError(t *testing.T) {
// run across four bits
s(4)(t)
}
func TestMonadReduceArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
readers := []ReaderIOResult[Config, int]{
Of[Config](11),
Of[Config](12),
Of[Config](13),
}
sum := func(acc, val int) int { return acc + val }
r := MonadReduceArray(readers, sum, 0)
res := r(config)()
assert.Equal(t, result.Of(36), res) // 11 + 12 + 13
}
func TestMonadReduceArrayWithError(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
testErr := errors.New("test error")
readers := []ReaderIOResult[Config, int]{
Of[Config](11),
Left[Config, int](testErr),
Of[Config](13),
}
sum := func(acc, val int) int { return acc + val }
r := MonadReduceArray(readers, sum, 0)
res := r(config)()
assert.True(t, result.IsLeft(res))
val, err := result.Unwrap(res)
assert.Equal(t, 0, val)
assert.Equal(t, testErr, err)
}
func TestReduceArray(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 5}
product := func(acc, val int) int { return acc * val }
reducer := ReduceArray[Config](product, 1)
readers := []ReaderIOResult[Config, int]{
Of[Config](10),
Of[Config](15),
}
r := reducer(readers)
res := r(config)()
assert.Equal(t, result.Of(150), res) // 10 * 15
}
func TestReduceArrayWithError(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 5}
testErr := errors.New("multiplication error")
product := func(acc, val int) int { return acc * val }
reducer := ReduceArray[Config](product, 1)
readers := []ReaderIOResult[Config, int]{
Of[Config](10),
Left[Config, int](testErr),
}
r := reducer(readers)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestMonadReduceArrayM(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
readers := []ReaderIOResult[Config, int]{
Of[Config](5),
Of[Config](10),
Of[Config](15),
}
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadReduceArrayM(readers, intAddMonoid)
res := r(config)()
assert.Equal(t, result.Of(30), res) // 5 + 10 + 15
}
func TestMonadReduceArrayMWithError(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
testErr := errors.New("monoid error")
readers := []ReaderIOResult[Config, int]{
Of[Config](5),
Left[Config, int](testErr),
Of[Config](15),
}
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadReduceArrayM(readers, intAddMonoid)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestReduceArrayM(t *testing.T) {
type Config struct{ Scale int }
config := Config{Scale: 3}
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
reducer := ReduceArrayM[Config](intMultMonoid)
readers := []ReaderIOResult[Config, int]{
Of[Config](3),
Of[Config](6),
}
r := reducer(readers)
res := r(config)()
assert.Equal(t, result.Of(18), res) // 3 * 6
}
func TestReduceArrayMWithError(t *testing.T) {
type Config struct{ Scale int }
config := Config{Scale: 3}
testErr := errors.New("scale error")
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
reducer := ReduceArrayM[Config](intMultMonoid)
readers := []ReaderIOResult[Config, int]{
Of[Config](3),
Left[Config, int](testErr),
}
r := reducer(readers)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestMonadTraverseReduceArray(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 10}
numbers := []int{1, 2, 3, 4}
multiply := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n * 10)
}
sum := func(acc, val int) int { return acc + val }
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
res := r(config)()
assert.Equal(t, result.Of(100), res) // 10 + 20 + 30 + 40
}
func TestMonadTraverseReduceArrayWithError(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 10}
testErr := errors.New("transform error")
numbers := []int{1, 2, 3, 4}
multiply := func(n int) ReaderIOResult[Config, int] {
if n == 3 {
return Left[Config, int](testErr)
}
return Of[Config](n * 10)
}
sum := func(acc, val int) int { return acc + val }
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestTraverseReduceArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
addBase := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n + 10)
}
product := func(acc, val int) int { return acc * val }
transformer := TraverseReduceArray(addBase, product, 1)
r := transformer([]int{2, 3, 4})
res := r(config)()
assert.Equal(t, result.Of(2184), res) // 12 * 13 * 14
}
func TestTraverseReduceArrayWithError(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
testErr := errors.New("addition error")
addBase := func(n int) ReaderIOResult[Config, int] {
if n == 3 {
return Left[Config, int](testErr)
}
return Of[Config](n + 10)
}
product := func(acc, val int) int { return acc * val }
transformer := TraverseReduceArray(addBase, product, 1)
r := transformer([]int{2, 3, 4})
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestMonadTraverseReduceArrayM(t *testing.T) {
type Config struct{ Offset int }
config := Config{Offset: 100}
numbers := []int{1, 2, 3}
addOffset := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n + 100)
}
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
res := r(config)()
assert.Equal(t, result.Of(306), res) // 101 + 102 + 103
}
func TestMonadTraverseReduceArrayMWithError(t *testing.T) {
type Config struct{ Offset int }
config := Config{Offset: 100}
testErr := errors.New("offset error")
numbers := []int{1, 2, 3}
addOffset := func(n int) ReaderIOResult[Config, int] {
if n == 2 {
return Left[Config, int](testErr)
}
return Of[Config](n + 100)
}
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestTraverseReduceArrayM(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
scale := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n * 5)
}
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
transformer := TraverseReduceArrayM(scale, intProdMonoid)
r := transformer([]int{2, 3, 4})
res := r(config)()
assert.Equal(t, result.Of(3000), res) // 10 * 15 * 20
}
func TestTraverseReduceArrayMWithError(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
testErr := errors.New("scaling error")
scale := func(n int) ReaderIOResult[Config, int] {
if n == 3 {
return Left[Config, int](testErr)
}
return Of[Config](n * 5)
}
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
transformer := TraverseReduceArrayM(scale, intProdMonoid)
r := transformer([]int{2, 3, 4})
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestReduceArrayEmptyArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
sum := func(acc, val int) int { return acc + val }
reducer := ReduceArray[Config](sum, 100)
readers := []ReaderIOResult[Config, int]{}
r := reducer(readers)
res := r(config)()
assert.Equal(t, result.Of(100), res) // Should return initial value
}
func TestTraverseReduceArrayEmptyArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
addBase := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n + 10)
}
sum := func(acc, val int) int { return acc + val }
transformer := TraverseReduceArray(addBase, sum, 50)
r := transformer([]int{})
res := r(config)()
assert.Equal(t, result.Of(50), res) // Should return initial value
}

View File

@@ -21,6 +21,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
type BenchContext struct {
@@ -53,7 +54,7 @@ func BenchmarkLeft(b *testing.B) {
func BenchmarkMap(b *testing.B) {
ctx := BenchContext{Value: 42}
rr := Of[BenchContext](10)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
mapped := F.Pipe1(rr, Map[BenchContext](double))
@@ -64,7 +65,7 @@ func BenchmarkMap(b *testing.B) {
func BenchmarkMapChain(b *testing.B) {
ctx := BenchContext{Value: 42}
rr := Of[BenchContext](1)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := F.Pipe3(
@@ -112,7 +113,7 @@ func BenchmarkChainDeep(b *testing.B) {
func BenchmarkAp(b *testing.B) {
ctx := BenchContext{Value: 42}
fab := Of[BenchContext](func(x int) int { return x * 2 })
fab := Of[BenchContext](N.Mul(2))
fa := Of[BenchContext](21)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -191,7 +192,7 @@ func BenchmarkErrorPropagation(b *testing.B) {
ctx := BenchContext{Value: 42}
err := benchError
rr := Left[BenchContext, int](err)
double := func(x int) int { return x * 2 }
double := N.Mul(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {

View File

@@ -23,6 +23,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
RRI "github.com/IBM/fp-go/v2/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
@@ -828,7 +829,7 @@ func TestMonadApResult(t *testing.T) {
})
t.Run("value is error", func(t *testing.T) {
double := func(x int) int { return x * 2 }
double := N.Mul(2)
fabr := Of[MyContext](double)
fa := result.Left[int](idiomaticTestError)
res := MonadApResult(fabr, fa)
@@ -876,7 +877,7 @@ func TestApResult(t *testing.T) {
})
t.Run("with triple composition", func(t *testing.T) {
triple := func(x int) int { return x * 3 }
triple := N.Mul(3)
fa := result.Of(7)
res := F.Pipe1(
Of[MyContext](triple),
@@ -927,7 +928,7 @@ func TestApResultI(t *testing.T) {
return 0, errors.New("parse error")
}
addTen := func(x int) int { return x + 10 }
addTen := N.Add(10)
t.Run("parse success", func(t *testing.T) {
value, err := parseValue("42")

View File

@@ -169,7 +169,7 @@ func FromReader[R, A any](r Reader[R, A]) ReaderResult[R, A] {
// Example:
//
// rr := readerresult.Of[Config](5)
// doubled := readerresult.MonadMap(rr, func(x int) int { return x * 2 })
// doubled := readerresult.MonadMap(rr, N.Mul(2))
// // doubled(cfg) returns result.Of(10)
//
//go:inline
@@ -182,7 +182,7 @@ func MonadMap[R, A, B any](fa ReaderResult[R, A], f func(A) B) ReaderResult[R, B
//
// Example:
//
// double := readerresult.Map[Config](func(x int) int { return x * 2 })
// double := readerresult.Map[Config](N.Mul(2))
// result := F.Pipe1(readerresult.Of[Config](5), double)
//
//go:inline
@@ -684,7 +684,7 @@ func FlattenI[R, A any](mma ReaderResult[R, RRI.ReaderResult[R, A]]) ReaderResul
// Example:
//
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// result := readerresult.MonadBiMap(rr, enrichErr, double)
//
//go:inline
@@ -698,7 +698,7 @@ func MonadBiMap[R, A, B any](fa ReaderResult[R, A], f Endomorphism[error], g fun
// Example:
//
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// result := F.Pipe1(rr, readerresult.BiMap[Config](enrichErr, double))
//
//go:inline
@@ -742,7 +742,7 @@ func Read[A, R any](r R) func(ReaderResult[R, A]) Result[A] {
//
// Example:
//
// fabr := readerresult.Of[Config](func(x int) int { return x * 2 })
// fabr := readerresult.Of[Config](N.Mul(2))
// result := readerresult.MonadFlap(fabr, 5) // Returns Of(10)
//
//go:inline

View File

@@ -22,6 +22,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
@@ -99,7 +100,7 @@ func TestMap(t *testing.T) {
func TestMonadMap(t *testing.T) {
rr := Of[MyContext](5)
doubled := MonadMap(rr, func(x int) int { return x * 2 })
doubled := MonadMap(rr, N.Mul(2))
assert.Equal(t, result.Of(10), doubled(defaultContext))
}
@@ -278,7 +279,7 @@ func TestFlatten(t *testing.T) {
func TestBiMap(t *testing.T) {
enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
double := func(x int) int { return x * 2 }
double := N.Mul(2)
res1 := F.Pipe1(Of[MyContext](5), BiMap[MyContext](enrichErr, double))(defaultContext)
assert.Equal(t, result.Of(10), res1)
@@ -308,7 +309,7 @@ func TestRead(t *testing.T) {
}
func TestFlap(t *testing.T) {
fabr := Of[MyContext](func(x int) int { return x * 2 })
fabr := Of[MyContext](N.Mul(2))
flapped := MonadFlap(fabr, 5)
assert.Equal(t, result.Of(10), flapped(defaultContext))
}

View File

@@ -66,7 +66,7 @@ func Of[S, R, E, A any](a A) StateReaderIOEither[S, R, E, A] {
//
// result := statereaderioeither.MonadMap(
// statereaderioeither.Of[AppState, Config, error](21),
// func(x int) int { return x * 2 },
// N.Mul(2),
// ) // Result contains 42
func MonadMap[S, R, E, A, B any](fa StateReaderIOEither[S, R, E, A], f func(A) B) StateReaderIOEither[S, R, E, B] {
return statet.MonadMap[StateReaderIOEither[S, R, E, A], StateReaderIOEither[S, R, E, B]](
@@ -81,7 +81,7 @@ func MonadMap[S, R, E, A, B any](fa StateReaderIOEither[S, R, E, A], f func(A) B
//
// Example:
//
// double := statereaderioeither.Map[AppState, Config, error](func(x int) int { return x * 2 })
// double := statereaderioeither.Map[AppState, Config, error](N.Mul(2))
// result := function.Pipe1(statereaderioeither.Of[AppState, Config, error](21), double)
func Map[S, R, E, A, B any](f func(A) B) Operator[S, R, E, A, B] {
return statet.Map[StateReaderIOEither[S, R, E, A], StateReaderIOEither[S, R, E, B]](
@@ -133,7 +133,7 @@ func Chain[S, R, E, A, B any](f Kleisli[S, R, E, A, B]) Operator[S, R, E, A, B]
//
// Example:
//
// fab := statereaderioeither.Of[AppState, Config, error](func(x int) int { return x * 2 })
// fab := statereaderioeither.Of[AppState, Config, error](N.Mul(2))
// fa := statereaderioeither.Of[AppState, Config, error](21)
// result := statereaderioeither.MonadAp(fab, fa) // Result contains 42
func MonadAp[B, S, R, E, A any](fab StateReaderIOEither[S, R, E, func(A) B], fa StateReaderIOEither[S, R, E, A]) StateReaderIOEither[S, R, E, B] {

View File

@@ -252,7 +252,7 @@ func TestFromState(t *testing.T) {
assert.True(t, E.IsRight(res))
E.Map[error](func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 11, P.Tail(p)) // Incremented value
assert.Equal(t, 11, P.Tail(p)) // Incremented value
assert.Equal(t, 11, P.Head(p).counter) // State updated
return p
})(res)
@@ -568,7 +568,7 @@ func TestStatefulComputation(t *testing.T) {
res := result(initialState)(ctx)()
assert.True(t, E.IsRight(res))
E.Map[error](func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
assert.Equal(t, 3, P.Head(p).counter) // State updated three times
return p
})(res)