1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-23 22:14:53 +02:00

fix: better tests for Lazy

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Dr. Carsten Leue
2025-11-12 10:46:07 +01:00
parent fd0550e71b
commit eb7fc9f77b
7 changed files with 1053 additions and 3 deletions

View File

@@ -21,10 +21,74 @@ import (
S "github.com/IBM/fp-go/v2/semigroup" S "github.com/IBM/fp-go/v2/semigroup"
) )
// ApplySemigroup lifts a Semigroup[A] to a Semigroup[Lazy[A]].
// This allows you to combine lazy computations using the semigroup operation
// on their underlying values.
//
// The resulting semigroup's Concat operation will evaluate both lazy computations
// and combine their results using the original semigroup's operation.
//
// Parameters:
// - s: A semigroup for values of type A
//
// Returns:
// - A semigroup for lazy computations of type A
//
// Example:
//
// import (
// M "github.com/IBM/fp-go/v2/monoid"
// "github.com/IBM/fp-go/v2/lazy"
// )
//
// // Create a semigroup for lazy integers using addition
// intAddSemigroup := lazy.ApplySemigroup(M.MonoidSum[int]())
//
// lazy1 := lazy.Of(5)
// lazy2 := lazy.Of(10)
//
// // Combine the lazy computations
// result := intAddSemigroup.Concat(lazy1, lazy2)() // 15
func ApplySemigroup[A any](s S.Semigroup[A]) S.Semigroup[Lazy[A]] { func ApplySemigroup[A any](s S.Semigroup[A]) S.Semigroup[Lazy[A]] {
return IO.ApplySemigroup(s) return IO.ApplySemigroup(s)
} }
// ApplicativeMonoid lifts a Monoid[A] to a Monoid[Lazy[A]].
// This allows you to combine lazy computations using the monoid operation
// on their underlying values, with an identity element.
//
// The resulting monoid's Concat operation will evaluate both lazy computations
// and combine their results using the original monoid's operation. The Empty
// operation returns a lazy computation that produces the monoid's identity element.
//
// Parameters:
// - m: A monoid for values of type A
//
// Returns:
// - A monoid for lazy computations of type A
//
// Example:
//
// import (
// M "github.com/IBM/fp-go/v2/monoid"
// "github.com/IBM/fp-go/v2/lazy"
// )
//
// // Create a monoid for lazy integers using addition
// intAddMonoid := lazy.ApplicativeMonoid(M.MonoidSum[int]())
//
// // Get the identity element (0 wrapped in lazy)
// empty := intAddMonoid.Empty()() // 0
//
// lazy1 := lazy.Of(5)
// lazy2 := lazy.Of(10)
//
// // Combine the lazy computations
// result := intAddMonoid.Concat(lazy1, lazy2)() // 15
//
// // Identity laws hold:
// // Concat(Empty(), x) == x
// // Concat(x, Empty()) == x
func ApplicativeMonoid[A any](m M.Monoid[A]) M.Monoid[Lazy[A]] { func ApplicativeMonoid[A any](m M.Monoid[A]) M.Monoid[Lazy[A]] {
return IO.ApplicativeMonoid(m) return IO.ApplicativeMonoid(m)
} }

269
v2/lazy/doc.go Normal file
View File

@@ -0,0 +1,269 @@
// 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 lazy provides a functional programming abstraction for synchronous computations
// without side effects. It represents deferred computations that are evaluated only when
// their result is needed.
//
// # Overview
//
// A Lazy[A] is simply a function that takes no arguments and returns a value of type A:
//
// type Lazy[A any] = func() A
//
// This allows you to defer the evaluation of a computation until it's actually needed,
// which is useful for:
// - Avoiding unnecessary computations
// - Creating infinite data structures
// - Implementing memoization
// - Composing computations in a pure functional style
//
// # Core Concepts
//
// The lazy package implements several functional programming patterns:
//
// **Functor**: Transform values inside a Lazy context using Map
//
// **Applicative**: Combine multiple Lazy computations using Ap and ApS
//
// **Monad**: Chain dependent computations using Chain and Bind
//
// **Memoization**: Cache computation results using Memoize
//
// # Basic Usage
//
// Creating and evaluating lazy computations:
//
// import (
// "fmt"
// "github.com/IBM/fp-go/v2/lazy"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Create a lazy computation
// computation := lazy.Of(42)
//
// // Transform it
// doubled := F.Pipe1(
// computation,
// lazy.Map(func(x int) int { return x * 2 }),
// )
//
// // Evaluate when needed
// result := doubled() // 84
//
// # Memoization
//
// Lazy computations can be memoized to ensure they're evaluated only once:
//
// import "math/rand"
//
// // Without memoization - generates different values each time
// random := lazy.FromLazy(rand.Int)
// value1 := random() // e.g., 12345
// value2 := random() // e.g., 67890 (different)
//
// // With memoization - caches the first result
// memoized := lazy.Memoize(rand.Int)
// value1 := memoized() // e.g., 12345
// value2 := memoized() // 12345 (same as value1)
//
// # Chaining Computations
//
// Use Chain to compose dependent computations:
//
// getUserId := lazy.Of(123)
//
// getUser := F.Pipe1(
// getUserId,
// lazy.Chain(func(id int) lazy.Lazy[User] {
// return lazy.Of(fetchUser(id))
// }),
// )
//
// user := getUser()
//
// # Do-Notation Style
//
// The package supports do-notation style composition using Bind and ApS:
//
// type Config struct {
// Host string
// Port int
// }
//
// result := F.Pipe2(
// lazy.Do(Config{}),
// lazy.Bind(
// func(host string) func(Config) Config {
// return func(c Config) Config { c.Host = host; return c }
// },
// func(c Config) lazy.Lazy[string] {
// return lazy.Of("localhost")
// },
// ),
// lazy.Bind(
// func(port int) func(Config) Config {
// return func(c Config) Config { c.Port = port; return c }
// },
// func(c Config) lazy.Lazy[int] {
// return lazy.Of(8080)
// },
// ),
// )
//
// config := result() // Config{Host: "localhost", Port: 8080}
//
// # Traverse and Sequence
//
// Transform collections of values into lazy computations:
//
// // Transform array elements
// numbers := []int{1, 2, 3}
// doubled := F.Pipe1(
// numbers,
// lazy.TraverseArray(func(x int) lazy.Lazy[int] {
// return lazy.Of(x * 2)
// }),
// )
// result := doubled() // []int{2, 4, 6}
//
// // Sequence array of lazy computations
// computations := []lazy.Lazy[int]{
// lazy.Of(1),
// lazy.Of(2),
// lazy.Of(3),
// }
// result := lazy.SequenceArray(computations)() // []int{1, 2, 3}
//
// # Retry Logic
//
// The package includes retry functionality for computations that may fail:
//
// import (
// R "github.com/IBM/fp-go/v2/retry"
// "time"
// )
//
// policy := R.CapDelay(
// 2*time.Second,
// R.Monoid.Concat(
// R.ExponentialBackoff(10),
// R.LimitRetries(5),
// ),
// )
//
// action := func(status R.RetryStatus) lazy.Lazy[string] {
// return lazy.Of(fetchData())
// }
//
// check := func(value string) bool {
// return value == "" // retry if empty
// }
//
// result := lazy.Retrying(policy, action, check)()
//
// # Algebraic Structures
//
// The package provides algebraic structures for combining lazy computations:
//
// **Semigroup**: Combine two lazy values using a semigroup operation
//
// import M "github.com/IBM/fp-go/v2/monoid"
//
// intAddSemigroup := lazy.ApplySemigroup(M.MonoidSum[int]())
// result := intAddSemigroup.Concat(lazy.Of(5), lazy.Of(10))() // 15
//
// **Monoid**: Combine lazy values with an identity element
//
// intAddMonoid := lazy.ApplicativeMonoid(M.MonoidSum[int]())
// empty := intAddMonoid.Empty()() // 0
// result := intAddMonoid.Concat(lazy.Of(5), lazy.Of(10))() // 15
//
// # Comparison
//
// Compare lazy computations by evaluating and comparing their results:
//
// import EQ "github.com/IBM/fp-go/v2/eq"
//
// eq := lazy.Eq(EQ.FromEquals[int]())
// result := eq.Equals(lazy.Of(42), lazy.Of(42)) // true
//
// # Key Functions
//
// **Creation**:
// - Of: Create a lazy computation from a value
// - FromLazy: Create a lazy computation from another lazy computation
// - FromImpure: Convert a side effect into a lazy computation
// - Defer: Create a lazy computation from a generator function
//
// **Transformation**:
// - Map: Transform the value inside a lazy computation
// - MapTo: Replace the value with a constant
// - Chain: Chain dependent computations
// - ChainFirst: Chain computations but keep the first result
// - Flatten: Flatten nested lazy computations
//
// **Combination**:
// - Ap: Apply a lazy function to a lazy value
// - ApFirst: Combine two computations, keeping the first result
// - ApSecond: Combine two computations, keeping the second result
//
// **Memoization**:
// - Memoize: Cache the result of a computation
//
// **Do-Notation**:
// - Do: Start a do-notation context
// - Bind: Bind a computation result to a context
// - Let: Attach a pure value to a context
// - LetTo: Attach a constant to a context
// - BindTo: Initialize a context from a value
// - ApS: Attach a value using applicative style
//
// **Lens-Based Operations**:
// - BindL: Bind using a lens
// - LetL: Let using a lens
// - LetToL: LetTo using a lens
// - ApSL: ApS using a lens
//
// **Collections**:
// - TraverseArray: Transform array elements into lazy computations
// - SequenceArray: Convert array of lazy computations to lazy array
// - TraverseRecord: Transform record values into lazy computations
// - SequenceRecord: Convert record of lazy computations to lazy record
//
// **Tuples**:
// - SequenceT1, SequenceT2, SequenceT3, SequenceT4: Combine lazy computations into tuples
//
// **Retry**:
// - Retrying: Retry a computation according to a policy
//
// **Algebraic**:
// - ApplySemigroup: Create a semigroup for lazy values
// - ApplicativeMonoid: Create a monoid for lazy values
// - Eq: Create an equality predicate for lazy values
//
// # Relationship to IO
//
// The lazy package is built on top of the io package and shares the same underlying
// implementation. The key difference is conceptual:
// - lazy.Lazy[A] represents a pure, synchronous computation without side effects
// - io.IO[A] represents a computation that may have side effects
//
// In practice, they are the same type, but the lazy package provides a more focused
// API for pure computations.
package lazy
// Made with Bob

View File

@@ -21,10 +21,28 @@ import (
"github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/io"
) )
// Of creates a lazy computation that returns the given value.
// This is the most basic way to lift a value into the Lazy context.
//
// The computation is pure and will always return the same value when evaluated.
//
// Example:
//
// computation := lazy.Of(42)
// result := computation() // 42
func Of[A any](a A) Lazy[A] { func Of[A any](a A) Lazy[A] {
return io.Of(a) return io.Of(a)
} }
// FromLazy creates a lazy computation from another lazy computation.
// This is an identity function that can be useful for type conversions or
// making the intent explicit in code.
//
// Example:
//
// original := func() int { return 42 }
// wrapped := lazy.FromLazy(original)
// result := wrapped() // 42
func FromLazy[A any](a Lazy[A]) Lazy[A] { func FromLazy[A any](a Lazy[A]) Lazy[A] {
return io.FromIO(a) return io.FromIO(a)
} }
@@ -34,22 +52,73 @@ func FromImpure(f func()) Lazy[any] {
return io.FromImpure(f) return io.FromImpure(f)
} }
// MonadOf creates a lazy computation that returns the given value.
// This is an alias for Of, provided for consistency with monadic naming conventions.
//
// Example:
//
// computation := lazy.MonadOf(42)
// result := computation() // 42
func MonadOf[A any](a A) Lazy[A] { func MonadOf[A any](a A) Lazy[A] {
return io.MonadOf(a) return io.MonadOf(a)
} }
// MonadMap transforms the value inside a lazy computation using the provided function.
// The transformation is not applied until the lazy computation is evaluated.
//
// This is the monadic version of Map, taking the lazy computation as the first parameter.
//
// Example:
//
// computation := lazy.Of(5)
// doubled := lazy.MonadMap(computation, func(x int) int { return x * 2 })
// result := doubled() // 10
func MonadMap[A, B any](fa Lazy[A], f func(A) B) Lazy[B] { func MonadMap[A, B any](fa Lazy[A], f func(A) B) Lazy[B] {
return io.MonadMap(fa, f) return io.MonadMap(fa, f)
} }
// Map transforms the value inside a lazy computation using the provided function.
// Returns a function that can be applied to a lazy computation.
//
// This is the curried version of MonadMap, useful for function composition.
//
// Example:
//
// double := lazy.Map(func(x int) int { return x * 2 })
// computation := lazy.Of(5)
// result := double(computation)() // 10
//
// // Or with pipe:
// result := F.Pipe1(lazy.Of(5), double)() // 10
func Map[A, B any](f func(A) B) func(fa Lazy[A]) Lazy[B] { func Map[A, B any](f func(A) B) func(fa Lazy[A]) Lazy[B] {
return io.Map(f) return io.Map(f)
} }
// MonadMapTo replaces the value inside a lazy computation with a constant value.
// The original computation is still evaluated, but its result is discarded.
//
// This is useful when you want to sequence computations but only care about
// the side effects (though Lazy should represent pure computations).
//
// Example:
//
// computation := lazy.Of("ignored")
// replaced := lazy.MonadMapTo(computation, 42)
// result := replaced() // 42
func MonadMapTo[A, B any](fa Lazy[A], b B) Lazy[B] { func MonadMapTo[A, B any](fa Lazy[A], b B) Lazy[B] {
return io.MonadMapTo(fa, b) return io.MonadMapTo(fa, b)
} }
// MapTo replaces the value inside a lazy computation with a constant value.
// Returns a function that can be applied to a lazy computation.
//
// This is the curried version of MonadMapTo.
//
// Example:
//
// replaceWith42 := lazy.MapTo[string](42)
// computation := lazy.Of("ignored")
// result := replaceWith42(computation)() // 42
func MapTo[A, B any](b B) Kleisli[Lazy[A], B] { func MapTo[A, B any](b B) Kleisli[Lazy[A], B] {
return io.MapTo[A](b) return io.MapTo[A](b)
} }
@@ -64,10 +133,32 @@ func Chain[A, B any](f Kleisli[A, B]) Kleisli[Lazy[A], B] {
return io.Chain(f) return io.Chain(f)
} }
// MonadAp applies a lazy function to a lazy value.
// Both the function and the value are evaluated when the result is evaluated.
//
// This is the applicative functor operation, allowing you to apply functions
// that are themselves wrapped in a lazy context.
//
// Example:
//
// lazyFunc := lazy.Of(func(x int) int { return x * 2 })
// lazyValue := lazy.Of(5)
// result := lazy.MonadAp(lazyFunc, lazyValue)() // 10
func MonadAp[B, A any](mab Lazy[func(A) B], ma Lazy[A]) Lazy[B] { func MonadAp[B, A any](mab Lazy[func(A) B], ma Lazy[A]) Lazy[B] {
return io.MonadApSeq(mab, ma) return io.MonadApSeq(mab, ma)
} }
// Ap applies a lazy function to a lazy value.
// Returns a function that takes a lazy function and returns a lazy result.
//
// This is the curried version of MonadAp, useful for function composition.
//
// Example:
//
// lazyValue := lazy.Of(5)
// applyTo5 := lazy.Ap[int](lazyValue)
// lazyFunc := lazy.Of(func(x int) int { return x * 2 })
// result := applyTo5(lazyFunc)() // 10
func Ap[B, A any](ma Lazy[A]) func(Lazy[func(A) B]) Lazy[B] { func Ap[B, A any](ma Lazy[A]) func(Lazy[func(A) B]) Lazy[B] {
return io.ApSeq[B](ma) return io.ApSeq[B](ma)
} }
@@ -123,7 +214,15 @@ func ChainTo[A, B any](fb Lazy[B]) Kleisli[Lazy[A], B] {
return io.ChainTo[A](fb) return io.ChainTo[A](fb)
} }
// Now returns the current timestamp // Now is a lazy computation that returns the current timestamp when evaluated.
// Each evaluation will return the current time at the moment of evaluation.
//
// Example:
//
// time1 := lazy.Now()
// // ... some time passes ...
// time2 := lazy.Now()
// // time1 and time2 will be different
var Now Lazy[time.Time] = io.Now var Now Lazy[time.Time] = io.Now
// Defer creates an IO by creating a brand new IO via a generator function, each time // Defer creates an IO by creating a brand new IO via a generator function, each time

View File

@@ -0,0 +1,505 @@
// 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 lazy
import (
"testing"
"time"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
M "github.com/IBM/fp-go/v2/monoid"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
func TestOf(t *testing.T) {
result := Of(42)
assert.Equal(t, 42, result())
}
func TestFromLazy(t *testing.T) {
original := func() int { return 42 }
wrapped := FromLazy(original)
assert.Equal(t, 42, wrapped())
}
func TestFromImpure(t *testing.T) {
counter := 0
impure := func() {
counter++
}
lazy := FromImpure(impure)
lazy()
assert.Equal(t, 1, counter)
}
func TestMonadOf(t *testing.T) {
result := MonadOf(42)
assert.Equal(t, 42, result())
}
func TestMonadMap(t *testing.T) {
result := MonadMap(Of(5), func(x int) int { return x * 2 })
assert.Equal(t, 10, result())
}
func TestMonadMapTo(t *testing.T) {
result := MonadMapTo(Of("ignored"), 42)
assert.Equal(t, 42, result())
}
func TestMapTo(t *testing.T) {
mapper := MapTo[string](42)
result := mapper(Of("ignored"))
assert.Equal(t, 42, result())
}
func TestMonadChain(t *testing.T) {
result := MonadChain(Of(5), func(x int) Lazy[int] {
return Of(x * 2)
})
assert.Equal(t, 10, result())
}
func TestMonadChainFirst(t *testing.T) {
result := MonadChainFirst(Of(5), func(x int) Lazy[string] {
return Of("ignored")
})
assert.Equal(t, 5, result())
}
func TestChainFirst(t *testing.T) {
chainer := ChainFirst(func(x int) Lazy[string] {
return Of("ignored")
})
result := chainer(Of(5))
assert.Equal(t, 5, result())
}
func TestMonadChainTo(t *testing.T) {
result := MonadChainTo(Of(5), Of(10))
assert.Equal(t, 10, result())
}
func TestChainTo(t *testing.T) {
chainer := ChainTo[int](Of(10))
result := chainer(Of(5))
assert.Equal(t, 10, result())
}
func TestMonadAp(t *testing.T) {
lazyFunc := Of(func(x int) int { return x * 2 })
lazyValue := Of(5)
result := MonadAp(lazyFunc, lazyValue)
assert.Equal(t, 10, result())
}
func TestMonadApFirst(t *testing.T) {
result := MonadApFirst(Of(5), Of(10))
assert.Equal(t, 5, result())
}
func TestMonadApSecond(t *testing.T) {
result := MonadApSecond(Of(5), Of(10))
assert.Equal(t, 10, result())
}
func TestNow(t *testing.T) {
before := time.Now()
result := Now()
after := time.Now()
assert.True(t, result.After(before) || result.Equal(before))
assert.True(t, result.Before(after) || result.Equal(after))
}
func TestDefer(t *testing.T) {
counter := 0
deferred := Defer(func() Lazy[int] {
counter++
return Of(counter)
})
// First execution
result1 := deferred()
assert.Equal(t, 1, result1)
// Second execution should generate a new computation
result2 := deferred()
assert.Equal(t, 2, result2)
}
func TestDo(t *testing.T) {
type State struct {
Value int
}
result := Do(State{Value: 42})
assert.Equal(t, State{Value: 42}, result())
}
func TestLet(t *testing.T) {
type State struct {
Value int
}
result := F.Pipe2(
Do(State{}),
Let(
func(v int) func(State) State {
return func(s State) State { s.Value = v; return s }
},
func(s State) int { return 42 },
),
Map(func(s State) int { return s.Value }),
)
assert.Equal(t, 42, result())
}
func TestLetTo(t *testing.T) {
type State struct {
Value int
}
result := F.Pipe2(
Do(State{}),
LetTo(
func(v int) func(State) State {
return func(s State) State { s.Value = v; return s }
},
42,
),
Map(func(s State) int { return s.Value }),
)
assert.Equal(t, 42, result())
}
func TestBindTo(t *testing.T) {
type State struct {
Value int
}
result := F.Pipe2(
Of(42),
BindTo(func(v int) State { return State{Value: v} }),
Map(func(s State) int { return s.Value }),
)
assert.Equal(t, 42, result())
}
func TestBindL(t *testing.T) {
type Config struct {
Port int
}
type State struct {
Config Config
}
// Create a lens manually
configLens := L.MakeLens(
func(s State) Config { return s.Config },
func(s State, cfg Config) State { s.Config = cfg; return s },
)
result := F.Pipe2(
Do(State{Config: Config{Port: 8080}}),
BindL(configLens, func(cfg Config) Lazy[Config] {
return Of(Config{Port: cfg.Port + 1})
}),
Map(func(s State) int { return s.Config.Port }),
)
assert.Equal(t, 8081, result())
}
func TestLetL(t *testing.T) {
type Config struct {
Port int
}
type State struct {
Config Config
}
// Create a lens manually
configLens := L.MakeLens(
func(s State) Config { return s.Config },
func(s State, cfg Config) State { s.Config = cfg; return s },
)
result := F.Pipe2(
Do(State{Config: Config{Port: 8080}}),
LetL(configLens, func(cfg Config) Config {
return Config{Port: cfg.Port + 1}
}),
Map(func(s State) int { return s.Config.Port }),
)
assert.Equal(t, 8081, result())
}
func TestLetToL(t *testing.T) {
type Config struct {
Port int
}
type State struct {
Config Config
}
// Create a lens manually
configLens := L.MakeLens(
func(s State) Config { return s.Config },
func(s State, cfg Config) State { s.Config = cfg; return s },
)
result := F.Pipe2(
Do(State{}),
LetToL(configLens, Config{Port: 8080}),
Map(func(s State) int { return s.Config.Port }),
)
assert.Equal(t, 8080, result())
}
func TestApSL(t *testing.T) {
type Config struct {
Port int
}
type State struct {
Config Config
}
// Create a lens manually
configLens := L.MakeLens(
func(s State) Config { return s.Config },
func(s State, cfg Config) State { s.Config = cfg; return s },
)
result := F.Pipe2(
Do(State{}),
ApSL(configLens, Of(Config{Port: 8080})),
Map(func(s State) int { return s.Config.Port }),
)
assert.Equal(t, 8080, result())
}
func TestSequenceT1(t *testing.T) {
result := SequenceT1(Of(42))
tuple := result()
assert.Equal(t, 42, tuple.F1)
}
func TestSequenceT2(t *testing.T) {
result := SequenceT2(Of(42), Of("hello"))
tuple := result()
assert.Equal(t, 42, tuple.F1)
assert.Equal(t, "hello", tuple.F2)
}
func TestSequenceT3(t *testing.T) {
result := SequenceT3(Of(42), Of("hello"), Of(true))
tuple := result()
assert.Equal(t, 42, tuple.F1)
assert.Equal(t, "hello", tuple.F2)
assert.Equal(t, true, tuple.F3)
}
func TestSequenceT4(t *testing.T) {
result := SequenceT4(Of(42), Of("hello"), Of(true), Of(3.14))
tuple := result()
assert.Equal(t, 42, tuple.F1)
assert.Equal(t, "hello", tuple.F2)
assert.Equal(t, true, tuple.F3)
assert.Equal(t, 3.14, tuple.F4)
}
func TestTraverseArray(t *testing.T) {
numbers := []int{1, 2, 3}
result := F.Pipe1(
numbers,
TraverseArray(func(x int) Lazy[int] {
return Of(x * 2)
}),
)
assert.Equal(t, []int{2, 4, 6}, result())
}
func TestTraverseArrayWithIndex(t *testing.T) {
numbers := []int{10, 20, 30}
result := F.Pipe1(
numbers,
TraverseArrayWithIndex(func(i int, x int) Lazy[int] {
return Of(x + i)
}),
)
assert.Equal(t, []int{10, 21, 32}, result())
}
func TestSequenceArray(t *testing.T) {
lazies := []Lazy[int]{Of(1), Of(2), Of(3)}
result := SequenceArray(lazies)
assert.Equal(t, []int{1, 2, 3}, result())
}
func TestMonadTraverseArray(t *testing.T) {
numbers := []int{1, 2, 3}
result := MonadTraverseArray(numbers, func(x int) Lazy[int] {
return Of(x * 2)
})
assert.Equal(t, []int{2, 4, 6}, result())
}
func TestTraverseRecord(t *testing.T) {
record := map[string]int{"a": 1, "b": 2}
result := F.Pipe1(
record,
TraverseRecord[string](func(x int) Lazy[int] {
return Of(x * 2)
}),
)
resultMap := result()
assert.Equal(t, 2, resultMap["a"])
assert.Equal(t, 4, resultMap["b"])
}
func TestTraverseRecordWithIndex(t *testing.T) {
record := map[string]int{"a": 10, "b": 20}
result := F.Pipe1(
record,
TraverseRecordWithIndex(func(k string, x int) Lazy[int] {
if k == "a" {
return Of(x + 1)
}
return Of(x + 2)
}),
)
resultMap := result()
assert.Equal(t, 11, resultMap["a"])
assert.Equal(t, 22, resultMap["b"])
}
func TestSequenceRecord(t *testing.T) {
record := map[string]Lazy[int]{
"a": Of(1),
"b": Of(2),
}
result := SequenceRecord(record)
resultMap := result()
assert.Equal(t, 1, resultMap["a"])
assert.Equal(t, 2, resultMap["b"])
}
func TestMonadTraverseRecord(t *testing.T) {
record := map[string]int{"a": 1, "b": 2}
result := MonadTraverseRecord(record, func(x int) Lazy[int] {
return Of(x * 2)
})
resultMap := result()
assert.Equal(t, 2, resultMap["a"])
assert.Equal(t, 4, resultMap["b"])
}
func TestApplySemigroup(t *testing.T) {
sg := ApplySemigroup(M.MakeMonoid(
func(a, b int) int { return a + b },
0,
))
result := sg.Concat(Of(5), Of(10))
assert.Equal(t, 15, result())
}
func TestApplicativeMonoid(t *testing.T) {
mon := ApplicativeMonoid(M.MakeMonoid(
func(a, b int) int { return a + b },
0,
))
// Test Empty
empty := mon.Empty()
assert.Equal(t, 0, empty())
// Test Concat
result := mon.Concat(Of(5), Of(10))
assert.Equal(t, 15, result())
// Test identity laws
left := mon.Concat(mon.Empty(), Of(5))
assert.Equal(t, 5, left())
right := mon.Concat(Of(5), mon.Empty())
assert.Equal(t, 5, right())
}
func TestEq(t *testing.T) {
eq := Eq(EQ.FromEquals(func(a, b int) bool { return a == b }))
assert.True(t, eq.Equals(Of(42), Of(42)))
assert.False(t, eq.Equals(Of(42), Of(43)))
}
func TestComplexDoNotation(t *testing.T) {
// Test a more complex do-notation scenario
result := F.Pipe3(
Do(utils.Empty),
Bind(utils.SetLastName, func(s utils.Initial) Lazy[string] {
return Of("Doe")
}),
Bind(utils.SetGivenName, func(s utils.WithLastName) Lazy[string] {
return Of("John")
}),
Map(utils.GetFullName),
)
assert.Equal(t, "John Doe", result())
}
func TestChainComposition(t *testing.T) {
// Test chaining multiple operations
double := func(x int) Lazy[int] {
return Of(x * 2)
}
addTen := func(x int) Lazy[int] {
return Of(x + 10)
}
result := F.Pipe2(
Of(5),
Chain(double),
Chain(addTen),
)
assert.Equal(t, 20, result())
}
func TestMapComposition(t *testing.T) {
// Test mapping multiple transformations
result := F.Pipe3(
Of(5),
Map(func(x int) int { return x * 2 }),
Map(func(x int) int { return x + 10 }),
Map(func(x int) int { return x }),
)
assert.Equal(t, 20, result())
}
// Made with Bob

View File

@@ -22,18 +22,56 @@ import (
// SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple // SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple
// SequenceT1 combines a single lazy computation into a lazy tuple.
// This is mainly useful for consistency with the other SequenceT functions.
//
// Example:
//
// lazy1 := lazy.Of(42)
// result := lazy.SequenceT1(lazy1)()
// // result is tuple.Tuple1[int]{F1: 42}
func SequenceT1[A any](a Lazy[A]) Lazy[tuple.Tuple1[A]] { func SequenceT1[A any](a Lazy[A]) Lazy[tuple.Tuple1[A]] {
return io.SequenceT1(a) return io.SequenceT1(a)
} }
// SequenceT2 combines two lazy computations into a lazy tuple of two elements.
// Both computations are evaluated when the result is evaluated.
//
// Example:
//
// lazy1 := lazy.Of(42)
// lazy2 := lazy.Of("hello")
// result := lazy.SequenceT2(lazy1, lazy2)()
// // result is tuple.Tuple2[int, string]{F1: 42, F2: "hello"}
func SequenceT2[A, B any](a Lazy[A], b Lazy[B]) Lazy[tuple.Tuple2[A, B]] { func SequenceT2[A, B any](a Lazy[A], b Lazy[B]) Lazy[tuple.Tuple2[A, B]] {
return io.SequenceT2(a, b) return io.SequenceT2(a, b)
} }
// SequenceT3 combines three lazy computations into a lazy tuple of three elements.
// All computations are evaluated when the result is evaluated.
//
// Example:
//
// lazy1 := lazy.Of(42)
// lazy2 := lazy.Of("hello")
// lazy3 := lazy.Of(true)
// result := lazy.SequenceT3(lazy1, lazy2, lazy3)()
// // result is tuple.Tuple3[int, string, bool]{F1: 42, F2: "hello", F3: true}
func SequenceT3[A, B, C any](a Lazy[A], b Lazy[B], c Lazy[C]) Lazy[tuple.Tuple3[A, B, C]] { func SequenceT3[A, B, C any](a Lazy[A], b Lazy[B], c Lazy[C]) Lazy[tuple.Tuple3[A, B, C]] {
return io.SequenceT3(a, b, c) return io.SequenceT3(a, b, c)
} }
// SequenceT4 combines four lazy computations into a lazy tuple of four elements.
// All computations are evaluated when the result is evaluated.
//
// Example:
//
// lazy1 := lazy.Of(42)
// lazy2 := lazy.Of("hello")
// lazy3 := lazy.Of(true)
// lazy4 := lazy.Of(3.14)
// result := lazy.SequenceT4(lazy1, lazy2, lazy3, lazy4)()
// // result is tuple.Tuple4[int, string, bool, float64]{F1: 42, F2: "hello", F3: true, F4: 3.14}
func SequenceT4[A, B, C, D any](a Lazy[A], b Lazy[B], c Lazy[C], d Lazy[D]) Lazy[tuple.Tuple4[A, B, C, D]] { func SequenceT4[A, B, C, D any](a Lazy[A], b Lazy[B], c Lazy[C], d Lazy[D]) Lazy[tuple.Tuple4[A, B, C, D]] {
return io.SequenceT4(a, b, c, d) return io.SequenceT4(a, b, c, d)
} }

View File

@@ -17,6 +17,18 @@ package lazy
import "github.com/IBM/fp-go/v2/io" import "github.com/IBM/fp-go/v2/io"
// MonadTraverseArray applies a function returning a lazy computation to all elements
// in an array and transforms this into a lazy computation of that array.
//
// This is the monadic version of TraverseArray, taking the array as the first parameter.
//
// Example:
//
// numbers := []int{1, 2, 3}
// result := lazy.MonadTraverseArray(numbers, func(x int) lazy.Lazy[int] {
// return lazy.Of(x * 2)
// })()
// // result is []int{2, 4, 6}
func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) Lazy[[]B] { func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) Lazy[[]B] {
return io.MonadTraverseArray(tas, f) return io.MonadTraverseArray(tas, f)
} }
@@ -38,6 +50,18 @@ func SequenceArray[A any](tas []Lazy[A]) Lazy[[]A] {
return io.SequenceArray(tas) return io.SequenceArray(tas)
} }
// MonadTraverseRecord applies a function returning a lazy computation to all values
// in a record (map) and transforms this into a lazy computation of that record.
//
// This is the monadic version of TraverseRecord, taking the record as the first parameter.
//
// Example:
//
// record := map[string]int{"a": 1, "b": 2}
// result := lazy.MonadTraverseRecord(record, func(x int) lazy.Lazy[int] {
// return lazy.Of(x * 2)
// })()
// // result is map[string]int{"a": 2, "b": 4}
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) Lazy[map[K]B] { func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) Lazy[map[K]B] {
return io.MonadTraverseRecord(tas, f) return io.MonadTraverseRecord(tas, f)
} }

View File

@@ -1,9 +1,60 @@
package lazy package lazy
type ( type (
// Lazy represents a synchronous computation without side effects // Lazy represents a synchronous computation without side effects.
// It is a function that takes no arguments and returns a value of type A.
//
// Lazy computations are evaluated only when their result is needed (lazy evaluation).
// This allows for:
// - Deferring expensive computations until they're actually required
// - Creating infinite data structures
// - Implementing memoization patterns
// - Composing pure computations in a functional style
//
// Example:
//
// // Create a lazy computation
// computation := lazy.Of(42)
//
// // Transform it (not evaluated yet)
// doubled := lazy.Map(func(x int) int { return x * 2 })(computation)
//
// // Evaluate when needed
// result := doubled() // 84
//
// Note: Lazy is an alias for io.IO[A] but represents pure computations
// without side effects, whereas IO represents computations that may have side effects.
Lazy[A any] = func() A Lazy[A any] = func() A
Kleisli[A, B any] = func(A) Lazy[B] // Kleisli represents a function that takes a value of type A and returns
// a lazy computation producing a value of type B.
//
// Kleisli arrows are used for composing monadic computations. They allow
// you to chain operations where each step depends on the result of the previous step.
//
// Example:
//
// // A Kleisli arrow that doubles a number lazily
// double := func(x int) lazy.Lazy[int] {
// return lazy.Of(x * 2)
// }
//
// // Chain it with another operation
// result := lazy.Chain(double)(lazy.Of(5))() // 10
Kleisli[A, B any] = func(A) Lazy[B]
// Operator represents a function that takes a lazy computation of type A
// and returns a lazy computation of type B.
//
// Operators are used to transform lazy computations. They are essentially
// Kleisli arrows where the input is already wrapped in a Lazy context.
//
// Example:
//
// // An operator that doubles the value in a lazy computation
// doubleOp := lazy.Map(func(x int) int { return x * 2 })
//
// // Apply it to a lazy computation
// result := doubleOp(lazy.Of(5))() // 10
Operator[A, B any] = Kleisli[Lazy[A], B] Operator[A, B any] = Kleisli[Lazy[A], B]
) )