From eb7fc9f77be4ae84b499cc7f1ffcf99c4b689915 Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Wed, 12 Nov 2025 10:46:07 +0100 Subject: [PATCH] fix: better tests for Lazy Signed-off-by: Dr. Carsten Leue --- v2/lazy/apply.go | 64 +++++ v2/lazy/doc.go | 269 ++++++++++++++++++ v2/lazy/lazy.go | 101 ++++++- v2/lazy/lazy_extended_test.go | 505 ++++++++++++++++++++++++++++++++++ v2/lazy/sequence.go | 38 +++ v2/lazy/traverse.go | 24 ++ v2/lazy/types.go | 55 +++- 7 files changed, 1053 insertions(+), 3 deletions(-) create mode 100644 v2/lazy/doc.go create mode 100644 v2/lazy/lazy_extended_test.go diff --git a/v2/lazy/apply.go b/v2/lazy/apply.go index 4656660..c242ff0 100644 --- a/v2/lazy/apply.go +++ b/v2/lazy/apply.go @@ -21,10 +21,74 @@ import ( 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]] { 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]] { return IO.ApplicativeMonoid(m) } diff --git a/v2/lazy/doc.go b/v2/lazy/doc.go new file mode 100644 index 0000000..87847bd --- /dev/null +++ b/v2/lazy/doc.go @@ -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 diff --git a/v2/lazy/lazy.go b/v2/lazy/lazy.go index 61497c9..b612e01 100644 --- a/v2/lazy/lazy.go +++ b/v2/lazy/lazy.go @@ -21,10 +21,28 @@ import ( "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] { 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] { return io.FromIO(a) } @@ -34,22 +52,73 @@ func FromImpure(f func()) Lazy[any] { 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] { 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] { 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] { 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] { 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] { 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) } +// 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] { 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] { 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) } -// 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 // Defer creates an IO by creating a brand new IO via a generator function, each time diff --git a/v2/lazy/lazy_extended_test.go b/v2/lazy/lazy_extended_test.go new file mode 100644 index 0000000..b264a5f --- /dev/null +++ b/v2/lazy/lazy_extended_test.go @@ -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 diff --git a/v2/lazy/sequence.go b/v2/lazy/sequence.go index 1d06899..0d77f08 100644 --- a/v2/lazy/sequence.go +++ b/v2/lazy/sequence.go @@ -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 +// 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]] { 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]] { 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]] { 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]] { return io.SequenceT4(a, b, c, d) } diff --git a/v2/lazy/traverse.go b/v2/lazy/traverse.go index ef59744..c5f4773 100644 --- a/v2/lazy/traverse.go +++ b/v2/lazy/traverse.go @@ -17,6 +17,18 @@ package lazy 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] { return io.MonadTraverseArray(tas, f) } @@ -38,6 +50,18 @@ func SequenceArray[A any](tas []Lazy[A]) Lazy[[]A] { 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] { return io.MonadTraverseRecord(tas, f) } diff --git a/v2/lazy/types.go b/v2/lazy/types.go index 23e39bf..9ecf8dc 100644 --- a/v2/lazy/types.go +++ b/v2/lazy/types.go @@ -1,9 +1,60 @@ package lazy 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 - 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] )