1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-21 23:47:34 +02:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
69691e9e70 fix: iterators
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-20 16:38:36 +01:00
Dr. Carsten Leue
d3c466bfb7 fix: some cleanup
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-19 13:18:49 +01:00
Dr. Carsten Leue
a6c6ea804f fix: overhaul record
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-18 18:32:45 +01:00
33 changed files with 4895 additions and 223 deletions

View File

@@ -98,7 +98,7 @@ func Example_resultAssertions() {
var t *testing.T // placeholder for example var t *testing.T // placeholder for example
// Assert success // Assert success
successResult := result.Of[int](42) successResult := result.Of(42)
assert.Success(successResult)(t) assert.Success(successResult)(t)
// Assert failure // Assert failure

View File

@@ -23,14 +23,15 @@ import (
IOR "github.com/IBM/fp-go/v2/ioresult" IOR "github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/lazy" L "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
R "github.com/IBM/fp-go/v2/record" R "github.com/IBM/fp-go/v2/record"
T "github.com/IBM/fp-go/v2/tuple" T "github.com/IBM/fp-go/v2/tuple"
"sync" "sync"
) )
func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] { func providerToEntry(p Provider) Entry[string, ProviderFactory] {
return T.MakeTuple2(p.Provides().Id(), p.Factory()) return pair.MakePair(p.Provides().Id(), p.Factory())
} }
func itemProviderToMap(p Provider) map[string][]ProviderFactory { func itemProviderToMap(p Provider) map[string][]ProviderFactory {

View File

@@ -4,10 +4,12 @@ import (
"github.com/IBM/fp-go/v2/iooption" "github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult" "github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/record"
) )
type ( type (
Option[T any] = option.Option[T] Option[T any] = option.Option[T]
IOResult[T any] = ioresult.IOResult[T] IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T] IOOption[T any] = iooption.IOOption[T]
Entry[K comparable, V any] = record.Entry[K, V]
) )

View File

@@ -4,6 +4,7 @@ import (
"github.com/IBM/fp-go/v2/context/ioresult" "github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/iooption" "github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/record"
"github.com/IBM/fp-go/v2/result" "github.com/IBM/fp-go/v2/result"
) )
@@ -12,4 +13,5 @@ type (
Result[T any] = result.Result[T] Result[T any] = result.Result[T]
IOResult[T any] = ioresult.IOResult[T] IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T] IOOption[T any] = iooption.IOOption[T]
Entry[K comparable, V any] = record.Entry[K, V]
) )

View File

@@ -0,0 +1,120 @@
// 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 readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
package readerresult
import (
"context"
"github.com/IBM/fp-go/v2/idiomatic/result"
)
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
//
// TailRec takes a Kleisli function that returns Trampoline[A, B] and converts it into a stack-safe,
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Land value.
//
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns an
// error result containing the context's cause error, preventing unnecessary computation.
//
// Type Parameters:
// - A: The input type for the recursive step
// - B: The final result type
//
// Parameters:
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Trampoline[A, B].
// When the result is Bounce(a), recursion continues with the new value 'a'.
// When the result is Land(b), recursion terminates with the final value 'b'.
//
// Returns:
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
//
// Behavior:
// - On each iteration, checks if the context has been canceled (short circuit)
// - If canceled, returns (zero value, context.Cause(ctx))
// - If the step returns an error, propagates the error as (zero value, error)
// - If the step returns Bounce(a), continues recursion with new value 'a'
// - If the step returns Land(b), terminates with success value (b, nil)
//
// Example - Factorial computation with context:
//
// type State struct {
// n int
// acc int
// }
//
// factorialStep := func(state State) ReaderResult[tailrec.Trampoline[State, int]] {
// return func(ctx context.Context) (tailrec.Trampoline[State, int], error) {
// if state.n <= 0 {
// return tailrec.Land[State](state.acc), nil
// }
// return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}), nil
// }
// }
//
// factorial := TailRec(factorialStep)
// result, err := factorial(State{5, 1})(ctx) // Returns (120, nil)
//
// Example - Context cancellation:
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
//
// computation := TailRec(someStep)
// result, err := computation(initialValue)(ctx)
// // Returns (zero value, context.Cause(ctx)) without executing any steps
//
// Example - Error handling:
//
// errorStep := func(n int) ReaderResult[tailrec.Trampoline[int, int]] {
// return func(ctx context.Context) (tailrec.Trampoline[int, int], error) {
// if n == 5 {
// return tailrec.Trampoline[int, int]{}, errors.New("computation error")
// }
// if n <= 0 {
// return tailrec.Land[int](n), nil
// }
// return tailrec.Bounce[int](n - 1), nil
// }
// }
//
// computation := TailRec(errorStep)
// result, err := computation(10)(ctx) // Returns (0, errors.New("computation error"))
//
//go:inline
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
return func(a A) ReaderResult[B] {
initialReader := f(a)
return func(ctx context.Context) (B, error) {
rdr := initialReader
for {
// short circuit
if ctx.Err() != nil {
return result.Left[B](context.Cause(ctx))
}
rec, e := rdr(ctx)
if e != nil {
return result.Left[B](e)
}
if rec.Landed {
return result.Of(rec.Land)
}
rdr = f(rec.Bounce)
}
}
}
}

View File

@@ -0,0 +1,597 @@
// 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 readerresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
A "github.com/IBM/fp-go/v2/array"
TR "github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
// TestTailRecFactorial tests factorial computation with context
func TestTailRecFactorial(t *testing.T) {
type State struct {
n int
acc int
}
factorialStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.n <= 0 {
return TR.Land[State](state.acc), nil
}
return TR.Bounce[int](State{state.n - 1, state.acc * state.n}), nil
}
}
factorial := TailRec(factorialStep)
result, err := factorial(State{5, 1})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 120, result)
}
// TestTailRecFibonacci tests Fibonacci computation
func TestTailRecFibonacci(t *testing.T) {
type State struct {
n int
prev int
curr int
}
fibStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.n <= 0 {
return TR.Land[State](state.curr), nil
}
return TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}), nil
}
}
fib := TailRec(fibStep)
result, err := fib(State{10, 0, 1})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 89, result) // 10th Fibonacci number
}
// TestTailRecCountdown tests countdown computation
func TestTailRecCountdown(t *testing.T) {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 0, result)
}
// TestTailRecImmediateTermination tests immediate termination (Land on first call)
func TestTailRecImmediateTermination(t *testing.T) {
immediateStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
return TR.Land[int](n * 2), nil
}
}
immediate := TailRec(immediateStep)
result, err := immediate(42)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 84, result)
}
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
func TestTailRecStackSafety(t *testing.T) {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10000)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 0, result)
}
// TestTailRecSumList tests summing a list
func TestTailRecSumList(t *testing.T) {
type State struct {
list []int
sum int
}
sumStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if A.IsEmpty(state.list) {
return TR.Land[State](state.sum), nil
}
return TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}), nil
}
}
sumList := TailRec(sumStep)
result, err := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 15, result)
}
// TestTailRecCollatzConjecture tests the Collatz conjecture
func TestTailRecCollatzConjecture(t *testing.T) {
collatzStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 1 {
return TR.Land[int](n), nil
}
if n%2 == 0 {
return TR.Bounce[int](n / 2), nil
}
return TR.Bounce[int](3*n + 1), nil
}
}
collatz := TailRec(collatzStep)
result, err := collatz(10)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 1, result)
}
// TestTailRecGCD tests greatest common divisor
func TestTailRecGCD(t *testing.T) {
type State struct {
a int
b int
}
gcdStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.b == 0 {
return TR.Land[State](state.a), nil
}
return TR.Bounce[int](State{state.b, state.a % state.b}), nil
}
}
gcd := TailRec(gcdStep)
result, err := gcd(State{48, 18})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 6, result)
}
// TestTailRecErrorPropagation tests that errors are properly propagated
func TestTailRecErrorPropagation(t *testing.T) {
expectedErr := errors.New("computation error")
errorStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n == 5 {
return TR.Trampoline[int, int]{}, expectedErr
}
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
computation := TailRec(errorStep)
result, err := computation(10)(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
func TestTailRecContextCancellationImmediate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately before execution
stepExecuted := false
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
stepExecuted = true
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10)(ctx)
// Should short circuit without executing any steps
assert.False(t, stepExecuted, "Step should not be executed when context is already canceled")
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
executionCount++
// Cancel after 3 iterations
if executionCount == 3 {
cancel()
}
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(100)(ctx)
// Should stop after cancellation
assert.Error(t, err)
assert.LessOrEqual(t, executionCount, 4, "Should stop shortly after cancellation")
assert.Equal(t, context.Canceled, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextWithTimeout tests behavior with timeout context
func TestTailRecContextWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
executionCount := 0
slowStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
executionCount++
// Simulate slow computation
time.Sleep(20 * time.Millisecond)
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
computation := TailRec(slowStep)
result, err := computation(100)(ctx)
// Should timeout and return error
assert.Error(t, err)
assert.Less(t, executionCount, 100, "Should not complete all iterations due to timeout")
assert.Equal(t, context.DeadlineExceeded, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextWithCause tests that context.Cause is properly returned
func TestTailRecContextWithCause(t *testing.T) {
customErr := errors.New("custom cancellation reason")
ctx, cancel := context.WithCancelCause(context.Background())
cancel(customErr)
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10)(ctx)
assert.Error(t, err)
assert.Equal(t, customErr, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
maxExecutions := 5
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
executionCount++
if executionCount == maxExecutions {
cancel()
}
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(1000)(ctx)
// Should detect cancellation on next iteration check
assert.Error(t, err)
// Should stop within 1-2 iterations after cancellation
assert.LessOrEqual(t, executionCount, maxExecutions+2)
assert.Equal(t, context.Canceled, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
func TestTailRecContextNotCanceled(t *testing.T) {
ctx := context.Background()
executionCount := 0
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
executionCount++
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10)(ctx)
assert.NoError(t, err)
assert.Equal(t, 11, executionCount) // 10, 9, 8, ..., 1, 0
assert.Equal(t, 0, result)
}
// TestTailRecPowerOfTwo tests computing power of 2
func TestTailRecPowerOfTwo(t *testing.T) {
type State struct {
exponent int
result int
target int
}
powerStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.exponent >= state.target {
return TR.Land[State](state.result), nil
}
return TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}), nil
}
}
power := TailRec(powerStep)
result, err := power(State{0, 1, 10})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 1024, result) // 2^10
}
// TestTailRecFindInRange tests finding a value in a range
func TestTailRecFindInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.current >= state.max {
return TR.Land[State](-1), nil // Not found
}
if state.current == state.target {
return TR.Land[State](state.current), nil // Found
}
return TR.Bounce[int](State{state.current + 1, state.max, state.target}), nil
}
}
find := TailRec(findStep)
result, err := find(State{0, 100, 42})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, result)
}
// TestTailRecFindNotInRange tests finding a value not in range
func TestTailRecFindNotInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.current >= state.max {
return TR.Land[State](-1), nil // Not found
}
if state.current == state.target {
return TR.Land[State](state.current), nil // Found
}
return TR.Bounce[int](State{state.current + 1, state.max, state.target}), nil
}
}
find := TailRec(findStep)
result, err := find(State{0, 100, 200})(context.Background())
assert.NoError(t, err)
assert.Equal(t, -1, result)
}
// TestTailRecWithContextValue tests that context values are accessible
func TestTailRecWithContextValue(t *testing.T) {
type contextKey string
const multiplierKey contextKey = "multiplier"
ctx := context.WithValue(context.Background(), multiplierKey, 3)
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 0 {
multiplier := ctx.Value(multiplierKey).(int)
return TR.Land[int](n * multiplier), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(5)(ctx)
assert.NoError(t, err)
assert.Equal(t, 0, result) // 0 * 3 = 0
}
// TestTailRecComplexState tests with complex state structure
func TestTailRecComplexState(t *testing.T) {
type ComplexState struct {
counter int
sum int
product int
completed bool
}
complexStep := func(state ComplexState) ReaderResult[TR.Trampoline[ComplexState, string]] {
return func(ctx context.Context) (TR.Trampoline[ComplexState, string], error) {
if state.counter <= 0 || state.completed {
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
return TR.Land[ComplexState](result), nil
}
newState := ComplexState{
counter: state.counter - 1,
sum: state.sum + state.counter,
product: state.product * state.counter,
completed: state.counter == 1,
}
return TR.Bounce[string](newState), nil
}
}
computation := TailRec(complexStep)
result, err := computation(ComplexState{5, 0, 1, false})(context.Background())
assert.NoError(t, err)
assert.Equal(t, "sum=15, product=120", result)
}
// TestTailRecZeroIterations tests when computation terminates immediately
func TestTailRecZeroIterations(t *testing.T) {
step := func(n int) ReaderResult[TR.Trampoline[int, string]] {
return func(ctx context.Context) (TR.Trampoline[int, string], error) {
return TR.Land[int]("immediate"), nil
}
}
computation := TailRec(step)
result, err := computation(0)(context.Background())
assert.NoError(t, err)
assert.Equal(t, "immediate", result)
}
// TestTailRecErrorInFirstIteration tests error on first iteration
func TestTailRecErrorInFirstIteration(t *testing.T) {
expectedErr := errors.New("first iteration error")
step := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
return TR.Trampoline[int, int]{}, expectedErr
}
}
computation := TailRec(step)
result, err := computation(10)(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
assert.Equal(t, 0, result)
}
// TestTailRecAlternatingBounce tests alternating between different values
func TestTailRecAlternatingBounce(t *testing.T) {
type State struct {
value int
alternate bool
count int
}
step := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.count >= 10 {
return TR.Land[State](state.value), nil
}
newValue := state.value
if state.alternate {
newValue += 1
} else {
newValue -= 1
}
return TR.Bounce[int](State{newValue, !state.alternate, state.count + 1}), nil
}
}
computation := TailRec(step)
result, err := computation(State{0, true, 0})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 0, result) // Should alternate +1, -1 and end at 0
}
// TestTailRecLargeAccumulation tests accumulating large values
func TestTailRecLargeAccumulation(t *testing.T) {
type State struct {
n int
sum int64
}
step := func(state State) ReaderResult[TR.Trampoline[State, int64]] {
return func(ctx context.Context) (TR.Trampoline[State, int64], error) {
if state.n <= 0 {
return TR.Land[State](state.sum), nil
}
return TR.Bounce[int64](State{state.n - 1, state.sum + int64(state.n)}), nil
}
}
computation := TailRec(step)
result, err := computation(State{1000, 0})(context.Background())
assert.NoError(t, err)
assert.Equal(t, int64(500500), result) // Sum of 1 to 1000
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result" "github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/tailrec"
) )
type ( type (
@@ -68,4 +69,6 @@ type (
// Prism represents an optic that focuses on a case of type A within a sum type S. // Prism represents an optic that focuses on a case of type A within a sum type S.
Prism[S, A any] = prism.Prism[S, A] Prism[S, A any] = prism.Prism[S, A]
Trampoline[A, B any] = tailrec.Trampoline[A, B]
) )

View File

@@ -247,7 +247,7 @@ func TestOrLeft(t *testing.T) {
} }
} }
orLeft := OrLeft[int, MyContext](enrichErr) orLeft := OrLeft[int](enrichErr)
v, err := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext) v, err := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext)
assert.NoError(t, err) assert.NoError(t, err)

488
v2/iterator/iter/bind.go Normal file
View File

@@ -0,0 +1,488 @@
// 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 iter
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Do creates a sequence containing a single element, typically used to start a do-notation chain.
// This is the entry point for monadic composition using do-notation style.
//
// Type Parameters:
// - S: The type of the state/structure being built
//
// Parameters:
// - empty: The initial value to wrap in a sequence
//
// Returns:
// - A sequence containing the single element
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
//
// // Start a do-notation chain
// result := Do(User{})
// // yields: User{Name: "", Age: 0}
//
//go:inline
func Do[S any](
empty S,
) Seq[S] {
return Of(empty)
}
// Bind performs a monadic bind operation in do-notation style, chaining a computation
// that produces a sequence and updating the state with the result.
//
// This function is the core of do-notation for sequences. It takes a Kleisli arrow
// (a function that returns a sequence) and a setter function that updates the state
// with the result. The setter is curried to allow partial application.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the Kleisli arrow
//
// Parameters:
// - setter: A curried function that takes a value T and returns a function that updates S1 to S2
// - f: A Kleisli arrow that takes S1 and produces a sequence of T
//
// Returns:
// - An Operator that transforms Seq[S1] to Seq[S2]
//
// Example:
//
// type State struct {
// Value int
// Double int
// }
//
// setValue := func(v int) func(State) State {
// return func(s State) State { s.Value = v; return s }
// }
//
// getValues := func(s State) Seq[int] {
// return From(1, 2, 3)
// }
//
// result := F.Pipe2(
// Do(State{}),
// Bind(setValue, getValues),
// )
// // yields: State{Value: 1}, State{Value: 2}, State{Value: 3}
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return C.Bind(
Chain[S1, S2],
Map[T, S2],
setter,
f,
)
}
// Let performs a pure computation in do-notation style, updating the state with a computed value.
//
// Unlike Bind, Let doesn't perform a monadic operation - it simply computes a value from
// the current state and updates the state with that value. This is useful for intermediate
// calculations that don't require sequencing.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of the computed value
//
// Parameters:
// - key: A curried function that takes a value T and returns a function that updates S1 to S2
// - f: A function that computes T from S1
//
// Returns:
// - An Operator that transforms Seq[S1] to Seq[S2]
//
// Example:
//
// type State struct {
// Value int
// Double int
// }
//
// setDouble := func(d int) func(State) State {
// return func(s State) State { s.Double = d; return s }
// }
//
// computeDouble := func(s State) int {
// return s.Value * 2
// }
//
// result := F.Pipe2(
// Do(State{Value: 5}),
// Let(setDouble, computeDouble),
// )
// // yields: State{Value: 5, Double: 10}
//
//go:inline
func Let[S1, S2, T any](
key func(T) func(S1) S2,
f func(S1) T,
) Operator[S1, S2] {
return F.Let(
Map[S1, S2],
key,
f,
)
}
// LetTo sets a field in the state to a constant value in do-notation style.
//
// This is a specialized version of Let that doesn't compute the value from the state,
// but instead uses a fixed value. It's useful for setting constants or default values.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of the value to set
//
// Parameters:
// - key: A curried function that takes a value T and returns a function that updates S1 to S2
// - b: The constant value to set
//
// Returns:
// - An Operator that transforms Seq[S1] to Seq[S2]
//
// Example:
//
// type State struct {
// Name string
// Status string
// }
//
// setStatus := func(s string) func(State) State {
// return func(st State) State { st.Status = s; return st }
// }
//
// result := F.Pipe2(
// Do(State{Name: "Alice"}),
// LetTo(setStatus, "active"),
// )
// // yields: State{Name: "Alice", Status: "active"}
//
//go:inline
func LetTo[S1, S2, T any](
key func(T) func(S1) S2,
b T,
) Operator[S1, S2] {
return F.LetTo(
Map[S1, S2],
key,
b,
)
}
// BindTo wraps a value into a structure using a setter function.
//
// This is typically used at the beginning of a do-notation chain to convert a simple
// value into a structured state. It's the inverse of extracting a value from a structure.
//
// Type Parameters:
// - S1: The structure type to create
// - T: The value type to wrap
//
// Parameters:
// - setter: A function that takes a value T and creates a structure S1
//
// Returns:
// - An Operator that transforms Seq[T] to Seq[S1]
//
// Example:
//
// type State struct {
// Value int
// }
//
// createState := func(v int) State {
// return State{Value: v}
// }
//
// result := F.Pipe2(
// From(1, 2, 3),
// BindTo(createState),
// )
// // yields: State{Value: 1}, State{Value: 2}, State{Value: 3}
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return C.BindTo(
Map[T, S1],
setter,
)
}
// BindToP wraps a value into a structure using a Prism's ReverseGet function.
//
// This is a specialized version of BindTo that works with Prisms (optics that focus
// on a case of a sum type). It uses the Prism's ReverseGet to construct the structure.
//
// Type Parameters:
// - S1: The structure type to create
// - T: The value type to wrap
//
// Parameters:
// - setter: A Prism that can construct S1 from T
//
// Returns:
// - An Operator that transforms Seq[T] to Seq[S1]
//
// Example:
//
// // Assuming a Prism for wrapping int into a Result type
// result := F.Pipe2(
// From(1, 2, 3),
// BindToP(successPrism),
// )
// // yields: Success(1), Success(2), Success(3)
//
//go:inline
func BindToP[S1, T any](
setter Prism[S1, T],
) Operator[T, S1] {
return BindTo(setter.ReverseGet)
}
// ApS applies a sequence of values to update a state using applicative style.
//
// This function combines applicative application with state updates. It takes a sequence
// of values and a setter function, and produces an operator that applies each value
// to update the state. This is useful for parallel composition of independent computations.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of values in the sequence
//
// Parameters:
// - setter: A curried function that takes a value T and returns a function that updates S1 to S2
// - fa: A sequence of values to apply
//
// Returns:
// - An Operator that transforms Seq[S1] to Seq[S2]
//
// Example:
//
// type State struct {
// X int
// Y int
// }
//
// setY := func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// }
//
// yValues := From(10, 20, 30)
//
// result := F.Pipe2(
// Do(State{X: 5}),
// ApS(setY, yValues),
// )
// // yields: State{X: 5, Y: 10}, State{X: 5, Y: 20}, State{X: 5, Y: 30}
//
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Seq[T],
) Operator[S1, S2] {
return A.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
setter,
fa,
)
}
// ApSL applies a sequence of values to update a state field using a Lens.
//
// This is a specialized version of ApS that works with Lenses (optics that focus on
// a field of a structure). It uses the Lens's Set function to update the field.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field being updated
//
// Parameters:
// - lens: A Lens focusing on the field to update
// - fa: A sequence of values to set
//
// Returns:
// - An Endomorphism on Seq[S] (transforms Seq[S] to Seq[S])
//
// Example:
//
// type State struct {
// Name string
// Age int
// }
//
// ageLens := lens.Prop[State, int]("Age")
// ages := From(25, 30, 35)
//
// result := F.Pipe2(
// Do(State{Name: "Alice"}),
// ApSL(ageLens, ages),
// )
// // yields: State{Name: "Alice", Age: 25}, State{Name: "Alice", Age: 30}, State{Name: "Alice", Age: 35}
//
//go:inline
func ApSL[S, T any](
lens Lens[S, T],
fa Seq[T],
) Endomorphism[Seq[S]] {
return ApS(lens.Set, fa)
}
// BindL performs a monadic bind on a field of a structure using a Lens.
//
// This function combines Lens-based field access with monadic binding. It extracts
// a field value using the Lens's Get, applies a Kleisli arrow to produce a sequence,
// and updates the field with each result using the Lens's Set.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field being accessed and updated
//
// Parameters:
// - lens: A Lens focusing on the field to bind
// - f: A Kleisli arrow that takes the field value and produces a sequence
//
// Returns:
// - An Endomorphism on Seq[S]
//
// Example:
//
// type State struct {
// Value int
// }
//
// valueLens := lens.Prop[State, int]("Value")
//
// multiplyValues := func(v int) Seq[int] {
// return From(v, v*2, v*3)
// }
//
// result := F.Pipe2(
// Do(State{Value: 5}),
// BindL(valueLens, multiplyValues),
// )
// // yields: State{Value: 5}, State{Value: 10}, State{Value: 15}
//
//go:inline
func BindL[S, T any](
lens Lens[S, T],
f Kleisli[T, T],
) Endomorphism[Seq[S]] {
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
// LetL performs a pure computation on a field of a structure using a Lens.
//
// This function extracts a field value using the Lens's Get, applies a pure function
// to compute a new value, and updates the field using the Lens's Set. It's useful
// for transforming fields without monadic effects.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field being transformed
//
// Parameters:
// - lens: A Lens focusing on the field to transform
// - f: An Endomorphism that transforms the field value
//
// Returns:
// - An Endomorphism on Seq[S]
//
// Example:
//
// type State struct {
// Count int
// }
//
// countLens := lens.Prop[State, int]("Count")
//
// increment := func(n int) int { return n + 1 }
//
// result := F.Pipe2(
// Do(State{Count: 5}),
// LetL(countLens, increment),
// )
// // yields: State{Count: 6}
//
//go:inline
func LetL[S, T any](
lens Lens[S, T],
f Endomorphism[T],
) Endomorphism[Seq[S]] {
return Let(lens.Set, function.Flow2(lens.Get, f))
}
// LetToL sets a field of a structure to a constant value using a Lens.
//
// This is a specialized version of LetL that sets a field to a fixed value rather
// than computing it from the current value. It's useful for setting defaults or
// resetting fields.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field being set
//
// Parameters:
// - lens: A Lens focusing on the field to set
// - b: The constant value to set
//
// Returns:
// - An Endomorphism on Seq[S]
//
// Example:
//
// type State struct {
// Status string
// }
//
// statusLens := lens.Prop[State, string]("Status")
//
// result := F.Pipe2(
// Do(State{Status: "pending"}),
// LetToL(statusLens, "active"),
// )
// // yields: State{Status: "active"}
//
//go:inline
func LetToL[S, T any](
lens Lens[S, T],
b T,
) Endomorphism[Seq[S]] {
return LetTo(lens.Set, b)
}

View File

@@ -0,0 +1,741 @@
// 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 iter
import (
"fmt"
"slices"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// Test types
type User struct {
Name string
Age int
}
type State struct {
Value int
Double int
Status string
}
// TestDo tests the Do function
func TestDo(t *testing.T) {
t.Run("creates sequence with single element", func(t *testing.T) {
result := Do(42)
values := slices.Collect(result)
assert.Equal(t, A.Of(42), values)
})
t.Run("creates sequence with struct", func(t *testing.T) {
user := User{Name: "Alice", Age: 30}
result := Do(user)
values := slices.Collect(result)
assert.Equal(t, A.Of(user), values)
})
t.Run("creates sequence with zero value", func(t *testing.T) {
result := Do(State{})
values := slices.Collect(result)
assert.Equal(t, []State{{Value: 0, Double: 0, Status: ""}}, values)
})
}
// TestBind tests the Bind function
func TestBind(t *testing.T) {
t.Run("binds simple value", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
getValues := func(s State) Seq[int] {
return From(1, 2, 3)
}
bindOp := Bind(setValue, getValues)
result := bindOp(Do(State{}))
values := slices.Collect(result)
expected := []State{
{Value: 1},
{Value: 2},
{Value: 3},
}
assert.Equal(t, expected, values)
})
t.Run("chains multiple binds", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
getValues := func(s State) Seq[int] {
return From(5)
}
computeDouble := func(s State) Seq[int] {
return From(s.Value * 2)
}
result := F.Flow2(
Bind(setValue, getValues),
Bind(setDouble, computeDouble),
)(Do(State{}))
values := slices.Collect(result)
expected := []State{{Value: 5, Double: 10}}
assert.Equal(t, expected, values)
})
t.Run("binds with multiple results", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
multiplyValues := func(s State) Seq[int] {
return From(s.Value, s.Value*2, s.Value*3)
}
bindOp := Bind(setValue, multiplyValues)
result := bindOp(Do(State{Value: 2}))
values := slices.Collect(result)
expected := []State{
{Value: 2},
{Value: 4},
{Value: 6},
}
assert.Equal(t, expected, values)
})
t.Run("binds with empty sequence", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
emptySeq := func(s State) Seq[int] {
return Empty[int]()
}
bindOp := Bind(setValue, emptySeq)
result := bindOp(Do(State{Value: 5}))
values := slices.Collect(result)
assert.Empty(t, values)
})
}
// TestLet tests the Let function
func TestLet(t *testing.T) {
t.Run("computes value from state", func(t *testing.T) {
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
computeDouble := func(s State) int {
return s.Value * 2
}
letOp := Let(setDouble, computeDouble)
result := letOp(Do(State{Value: 5}))
values := slices.Collect(result)
expected := []State{{Value: 5, Double: 10}}
assert.Equal(t, expected, values)
})
t.Run("chains multiple lets", func(t *testing.T) {
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
computeDouble := func(s State) int {
return s.Value * 2
}
computeValue := func(s State) int {
return s.Double / 2
}
result := F.Flow2(
Let(setDouble, computeDouble),
Let(setValue, computeValue),
)(Do(State{Value: 7}))
values := slices.Collect(result)
expected := []State{{Value: 7, Double: 14}}
assert.Equal(t, expected, values)
})
t.Run("computes complex transformation", func(t *testing.T) {
setStatus := func(s string) func(State) State {
return func(st State) State {
st.Status = s
return st
}
}
computeStatus := func(s State) string {
if s.Value > 10 {
return "high"
}
return "low"
}
letOp := Let(setStatus, computeStatus)
result := letOp(Do(State{Value: 15}))
values := slices.Collect(result)
expected := []State{{Value: 15, Status: "high"}}
assert.Equal(t, expected, values)
})
}
// TestLetTo tests the LetTo function
func TestLetTo(t *testing.T) {
t.Run("sets constant value", func(t *testing.T) {
setStatus := func(s string) func(State) State {
return func(st State) State {
st.Status = s
return st
}
}
letToOp := LetTo(setStatus, "active")
result := letToOp(Do(State{Value: 5}))
values := slices.Collect(result)
expected := []State{{Value: 5, Status: "active"}}
assert.Equal(t, expected, values)
})
t.Run("chains multiple LetTo calls", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
setStatus := func(st string) func(State) State {
return func(s State) State {
s.Status = st
return s
}
}
result := F.Flow2(
LetTo(setValue, 42),
LetTo(setStatus, "ready"),
)(Do(State{}))
values := slices.Collect(result)
expected := []State{{Value: 42, Status: "ready"}}
assert.Equal(t, expected, values)
})
t.Run("sets zero value", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
letToOp := LetTo(setValue, 0)
result := letToOp(Do(State{Value: 100}))
values := slices.Collect(result)
expected := []State{{Value: 0}}
assert.Equal(t, expected, values)
})
}
// TestBindTo tests the BindTo function
func TestBindTo(t *testing.T) {
t.Run("wraps values into structure", func(t *testing.T) {
createState := func(v int) State {
return State{Value: v}
}
bindToOp := BindTo(createState)
result := bindToOp(From(1, 2, 3))
values := slices.Collect(result)
expected := []State{
{Value: 1},
{Value: 2},
{Value: 3},
}
assert.Equal(t, expected, values)
})
t.Run("wraps into complex structure", func(t *testing.T) {
createUser := func(name string) User {
return User{Name: name, Age: 0}
}
bindToOp := BindTo(createUser)
result := bindToOp(From("Alice", "Bob", "Charlie"))
values := slices.Collect(result)
expected := []User{
{Name: "Alice", Age: 0},
{Name: "Bob", Age: 0},
{Name: "Charlie", Age: 0},
}
assert.Equal(t, expected, values)
})
t.Run("wraps empty sequence", func(t *testing.T) {
createState := func(v int) State {
return State{Value: v}
}
bindToOp := BindTo(createState)
result := bindToOp(Empty[int]())
values := slices.Collect(result)
assert.Empty(t, values)
})
}
// TestApS tests the ApS function
func TestApS(t *testing.T) {
t.Run("applies sequence of values", func(t *testing.T) {
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
doubles := From(10, 20, 30)
apOp := ApS(setDouble, doubles)
result := apOp(Do(State{Value: 5}))
values := slices.Collect(result)
expected := []State{
{Value: 5, Double: 10},
{Value: 5, Double: 20},
{Value: 5, Double: 30},
}
assert.Equal(t, expected, values)
})
t.Run("applies with empty sequence", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
apOp := ApS(setValue, Empty[int]())
result := apOp(Do(State{Value: 5}))
values := slices.Collect(result)
assert.Empty(t, values)
})
t.Run("chains multiple ApS calls", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
values := From(1, 2)
doubles := From(10, 20)
result := F.Flow2(
ApS(setValue, values),
ApS(setDouble, doubles),
)(Do(State{}))
results := slices.Collect(result)
// Cartesian product: 2 values × 2 doubles = 4 results
assert.Len(t, results, 4)
})
}
// TestDoNotationChain tests a complete do-notation chain
func TestDoNotationChain(t *testing.T) {
t.Run("complex do-notation chain", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
setStatus := func(st string) func(State) State {
return func(s State) State {
s.Status = st
return s
}
}
getValues := func(s State) Seq[int] {
return From(5, 10)
}
computeDouble := func(s State) int {
return s.Value * 2
}
result := F.Flow3(
Bind(setValue, getValues),
Let(setDouble, computeDouble),
LetTo(setStatus, "computed"),
)(Do(State{}))
results := slices.Collect(result)
expected := []State{
{Value: 5, Double: 10, Status: "computed"},
{Value: 10, Double: 20, Status: "computed"},
}
assert.Equal(t, expected, results)
})
t.Run("mixed bind and let operations", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
getInitial := func(s State) Seq[int] {
return From(3)
}
multiplyValue := func(s State) Seq[int] {
return From(s.Value*2, s.Value*3)
}
result := F.Flow2(
Bind(setValue, getInitial),
Bind(setDouble, multiplyValue),
)(Do(State{}))
results := slices.Collect(result)
expected := []State{
{Value: 3, Double: 6},
{Value: 3, Double: 9},
}
assert.Equal(t, expected, results)
})
}
// TestEdgeCases tests edge cases
func TestEdgeCases(t *testing.T) {
t.Run("bind with single element", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
getSingle := func(s State) Seq[int] {
return From(42)
}
bindOp := Bind(setValue, getSingle)
result := bindOp(Do(State{}))
results := slices.Collect(result)
expected := []State{{Value: 42}}
assert.Equal(t, expected, results)
})
t.Run("multiple binds with cartesian product", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
getValues := func(s State) Seq[int] {
return From(1, 2)
}
getDoubles := func(s State) Seq[int] {
return From(10, 20)
}
result := F.Flow2(
Bind(setValue, getValues),
Bind(setDouble, getDoubles),
)(Do(State{}))
results := slices.Collect(result)
// Should produce cartesian product: 2 × 2 = 4 results
assert.Len(t, results, 4)
})
t.Run("let with identity function", func(t *testing.T) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
identity := func(s State) int {
return s.Value
}
letOp := Let(setValue, identity)
result := letOp(Do(State{Value: 99}))
results := slices.Collect(result)
expected := []State{{Value: 99}}
assert.Equal(t, expected, results)
})
}
// Benchmark tests
func BenchmarkBind(b *testing.B) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
getValues := func(s State) Seq[int] {
return From(1, 2, 3, 4, 5)
}
bindOp := Bind(setValue, getValues)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := bindOp(Do(State{}))
// Consume the sequence
for range result {
}
}
}
func BenchmarkLet(b *testing.B) {
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
computeDouble := func(s State) int {
return s.Value * 2
}
letOp := Let(setDouble, computeDouble)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := letOp(Do(State{Value: 5}))
// Consume the sequence
for range result {
}
}
}
func BenchmarkDoNotationChain(b *testing.B) {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
setStatus := func(st string) func(State) State {
return func(s State) State {
s.Status = st
return s
}
}
getValues := func(s State) Seq[int] {
return From(5, 10, 15)
}
computeDouble := func(s State) int {
return s.Value * 2
}
chain := F.Flow3(
Bind(setValue, getValues),
Let(setDouble, computeDouble),
LetTo(setStatus, "computed"),
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := chain(Do(State{}))
// Consume the sequence
for range result {
}
}
}
// Example tests for documentation
func ExampleDo() {
result := Do(42)
for v := range result {
fmt.Println(v)
}
// Output: 42
}
func ExampleBind() {
setValue := func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
}
getValues := func(s State) Seq[int] {
return From(1, 2, 3)
}
bindOp := Bind(setValue, getValues)
result := bindOp(Do(State{}))
for s := range result {
fmt.Printf("Value: %d\n", s.Value)
}
// Output:
// Value: 1
// Value: 2
// Value: 3
}
func ExampleLet() {
setDouble := func(d int) func(State) State {
return func(s State) State {
s.Double = d
return s
}
}
computeDouble := func(s State) int {
return s.Value * 2
}
letOp := Let(setDouble, computeDouble)
result := letOp(Do(State{Value: 5}))
for s := range result {
fmt.Printf("Value: %d, Double: %d\n", s.Value, s.Double)
}
// Output: Value: 5, Double: 10
}
func ExampleLetTo() {
setStatus := func(s string) func(State) State {
return func(st State) State {
st.Status = s
return st
}
}
letToOp := LetTo(setStatus, "active")
result := letToOp(Do(State{Value: 5}))
for s := range result {
fmt.Printf("Value: %d, Status: %s\n", s.Value, s.Status)
}
// Output: Value: 5, Status: active
}

View File

@@ -0,0 +1,82 @@
// 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 iter
import (
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
)
// Compress filters elements from a sequence based on a corresponding sequence of boolean selectors.
//
// This function takes a sequence of boolean values and returns an operator that filters
// elements from the input sequence. An element is included in the output if and only if
// the corresponding boolean selector is true. The filtering stops when either sequence
// is exhausted.
//
// The implementation works by:
// 1. Zipping the input sequence with the selector sequence
// 2. Converting the Seq2 to a sequence of Pairs
// 3. Filtering to keep only pairs where the boolean (tail) is true
// 4. Extracting the original values (head) from the filtered pairs
//
// Type Parameters:
// - U: The type of elements in the sequence to be filtered
//
// Parameters:
// - sel: A sequence of boolean values used as selectors
//
// Returns:
// - An Operator that filters elements based on the selector sequence
//
// Example - Basic filtering:
//
// data := iter.From(1, 2, 3, 4, 5)
// selectors := iter.From(true, false, true, false, true)
// filtered := iter.Compress(selectors)(data)
// // yields: 1, 3, 5
//
// Example - Shorter selector sequence:
//
// data := iter.From("a", "b", "c", "d", "e")
// selectors := iter.From(true, true, false)
// filtered := iter.Compress(selectors)(data)
// // yields: "a", "b" (stops when selectors are exhausted)
//
// Example - All false selectors:
//
// data := iter.From(1, 2, 3)
// selectors := iter.From(false, false, false)
// filtered := iter.Compress(selectors)(data)
// // yields: nothing (empty sequence)
//
// Example - All true selectors:
//
// data := iter.From(10, 20, 30)
// selectors := iter.From(true, true, true)
// filtered := iter.Compress(selectors)(data)
// // yields: 10, 20, 30 (all elements pass through)
func Compress[U any](sel Seq[bool]) Operator[U, U] {
return F.Flow3(
Zip[U](sel),
ToSeqPair[U, bool],
FilterMap(F.Flow2(
O.FromPredicate(P.Tail[U, bool]),
O.Map(P.Head[U, bool]),
)),
)
}

View File

@@ -0,0 +1,365 @@
// 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 iter
import (
"fmt"
"testing"
P "github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
// TestCompress tests the Compress function
func TestCompress(t *testing.T) {
t.Run("filters with alternating selectors", func(t *testing.T) {
data := From(1, 2, 3, 4, 5)
selectors := From(true, false, true, false, true)
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Equal(t, []int{1, 3, 5}, result)
})
t.Run("filters strings with boolean selectors", func(t *testing.T) {
data := From("a", "b", "c", "d", "e")
selectors := From(true, true, false, false, true)
filtered := Compress[string](selectors)(data)
result := toSlice(filtered)
assert.Equal(t, []string{"a", "b", "e"}, result)
})
t.Run("all true selectors pass all elements", func(t *testing.T) {
data := From(10, 20, 30)
selectors := From(true, true, true)
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Equal(t, []int{10, 20, 30}, result)
})
t.Run("all false selectors produce empty sequence", func(t *testing.T) {
data := From(1, 2, 3)
selectors := From(false, false, false)
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Empty(t, result)
})
t.Run("shorter selector sequence stops early", func(t *testing.T) {
data := From("a", "b", "c", "d", "e")
selectors := From(true, true, false)
filtered := Compress[string](selectors)(data)
result := toSlice(filtered)
assert.Equal(t, []string{"a", "b"}, result)
})
t.Run("shorter data sequence stops early", func(t *testing.T) {
data := From(1, 2, 3)
selectors := From(true, false, true, true, true)
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Equal(t, []int{1, 3}, result)
})
t.Run("empty data sequence produces empty result", func(t *testing.T) {
data := Empty[int]()
selectors := From(true, true, true)
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Empty(t, result)
})
t.Run("empty selector sequence produces empty result", func(t *testing.T) {
data := From(1, 2, 3)
selectors := Empty[bool]()
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Empty(t, result)
})
t.Run("both empty sequences produce empty result", func(t *testing.T) {
data := Empty[int]()
selectors := Empty[bool]()
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Empty(t, result)
})
t.Run("single element with true selector", func(t *testing.T) {
data := From(42)
selectors := From(true)
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Equal(t, []int{42}, result)
})
t.Run("single element with false selector", func(t *testing.T) {
data := From(42)
selectors := From(false)
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Empty(t, result)
})
}
// TestCompressWithComplexTypes tests Compress with complex data types
func TestCompressWithComplexTypes(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("filters struct values", func(t *testing.T) {
data := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
Person{"David", 28},
)
selectors := From(true, false, true, false)
filtered := Compress[Person](selectors)(data)
result := toSlice(filtered)
expected := []Person{
{"Alice", 30},
{"Charlie", 35},
}
assert.Equal(t, expected, result)
})
t.Run("filters pointer values", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
p3 := &Person{"Charlie", 35}
data := From(p1, p2, p3)
selectors := From(false, true, true)
filtered := Compress[*Person](selectors)(data)
result := toSlice(filtered)
assert.Equal(t, []*Person{p2, p3}, result)
})
}
// TestCompressWithChainedOperations tests Compress with other operations
func TestCompressWithChainedOperations(t *testing.T) {
t.Run("compress then map", func(t *testing.T) {
data := From(1, 2, 3, 4, 5)
selectors := From(true, false, true, false, true)
result := toSlice(
MonadMap(
Compress[int](selectors)(data),
func(x int) int { return x * 10 },
),
)
assert.Equal(t, []int{10, 30, 50}, result)
})
t.Run("map then compress", func(t *testing.T) {
data := From(1, 2, 3, 4, 5)
mapped := MonadMap(data, func(x int) int { return x * 2 })
selectors := From(true, true, false, false, true)
result := toSlice(Compress[int](selectors)(mapped))
assert.Equal(t, []int{2, 4, 10}, result)
})
t.Run("compress with filtered data", func(t *testing.T) {
data := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
evens := MonadFilter(data, func(x int) bool { return x%2 == 0 })
selectors := From(true, false, true, false, true)
result := toSlice(Compress[int](selectors)(evens))
assert.Equal(t, []int{2, 6, 10}, result)
})
}
// TestToSeqPair tests the ToSeqPair function
func TestToSeqPair(t *testing.T) {
t.Run("converts Seq2 to sequence of pairs", func(t *testing.T) {
seq2 := MonadZip(From("a", "b", "c"), From(1, 2, 3))
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
expected := []Pair[string, int]{
P.MakePair("a", 1),
P.MakePair("b", 2),
P.MakePair("c", 3),
}
assert.Equal(t, expected, result)
})
t.Run("converts empty Seq2", func(t *testing.T) {
seq2 := MonadZip(Empty[int](), Empty[string]())
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
assert.Empty(t, result)
})
t.Run("converts single pair", func(t *testing.T) {
seq2 := MonadZip(From(42), From("answer"))
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
expected := []Pair[int, string]{
P.MakePair(42, "answer"),
}
assert.Equal(t, expected, result)
})
t.Run("stops at shorter sequence", func(t *testing.T) {
seq2 := MonadZip(From(1, 2, 3, 4, 5), From("a", "b"))
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
expected := []Pair[int, string]{
P.MakePair(1, "a"),
P.MakePair(2, "b"),
}
assert.Equal(t, expected, result)
})
}
// TestToSeqPairWithOperations tests ToSeqPair with other operations
func TestToSeqPairWithOperations(t *testing.T) {
t.Run("map over pairs", func(t *testing.T) {
seq2 := MonadZip(From(1, 2, 3), From(10, 20, 30))
pairs := ToSeqPair(seq2)
sums := MonadMap(pairs, func(p Pair[int, int]) int {
return P.Head(p) + P.Tail(p)
})
result := toSlice(sums)
assert.Equal(t, []int{11, 22, 33}, result)
})
t.Run("filter pairs", func(t *testing.T) {
seq2 := MonadZip(From(1, 2, 3, 4, 5), From(10, 20, 30, 40, 50))
pairs := ToSeqPair(seq2)
filtered := MonadFilter(pairs, func(p Pair[int, int]) bool {
return P.Head(p)%2 == 0
})
result := toSlice(filtered)
expected := []Pair[int, int]{
P.MakePair(2, 20),
P.MakePair(4, 40),
}
assert.Equal(t, expected, result)
})
t.Run("extract first elements from pairs", func(t *testing.T) {
seq2 := MonadZip(From(1, 2, 3), From("x", "y", "z"))
pairs := ToSeqPair(seq2)
firsts := MonadMap(pairs, func(p Pair[int, string]) int {
return P.Head(p)
})
result := toSlice(firsts)
assert.Equal(t, []int{1, 2, 3}, result)
})
t.Run("extract second elements from pairs", func(t *testing.T) {
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
pairs := ToSeqPair(seq2)
seconds := MonadMap(pairs, func(p Pair[int, string]) string {
return P.Tail(p)
})
result := toSlice(seconds)
assert.Equal(t, []string{"a", "b", "c"}, result)
})
}
// TestCompressAndToSeqPairTogether tests using both functions together
func TestCompressAndToSeqPairTogether(t *testing.T) {
t.Run("compress uses ToSeqPair internally", func(t *testing.T) {
// This test verifies the integration works correctly
data := From(10, 20, 30, 40, 50)
selectors := From(true, false, true, true, false)
filtered := Compress[int](selectors)(data)
result := toSlice(filtered)
assert.Equal(t, []int{10, 30, 40}, result)
})
}
// Benchmark tests
func BenchmarkCompress(b *testing.B) {
data := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
selectors := From(true, false, true, false, true, false, true, false, true, false)
b.ResetTimer()
for i := 0; i < b.N; i++ {
filtered := Compress[int](selectors)(data)
for range filtered {
}
}
}
func BenchmarkToSeqPair(b *testing.B) {
seq2 := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
pairs := ToSeqPair(seq2)
for range pairs {
}
}
}
// Example tests for documentation
func ExampleCompress() {
data := From(1, 2, 3, 4, 5)
selectors := From(true, false, true, false, true)
filtered := Compress[int](selectors)(data)
for v := range filtered {
fmt.Printf("%d ", v)
}
// Output: 1 3 5
}
func ExampleCompress_allTrue() {
data := From(10, 20, 30)
selectors := From(true, true, true)
filtered := Compress[int](selectors)(data)
for v := range filtered {
fmt.Printf("%d ", v)
}
// Output: 10 20 30
}
func ExampleCompress_allFalse() {
data := From(1, 2, 3)
selectors := From(false, false, false)
filtered := Compress[int](selectors)(data)
count := 0
for range filtered {
count++
}
fmt.Printf("Count: %d\n", count)
// Output: Count: 0
}
func ExampleToSeqPair() {
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
pairs := ToSeqPair(seq2)
for p := range pairs {
fmt.Printf("(%d, %s) ", P.Head(p), P.Tail(p))
}
// Output: (1, a) (2, b) (3, c)
}
func ExampleToSeqPair_withMap() {
seq2 := MonadZip(From(1, 2, 3), From(10, 20, 30))
pairs := ToSeqPair(seq2)
sums := MonadMap(pairs, func(p Pair[int, int]) int {
return P.Head(p) + P.Tail(p)
})
for sum := range sums {
fmt.Printf("%d ", sum)
}
// Output: 11 22 33
}

58
v2/iterator/iter/first.go Normal file
View File

@@ -0,0 +1,58 @@
// 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 iter
import "github.com/IBM/fp-go/v2/option"
// First returns the first element from an [Iterator] wrapped in an [Option].
//
// This function attempts to retrieve the first element from the iterator. If the iterator
// contains at least one element, it returns Some(element). If the iterator is empty,
// it returns None. The function consumes only the first element of the iterator.
//
// Type Parameters:
// - U: The type of elements in the iterator
//
// Parameters:
// - mu: The input iterator to get the first element from
//
// Returns:
// - Option[U]: Some(first element) if the iterator is non-empty, None otherwise
//
// Example with non-empty sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// first := iter.First(seq)
// // Returns: Some(1)
//
// Example with empty sequence:
//
// seq := iter.Empty[int]()
// first := iter.First(seq)
// // Returns: None
//
// Example with filtered sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// filtered := iter.Filter(func(x int) bool { return x > 3 })(seq)
// first := iter.First(filtered)
// // Returns: Some(4)
func First[U any](mu Seq[U]) Option[U] {
for u := range mu {
return option.Some(u)
}
return option.None[U]()
}

View File

@@ -0,0 +1,346 @@
// 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 iter
import (
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestFirst tests getting the first element from a non-empty sequence
func TestFirst(t *testing.T) {
t.Run("returns first element from integer sequence", func(t *testing.T) {
seq := From(1, 2, 3)
fst := First(seq)
assert.Equal(t, O.Of(1), fst)
})
t.Run("returns first element from string sequence", func(t *testing.T) {
seq := From("a", "b", "c")
fst := First(seq)
assert.Equal(t, O.Of("a"), fst)
})
t.Run("returns first element from single element sequence", func(t *testing.T) {
seq := From(42)
fst := First(seq)
assert.Equal(t, O.Of(42), fst)
})
t.Run("returns first element from large sequence", func(t *testing.T) {
seq := From(100, 200, 300, 400, 500)
fst := First(seq)
assert.Equal(t, O.Of(100), fst)
})
}
// TestFirstEmpty tests getting the first element from an empty sequence
func TestFirstEmpty(t *testing.T) {
t.Run("returns None for empty integer sequence", func(t *testing.T) {
seq := Empty[int]()
fst := First(seq)
assert.Equal(t, O.None[int](), fst)
})
t.Run("returns None for empty string sequence", func(t *testing.T) {
seq := Empty[string]()
fst := First(seq)
assert.Equal(t, O.None[string](), fst)
})
t.Run("returns None for empty struct sequence", func(t *testing.T) {
type TestStruct struct {
Value int
}
seq := Empty[TestStruct]()
fst := First(seq)
assert.Equal(t, O.None[TestStruct](), fst)
})
}
// TestFirstWithFiltered tests First with filtered sequences
func TestFirstWithFiltered(t *testing.T) {
t.Run("returns first element matching filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
filtered := MonadFilter(seq, N.MoreThan(3))
fst := First(filtered)
assert.Equal(t, O.Of(4), fst)
})
t.Run("returns None when no elements match filter", func(t *testing.T) {
seq := From(1, 2, 3)
filtered := MonadFilter(seq, N.MoreThan(10))
fst := First(filtered)
assert.Equal(t, O.None[int](), fst)
})
t.Run("returns first even number", func(t *testing.T) {
seq := From(1, 3, 5, 6, 7, 8)
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
fst := First(filtered)
assert.Equal(t, O.Of(6), fst)
})
}
// TestFirstWithMapped tests First with mapped sequences
func TestFirstWithMapped(t *testing.T) {
t.Run("returns first element after mapping", func(t *testing.T) {
seq := From(1, 2, 3)
mapped := MonadMap(seq, N.Mul(2))
fst := First(mapped)
assert.Equal(t, O.Of(2), fst)
})
t.Run("returns first string after mapping", func(t *testing.T) {
seq := From(1, 2, 3)
mapped := MonadMap(seq, S.Format[int]("num-%d"))
fst := First(mapped)
assert.Equal(t, O.Of("num-1"), fst)
})
}
// TestFirstWithComplex tests First with complex types
func TestFirstWithComplex(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("returns first person", func(t *testing.T) {
seq := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
)
fst := First(seq)
expected := O.Of(Person{"Alice", 30})
assert.Equal(t, expected, fst)
})
t.Run("returns first pointer", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
seq := From(p1, p2)
fst := First(seq)
assert.Equal(t, O.Of(p1), fst)
})
}
// TestFirstDoesNotConsumeEntireSequence tests that First only consumes the first element
func TestFirstDoesNotConsumeEntireSequence(t *testing.T) {
t.Run("only consumes first element", func(t *testing.T) {
callCount := 0
seq := MonadMap(From(1, 2, 3, 4, 5), func(x int) int {
callCount++
return x * 2
})
fst := First(seq)
assert.Equal(t, O.Of(2), fst)
// Should only have called the map function once for the first element
assert.Equal(t, 1, callCount)
})
}
// TestFirstWithChainedOperations tests First with multiple chained operations
func TestFirstWithChainedOperations(t *testing.T) {
t.Run("chains filter, map, and first", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, N.MoreThan(5))
mapped := MonadMap(filtered, N.Mul(10))
result := First(mapped)
assert.Equal(t, O.Of(60), result)
})
t.Run("chains map and filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
filtered := MonadFilter(mapped, N.MoreThan(5))
result := First(filtered)
assert.Equal(t, O.Of(6), result)
})
}
// TestFirstWithReplicate tests First with replicated values
func TestFirstWithReplicate(t *testing.T) {
t.Run("returns first from replicated sequence", func(t *testing.T) {
seq := Replicate(5, 42)
fst := First(seq)
assert.Equal(t, O.Of(42), fst)
})
t.Run("returns None from zero replications", func(t *testing.T) {
seq := Replicate(0, 42)
fst := First(seq)
assert.Equal(t, O.None[int](), fst)
})
}
// TestFirstWithMakeBy tests First with MakeBy
func TestFirstWithMakeBy(t *testing.T) {
t.Run("returns first generated element", func(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i * i })
fst := First(seq)
assert.Equal(t, O.Of(0), fst)
})
t.Run("returns None for zero elements", func(t *testing.T) {
seq := MakeBy(0, F.Identity[int])
fst := First(seq)
assert.Equal(t, O.None[int](), fst)
})
}
// TestFirstWithPrepend tests First with Prepend
func TestFirstWithPrepend(t *testing.T) {
t.Run("returns prepended element", func(t *testing.T) {
seq := From(2, 3, 4)
prepended := Prepend(1)(seq)
fst := First(prepended)
assert.Equal(t, O.Of(1), fst)
})
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
prepended := Prepend(42)(seq)
fst := First(prepended)
assert.Equal(t, O.Of(42), fst)
})
}
// TestFirstWithAppend tests First with Append
func TestFirstWithAppend(t *testing.T) {
t.Run("returns first element, not appended", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
fst := First(appended)
assert.Equal(t, O.Of(1), fst)
})
t.Run("returns appended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
appended := Append(42)(seq)
fst := First(appended)
assert.Equal(t, O.Of(42), fst)
})
}
// TestFirstWithChain tests First with Chain (flatMap)
func TestFirstWithChain(t *testing.T) {
t.Run("returns first from chained sequence", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
fst := First(chained)
assert.Equal(t, O.Of(1), fst)
})
t.Run("returns None when chain produces empty", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return Empty[int]()
})
fst := First(chained)
assert.Equal(t, O.None[int](), fst)
})
}
// TestFirstWithFlatten tests First with Flatten
func TestFirstWithFlatten(t *testing.T) {
t.Run("returns first from flattened sequence", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5))
flattened := Flatten(nested)
fst := First(flattened)
assert.Equal(t, O.Of(1), fst)
})
t.Run("returns None from empty nested sequence", func(t *testing.T) {
nested := Empty[Seq[int]]()
flattened := Flatten(nested)
fst := First(flattened)
assert.Equal(t, O.None[int](), fst)
})
}
// Benchmark tests
func BenchmarkFirst(b *testing.B) {
seq := From(1, 2, 3, 4, 5)
b.ResetTimer()
for b.Loop() {
First(seq)
}
}
func BenchmarkFirstLargeSequence(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
seq := From(data...)
b.ResetTimer()
for b.Loop() {
First(seq)
}
}
func BenchmarkFirstFiltered(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for b.Loop() {
filtered := MonadFilter(seq, N.MoreThan(5))
First(filtered)
}
}
// Example tests for documentation
func ExampleFirst() {
seq := From(1, 2, 3, 4, 5)
first := First(seq)
if value, ok := O.Unwrap(first); ok {
fmt.Printf("First element: %d\n", value)
}
// Output: First element: 1
}
func ExampleFirst_empty() {
seq := Empty[int]()
first := First(seq)
if _, ok := O.Unwrap(first); !ok {
fmt.Println("Sequence is empty")
}
// Output: Sequence is empty
}
func ExampleFirst_filtered() {
seq := From(1, 2, 3, 4, 5)
filtered := MonadFilter(seq, N.MoreThan(3))
first := First(filtered)
if value, ok := O.Unwrap(first); ok {
fmt.Printf("First element > 3: %d\n", value)
}
// Output: First element > 3: 4
}

View File

@@ -51,6 +51,7 @@ import (
G "github.com/IBM/fp-go/v2/internal/iter" G "github.com/IBM/fp-go/v2/internal/iter"
M "github.com/IBM/fp-go/v2/monoid" M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
) )
// Of creates a sequence containing a single element. // Of creates a sequence containing a single element.
@@ -849,9 +850,9 @@ func Append[A any](tail A) Operator[A, A] {
// //
// seqA := From(1, 2, 3) // seqA := From(1, 2, 3)
// seqB := From("a", "b") // seqB := From("a", "b")
// result := MonadZip(seqB, seqA) // result := MonadZip(seqA, seqB)
// // yields: (1, "a"), (2, "b") // // yields: (1, "a"), (2, "b")
func MonadZip[A, B any](fb Seq[B], fa Seq[A]) Seq2[A, B] { func MonadZip[A, B any](fa Seq[A], fb Seq[B]) Seq2[A, B] {
return func(yield func(A, B) bool) { return func(yield func(A, B) bool) {
na, sa := I.Pull(fa) na, sa := I.Pull(fa)
@@ -882,8 +883,8 @@ func MonadZip[A, B any](fb Seq[B], fa Seq[A]) Seq2[A, B] {
// // yields: (1, "a"), (2, "b"), (3, "c") // // yields: (1, "a"), (2, "b"), (3, "c")
// //
//go:inline //go:inline
func Zip[A, B any](fa Seq[A]) func(Seq[B]) Seq2[A, B] { func Zip[A, B any](fb Seq[B]) func(Seq[A]) Seq2[A, B] {
return F.Bind2nd(MonadZip[A, B], fa) return F.Bind2nd(MonadZip[A, B], fb)
} }
//go:inline //go:inline
@@ -895,3 +896,50 @@ func MonadMapToArray[A, B any](fa Seq[A], f func(A) B) []B {
func MapToArray[A, B any](f func(A) B) func(Seq[A]) []B { func MapToArray[A, B any](f func(A) B) func(Seq[A]) []B {
return G.MapToArray[Seq[A], []B](f) return G.MapToArray[Seq[A], []B](f)
} }
// ToSeqPair converts a key-value sequence (Seq2) into a sequence of Pairs.
//
// This function transforms a Seq2[A, B] (which yields key-value pairs when iterated)
// into a Seq[Pair[A, B]] (which yields Pair objects). This is useful when you need
// to work with pairs as first-class values rather than as separate key-value arguments.
//
// Type Parameters:
// - A: The type of the first element (key) in each pair
// - B: The type of the second element (value) in each pair
//
// Parameters:
// - as: A Seq2 that yields key-value pairs
//
// Returns:
// - A Seq that yields Pair objects containing the key-value pairs
//
// Example - Basic conversion:
//
// seq2 := iter.MonadZip(iter.From("a", "b", "c"), iter.From(1, 2, 3))
// pairs := iter.ToSeqPair(seq2)
// // yields: Pair("a", 1), Pair("b", 2), Pair("c", 3)
//
// Example - Using with Map:
//
// seq2 := iter.MonadZip(iter.From(1, 2, 3), iter.From(10, 20, 30))
// pairs := iter.ToSeqPair(seq2)
// sums := iter.MonadMap(pairs, func(p Pair[int, int]) int {
// return p.Fst + p.Snd
// })
// // yields: 11, 22, 33
//
// Example - Empty sequence:
//
// seq2 := iter.Empty[int]()
// zipped := iter.MonadZip(seq2, iter.Empty[string]())
// pairs := iter.ToSeqPair(zipped)
// // yields: nothing (empty sequence)
func ToSeqPair[A, B any](as Seq2[A, B]) Seq[Pair[A, B]] {
return func(yield Predicate[Pair[A, B]]) {
for a, b := range as {
if !yield(pair.MakePair(a, b)) {
return
}
}
}
}

View File

@@ -22,9 +22,11 @@ import (
"strings" "strings"
"testing" "testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number" N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record"
S "github.com/IBM/fp-go/v2/string" S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -42,13 +44,13 @@ func toMap[K comparable, V any](seq Seq2[K, V]) map[K]V {
func TestOf(t *testing.T) { func TestOf(t *testing.T) {
seq := Of(42) seq := Of(42)
result := toSlice(seq) result := toSlice(seq)
assert.Equal(t, []int{42}, result) assert.Equal(t, A.Of(42), result)
} }
func TestOf2(t *testing.T) { func TestOf2(t *testing.T) {
seq := Of2("key", 100) seq := Of2("key", 100)
result := toMap(seq) result := toMap(seq)
assert.Equal(t, map[string]int{"key": 100}, result) assert.Equal(t, R.Of("key", 100), result)
} }
func TestFrom(t *testing.T) { func TestFrom(t *testing.T) {
@@ -460,7 +462,7 @@ func TestMonadZip(t *testing.T) {
var pairs []string var pairs []string
for a, b := range result { for a, b := range result {
pairs = append(pairs, fmt.Sprintf("%d:%s", a, b)) pairs = append(pairs, fmt.Sprintf("%d:%s", b, a))
} }
assert.Equal(t, []string{"1:a", "2:b"}, pairs) assert.Equal(t, []string{"1:a", "2:b"}, pairs)
} }
@@ -468,8 +470,8 @@ func TestMonadZip(t *testing.T) {
func TestZip(t *testing.T) { func TestZip(t *testing.T) {
seqA := From(1, 2, 3) seqA := From(1, 2, 3)
seqB := From("a", "b", "c") seqB := From("a", "b", "c")
zipWithA := Zip[int, string](seqA) zipWithA := Zip[int](seqB)
result := zipWithA(seqB) result := zipWithA(seqA)
var pairs []string var pairs []string
for a, b := range result { for a, b := range result {
@@ -587,3 +589,29 @@ func ExampleMonoid() {
fmt.Println(result) fmt.Println(result)
// Output: [1 2 3 4 5 6] // Output: [1 2 3 4 5 6]
} }
func TestMonadMapToArray(t *testing.T) {
seq := From(1, 2, 3)
result := MonadMapToArray(seq, N.Mul(2))
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMonadMapToArrayEmpty(t *testing.T) {
seq := Empty[int]()
result := MonadMapToArray(seq, N.Mul(2))
assert.Empty(t, result)
}
func TestMapToArray(t *testing.T) {
seq := From(1, 2, 3)
mapper := MapToArray(N.Mul(2))
result := mapper(seq)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMapToArrayIdentity(t *testing.T) {
seq := From("a", "b", "c")
mapper := MapToArray(F.Identity[string])
result := mapper(seq)
assert.Equal(t, []string{"a", "b", "c"}, result)
}

View File

@@ -18,8 +18,12 @@ package iter
import ( import (
I "iter" I "iter"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/iterator/stateless" "github.com/IBM/fp-go/v2/iterator/stateless"
"github.com/IBM/fp-go/v2/optics/lens/option" "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate" "github.com/IBM/fp-go/v2/predicate"
) )
@@ -54,4 +58,12 @@ type (
// Operator2 represents a transformation from one key-value sequence to another. // Operator2 represents a transformation from one key-value sequence to another.
Operator2[K, A, B any] = Kleisli2[K, Seq2[K, A], B] Operator2[K, A, B any] = Kleisli2[K, Seq2[K, A], B]
Lens[S, A any] = lens.Lens[S, A]
Prism[S, A any] = prism.Prism[S, A]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Pair[A, B any] = pair.Pair[A, B]
) )

View File

@@ -19,8 +19,28 @@ import (
G "github.com/IBM/fp-go/v2/iterator/stateless/generic" G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
) )
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element. // Cycle creates an [Iterator] that repeats the elements of the input [Iterator] indefinitely.
// Note, the [Iterator] does not produce any output until the predicate first becomes false // The iterator cycles through all elements of the input, and when it reaches the end, it starts over from the beginning.
// This creates an infinite iterator, so it should be used with caution and typically combined with operations that limit the output.
//
// Type Parameters:
// - U: The type of elements in the iterator
//
// Parameters:
// - ma: The input iterator to cycle through
//
// Returns:
// - An iterator that infinitely repeats the elements of the input iterator
//
// Example:
//
// iter := stateless.FromArray([]int{1, 2, 3})
// cycled := stateless.Cycle(iter)
// // Produces: 1, 2, 3, 1, 2, 3, 1, 2, 3, ... (infinitely)
//
// // Typically used with Take to limit output:
// limited := stateless.Take(7)(cycled)
// // Produces: 1, 2, 3, 1, 2, 3, 1
func Cycle[U any](ma Iterator[U]) Iterator[U] { func Cycle[U any](ma Iterator[U]) Iterator[U] {
return G.Cycle(ma) return G.Cycle(ma)
} }

View File

@@ -19,7 +19,39 @@ import (
G "github.com/IBM/fp-go/v2/iterator/stateless/generic" G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
) )
// First returns the first item in an iterator if such an item exists // First returns the first element from an [Iterator] wrapped in an [Option].
//
// This function attempts to retrieve the first element from the iterator. If the iterator
// contains at least one element, it returns Some(element). If the iterator is empty,
// it returns None. The function consumes only the first element of the iterator.
//
// Type Parameters:
// - U: The type of elements in the iterator
//
// Parameters:
// - mu: The input iterator to get the first element from
//
// Returns:
// - Option[U]: Some(first element) if the iterator is non-empty, None otherwise
//
// Example with non-empty iterator:
//
// iter := stateless.From(1, 2, 3, 4, 5)
// first := stateless.First(iter)
// // Returns: Some(1)
//
// Example with empty iterator:
//
// iter := stateless.Empty[int]()
// first := stateless.First(iter)
// // Returns: None
//
// Example with filtered iterator:
//
// iter := stateless.From(1, 2, 3, 4, 5)
// filtered := stateless.Filter(func(x int) bool { return x > 3 })(iter)
// first := stateless.First(filtered)
// // Returns: Some(4)
func First[U any](mu Iterator[U]) Option[U] { func First[U any](mu Iterator[U]) Option[U] {
return G.First(mu) return G.First(mu)
} }

View File

@@ -16,26 +16,240 @@
package stateless package stateless
import ( import (
"fmt"
"testing" "testing"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// TestFirst tests getting the first element from a non-empty iterator
func TestFirst(t *testing.T) { func TestFirst(t *testing.T) {
t.Run("returns first element from integer iterator", func(t *testing.T) {
seq := From(1, 2, 3) seq := From(1, 2, 3)
fst := First(seq) fst := First(seq)
assert.Equal(t, O.Of(1), fst) assert.Equal(t, O.Of(1), fst)
})
t.Run("returns first element from string iterator", func(t *testing.T) {
seq := From("a", "b", "c")
fst := First(seq)
assert.Equal(t, O.Of("a"), fst)
})
t.Run("returns first element from single element iterator", func(t *testing.T) {
seq := From(42)
fst := First(seq)
assert.Equal(t, O.Of(42), fst)
})
t.Run("returns first element from large iterator", func(t *testing.T) {
seq := From(100, 200, 300, 400, 500)
fst := First(seq)
assert.Equal(t, O.Of(100), fst)
})
} }
// TestNoFirst tests getting the first element from an empty iterator
func TestNoFirst(t *testing.T) { func TestNoFirst(t *testing.T) {
t.Run("returns None for empty integer iterator", func(t *testing.T) {
seq := Empty[int]() seq := Empty[int]()
fst := First(seq)
assert.Equal(t, O.None[int](), fst)
})
t.Run("returns None for empty string iterator", func(t *testing.T) {
seq := Empty[string]()
fst := First(seq)
assert.Equal(t, O.None[string](), fst)
})
t.Run("returns None for empty struct iterator", func(t *testing.T) {
type TestStruct struct {
Value int
}
seq := Empty[TestStruct]()
fst := First(seq)
assert.Equal(t, O.None[TestStruct](), fst)
})
}
// TestFirstWithFiltered tests First with filtered iterators
func TestFirstWithFiltered(t *testing.T) {
t.Run("returns first element matching filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
filtered := Filter(N.MoreThan(3))(seq)
fst := First(filtered)
assert.Equal(t, O.Of(4), fst)
})
t.Run("returns None when no elements match filter", func(t *testing.T) {
seq := From(1, 2, 3)
filtered := Filter(N.MoreThan(10))(seq)
fst := First(filtered)
assert.Equal(t, O.None[int](), fst)
})
t.Run("returns first even number", func(t *testing.T) {
seq := From(1, 3, 5, 6, 7, 8)
filtered := Filter(func(x int) bool { return x%2 == 0 })(seq)
fst := First(filtered)
assert.Equal(t, O.Of(6), fst)
})
}
// TestFirstWithMapped tests First with mapped iterators
func TestFirstWithMapped(t *testing.T) {
t.Run("returns first element after mapping", func(t *testing.T) {
seq := From(1, 2, 3)
mapped := Map(N.Mul(2))(seq)
fst := First(mapped)
assert.Equal(t, O.Of(2), fst)
})
t.Run("returns first string after mapping", func(t *testing.T) {
seq := From(1, 2, 3)
mapped := Map(S.Format[int]("num-%d"))(seq)
fst := First(mapped)
assert.Equal(t, O.Of("num-1"), fst)
})
}
// TestFirstWithTake tests First with Take
func TestFirstWithTake(t *testing.T) {
t.Run("returns first element from taken subset", func(t *testing.T) {
seq := From(10, 20, 30, 40, 50)
taken := Take[int](3)(seq)
fst := First(taken)
assert.Equal(t, O.Of(10), fst)
})
t.Run("returns None when taking zero elements", func(t *testing.T) {
seq := From(1, 2, 3)
taken := Take[int](0)(seq)
fst := First(taken)
assert.Equal(t, O.None[int](), fst)
})
}
// TestFirstWithComplex tests First with complex types
func TestFirstWithComplex(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("returns first person", func(t *testing.T) {
seq := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
)
fst := First(seq)
expected := O.Of(Person{"Alice", 30})
assert.Equal(t, expected, fst)
})
t.Run("returns first pointer", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
seq := From(p1, p2)
fst := First(seq)
assert.Equal(t, O.Of(p1), fst)
})
}
// TestFirstDoesNotConsumeEntireIterator tests that First only consumes the first element
func TestFirstDoesNotConsumeEntireIterator(t *testing.T) {
t.Run("only consumes first element", func(t *testing.T) {
callCount := 0
seq := Map(func(x int) int {
callCount++
return x * 2
})(From(1, 2, 3, 4, 5))
fst := First(seq) fst := First(seq)
assert.Equal(t, O.None[int](), fst) assert.Equal(t, O.Of(2), fst)
// Should only have called the map function once for the first element
assert.Equal(t, 1, callCount)
})
}
// TestFirstWithChainedOperations tests First with multiple chained operations
func TestFirstWithChainedOperations(t *testing.T) {
t.Run("chains filter, map, and first", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result := First(
Map(N.Mul(10))(
Filter(N.MoreThan(5))(seq),
),
)
assert.Equal(t, O.Of(60), result)
})
t.Run("chains take, filter, and first", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result := First(
Filter(N.MoreThan(3))(
Take[int](7)(seq),
),
)
assert.Equal(t, O.Of(4), result)
})
}
// Benchmark tests
func BenchmarkFirst(b *testing.B) {
seq := From(1, 2, 3, 4, 5)
b.ResetTimer()
for b.Loop() {
First(seq)
}
}
func BenchmarkFirstLargeIterator(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
seq := FromArray(data)
for b.Loop() {
First(seq)
}
}
// Example tests for documentation
func ExampleFirst() {
iter := From(1, 2, 3, 4, 5)
first := First(iter)
if value, ok := O.Unwrap(first); ok {
fmt.Printf("First element: %d\n", value)
}
// Output: First element: 1
}
func ExampleFirst_empty() {
iter := Empty[int]()
first := First(iter)
if _, ok := O.Unwrap(first); !ok {
fmt.Println("Iterator is empty")
}
// Output: Iterator is empty
}
func ExampleFirst_filtered() {
iter := From(1, 2, 3, 4, 5)
filtered := Filter(N.MoreThan(3))(iter)
first := First(filtered)
if value, ok := O.Unwrap(first); ok {
fmt.Printf("First element > 3: %d\n", value)
}
// Output: First element > 3: 4
} }

View File

@@ -85,7 +85,7 @@ func ChainReaderK[
GEB ~func(E) ET.Either[L, B], GEB ~func(E) ET.Either[L, B],
GB ~func(E) B, GB ~func(E) B,
L, E, A, B any](f func(A) GB) func(GEA) GEB { L, E, A, B any](f func(A) GB) func(GEA) GEB {
return Chain[GEA, GEB, L, E, A, B](F.Flow2(f, FromReader[GB, GEB, L, E, B])) return Chain[GEA](F.Flow2(f, FromReader[GB, GEB, L, E, B]))
} }
func Of[GEA ~func(E) ET.Either[L, A], L, E, A any](a A) GEA { func Of[GEA ~func(E) ET.Either[L, A], L, E, A any](a A) GEA {

View File

@@ -16,7 +16,6 @@
package record package record
import ( import (
Mo "github.com/IBM/fp-go/v2/monoid"
G "github.com/IBM/fp-go/v2/record/generic" G "github.com/IBM/fp-go/v2/record/generic"
) )
@@ -30,8 +29,8 @@ import (
// Count int // Count int
// } // }
// result := record.Do[string, State]() // result := record.Do[string, State]()
func Do[K comparable, S any]() map[K]S { func Do[K comparable, S any]() Record[K, S] {
return G.Do[map[K]S]() return G.Do[Record[K, S]]()
} }
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]. // Bind attaches the result of a computation to a context [S1] to produce a context [S2].
@@ -68,29 +67,87 @@ func Do[K comparable, S any]() map[K]S {
// }, // },
// ), // ),
// ) // )
func Bind[S1, T any, K comparable, S2 any](m Mo.Monoid[map[K]S2]) func(setter func(T) func(S1) S2, f func(S1) map[K]T) func(map[K]S1) map[K]S2 { func Bind[S1, T any, K comparable, S2 any](m Monoid[Record[K, S2]]) func(
return G.Bind[map[K]S1, map[K]S2, map[K]T](m) setter func(T) func(S1) S2,
f Kleisli[K, S1, T],
) Operator[K, S1, S2] {
return G.Bind[Record[K, S1], Record[K, S2], Record[K, T]](m)
} }
// Let attaches the result of a computation to a context [S1] to produce a context [S2] // Let attaches the result of a computation to a context [S1] to produce a context [S2].
// Unlike Bind, Let does not require a Monoid because it transforms each value independently
// without merging multiple maps.
//
// The setter function takes the computed value and returns a function that updates the context.
// The computation function f takes the current context and produces a value.
//
// Example:
//
// type State struct {
// Name string
// Length int
// }
//
// result := F.Pipe2(
// map[string]State{"a": {Name: "Alice"}},
// record.Let(
// func(length int) func(State) State {
// return func(s State) State { s.Length = length; return s }
// },
// func(s State) int { return len(s.Name) },
// ),
// ) // map[string]State{"a": {Name: "Alice", Length: 5}}
func Let[S1, T any, K comparable, S2 any]( func Let[S1, T any, K comparable, S2 any](
setter func(T) func(S1) S2, setter func(T) func(S1) S2,
f func(S1) T, f func(S1) T,
) func(map[K]S1) map[K]S2 { ) Operator[K, S1, S2] {
return G.Let[map[K]S1, map[K]S2](setter, f) return G.Let[Record[K, S1], Record[K, S2]](setter, f)
} }
// LetTo attaches the a value to a context [S1] to produce a context [S2] // LetTo attaches a constant value to a context [S1] to produce a context [S2].
// This is similar to Let but uses a fixed value instead of computing it from the context.
//
// The setter function takes the value and returns a function that updates the context.
//
// Example:
//
// type State struct {
// Name string
// Version int
// }
//
// result := F.Pipe2(
// map[string]State{"a": {Name: "Alice"}},
// record.LetTo(
// func(version int) func(State) State {
// return func(s State) State { s.Version = version; return s }
// },
// 2,
// ),
// ) // map[string]State{"a": {Name: "Alice", Version: 2}}
func LetTo[S1, T any, K comparable, S2 any]( func LetTo[S1, T any, K comparable, S2 any](
setter func(T) func(S1) S2, setter func(T) func(S1) S2,
b T, b T,
) func(map[K]S1) map[K]S2 { ) Operator[K, S1, S2] {
return G.LetTo[map[K]S1, map[K]S2](setter, b) return G.LetTo[Record[K, S1], Record[K, S2]](setter, b)
} }
// BindTo initializes a new state [S1] from a value [T] // BindTo initializes a new state [S1] from a value [T].
func BindTo[S1, T any, K comparable](setter func(T) S1) func(map[K]T) map[K]S1 { // This is typically used as the first step in a do-notation chain to convert
return G.BindTo[map[K]S1, map[K]T](setter) // a simple map of values into a map of state objects.
//
// Example:
//
// type State struct {
// Name string
// }
//
// result := F.Pipe1(
// map[string]string{"a": "Alice", "b": "Bob"},
// record.BindTo(func(name string) State { return State{Name: name} }),
// ) // map[string]State{"a": {Name: "Alice"}, "b": {Name: "Bob"}}
func BindTo[S1, T any, K comparable](setter func(T) S1) Operator[K, T, S1] {
return G.BindTo[Record[K, S1], Record[K, T]](setter)
} }
// ApS attaches a value to a context [S1] to produce a context [S2] by considering // ApS attaches a value to a context [S1] to produce a context [S2] by considering
@@ -126,6 +183,9 @@ func BindTo[S1, T any, K comparable](setter func(T) S1) func(map[K]T) map[K]S1 {
// counts, // counts,
// ), // ),
// ) // map[string]State{"a": {Name: "Alice", Count: 10}, "b": {Name: "Bob", Count: 20}} // ) // map[string]State{"a": {Name: "Alice", Count: 10}, "b": {Name: "Bob", Count: 20}}
func ApS[S1, T any, K comparable, S2 any](m Mo.Monoid[map[K]S2]) func(setter func(T) func(S1) S2, fa map[K]T) func(map[K]S1) map[K]S2 { func ApS[S1, T any, K comparable, S2 any](m Monoid[Record[K, S2]]) func(
return G.ApS[map[K]S1, map[K]S2, map[K]T](m) setter func(T) func(S1) S2,
fa Record[K, T],
) Operator[K, S1, S2] {
return G.ApS[Record[K, S1], Record[K, S2], Record[K, T]](m)
} }

225
v2/record/bind_test.go Normal file
View File

@@ -0,0 +1,225 @@
// 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 record
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
type TestState struct {
Name string
Count int
Version int
}
func TestDo(t *testing.T) {
result := Do[string, TestState]()
assert.NotNil(t, result)
assert.Empty(t, result)
assert.Equal(t, map[string]TestState{}, result)
}
func TestBindTo(t *testing.T) {
input := map[string]string{"a": "Alice", "b": "Bob"}
result := F.Pipe1(
input,
BindTo[TestState, string, string](func(name string) TestState {
return TestState{Name: name}
}),
)
expected := map[string]TestState{
"a": {Name: "Alice"},
"b": {Name: "Bob"},
}
assert.Equal(t, expected, result)
}
func TestLet(t *testing.T) {
input := map[string]TestState{
"a": {Name: "Alice"},
"b": {Name: "Bob"},
}
result := F.Pipe1(
input,
Let[TestState, int, string](
func(length int) func(TestState) TestState {
return func(s TestState) TestState {
s.Count = length
return s
}
},
func(s TestState) int {
return len(s.Name)
},
),
)
expected := map[string]TestState{
"a": {Name: "Alice", Count: 5},
"b": {Name: "Bob", Count: 3},
}
assert.Equal(t, expected, result)
}
func TestLetTo(t *testing.T) {
input := map[string]TestState{
"a": {Name: "Alice"},
"b": {Name: "Bob"},
}
result := F.Pipe1(
input,
LetTo[TestState, int, string](
func(version int) func(TestState) TestState {
return func(s TestState) TestState {
s.Version = version
return s
}
},
2,
),
)
expected := map[string]TestState{
"a": {Name: "Alice", Version: 2},
"b": {Name: "Bob", Version: 2},
}
assert.Equal(t, expected, result)
}
func TestBind(t *testing.T) {
monoid := MergeMonoid[string, TestState]()
// Bind chains computations where each step can depend on previous results
result := F.Pipe1(
map[string]string{"x": "test"},
Bind[string, int](monoid)(
func(length int) func(string) TestState {
return func(s string) TestState {
return TestState{Name: s, Count: length}
}
},
func(s string) map[string]int {
return map[string]int{"x": len(s)}
},
),
)
expected := map[string]TestState{
"x": {Name: "test", Count: 4},
}
assert.Equal(t, expected, result)
}
func TestApS(t *testing.T) {
monoid := MergeMonoid[string, TestState]()
// ApS applies independent computations
names := map[string]string{"x": "Alice"}
counts := map[string]int{"x": 10}
result := F.Pipe2(
map[string]TestState{"x": {}},
ApS[TestState, string](monoid)(
func(name string) func(TestState) TestState {
return func(s TestState) TestState {
s.Name = name
return s
}
},
names,
),
ApS[TestState, int](monoid)(
func(count int) func(TestState) TestState {
return func(s TestState) TestState {
s.Count = count
return s
}
},
counts,
),
)
expected := map[string]TestState{
"x": {Name: "Alice", Count: 10},
}
assert.Equal(t, expected, result)
}
func TestBindChain(t *testing.T) {
// Test a complete do-notation chain with BindTo, Let, and LetTo
result := F.Pipe3(
map[string]string{"x": "Alice", "y": "Bob"},
BindTo[TestState, string, string](func(name string) TestState {
return TestState{Name: name}
}),
Let[TestState, int, string](
func(count int) func(TestState) TestState {
return func(s TestState) TestState {
s.Count = count
return s
}
},
func(s TestState) int {
return len(s.Name)
},
),
LetTo[TestState, int, string](
func(version int) func(TestState) TestState {
return func(s TestState) TestState {
s.Version = version
return s
}
},
1,
),
)
expected := map[string]TestState{
"x": {Name: "Alice", Count: 5, Version: 1},
"y": {Name: "Bob", Count: 3, Version: 1},
}
assert.Equal(t, expected, result)
}
func TestBindWithDependentComputation(t *testing.T) {
// Test Bind where the computation creates new keys based on input
monoid := MergeMonoid[string, TestState]()
result := F.Pipe1(
map[string]int{"x": 5},
Bind[int, string](monoid)(
func(str string) func(int) TestState {
return func(n int) TestState {
return TestState{Name: str, Count: n}
}
},
func(n int) map[string]string {
// Create a string based on the number
result := ""
for i := 0; i < n; i++ {
result += "a"
}
return map[string]string{"x": result}
},
),
)
expected := map[string]TestState{
"x": {Name: "aaaaa", Count: 5},
}
assert.Equal(t, expected, result)
}

85
v2/record/coverage.out Normal file
View File

@@ -0,0 +1,85 @@
mode: set
github.com/IBM/fp-go/v2/record/bind.go:33.40,35.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:71.144,73.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:79.27,81.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:87.27,89.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:92.80,94.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:129.135,131.2 1 0
github.com/IBM/fp-go/v2/record/eq.go:23.55,25.2 1 0
github.com/IBM/fp-go/v2/record/eq.go:28.56,30.2 1 1
github.com/IBM/fp-go/v2/record/monoid.go:25.75,27.2 1 1
github.com/IBM/fp-go/v2/record/monoid.go:30.63,32.2 1 1
github.com/IBM/fp-go/v2/record/monoid.go:35.64,37.2 1 1
github.com/IBM/fp-go/v2/record/monoid.go:40.59,42.2 1 1
github.com/IBM/fp-go/v2/record/record.go:28.51,30.2 1 1
github.com/IBM/fp-go/v2/record/record.go:33.54,35.2 1 1
github.com/IBM/fp-go/v2/record/record.go:38.47,40.2 1 1
github.com/IBM/fp-go/v2/record/record.go:43.49,45.2 1 1
github.com/IBM/fp-go/v2/record/record.go:48.72,50.2 1 1
github.com/IBM/fp-go/v2/record/record.go:53.92,55.2 1 1
github.com/IBM/fp-go/v2/record/record.go:57.80,59.2 1 1
github.com/IBM/fp-go/v2/record/record.go:61.92,63.2 1 1
github.com/IBM/fp-go/v2/record/record.go:65.84,67.2 1 1
github.com/IBM/fp-go/v2/record/record.go:69.96,71.2 1 1
github.com/IBM/fp-go/v2/record/record.go:73.71,75.2 1 1
github.com/IBM/fp-go/v2/record/record.go:77.83,79.2 1 1
github.com/IBM/fp-go/v2/record/record.go:81.87,83.2 1 1
github.com/IBM/fp-go/v2/record/record.go:85.75,87.2 1 1
github.com/IBM/fp-go/v2/record/record.go:89.69,91.2 1 1
github.com/IBM/fp-go/v2/record/record.go:93.73,95.2 1 1
github.com/IBM/fp-go/v2/record/record.go:97.81,99.2 1 1
github.com/IBM/fp-go/v2/record/record.go:101.85,103.2 1 1
github.com/IBM/fp-go/v2/record/record.go:106.65,108.2 1 1
github.com/IBM/fp-go/v2/record/record.go:111.67,113.2 1 1
github.com/IBM/fp-go/v2/record/record.go:116.52,118.2 1 1
github.com/IBM/fp-go/v2/record/record.go:120.84,122.2 1 0
github.com/IBM/fp-go/v2/record/record.go:125.70,127.2 1 1
github.com/IBM/fp-go/v2/record/record.go:130.43,132.2 1 1
github.com/IBM/fp-go/v2/record/record.go:135.47,137.2 1 1
github.com/IBM/fp-go/v2/record/record.go:139.60,141.2 1 1
github.com/IBM/fp-go/v2/record/record.go:143.62,145.2 1 1
github.com/IBM/fp-go/v2/record/record.go:147.65,149.2 1 1
github.com/IBM/fp-go/v2/record/record.go:151.68,153.2 1 1
github.com/IBM/fp-go/v2/record/record.go:155.63,157.2 1 1
github.com/IBM/fp-go/v2/record/record.go:160.55,162.2 1 1
github.com/IBM/fp-go/v2/record/record.go:165.103,167.2 1 1
github.com/IBM/fp-go/v2/record/record.go:170.91,172.2 1 1
github.com/IBM/fp-go/v2/record/record.go:175.72,177.2 1 1
github.com/IBM/fp-go/v2/record/record.go:180.84,182.2 1 1
github.com/IBM/fp-go/v2/record/record.go:185.49,187.2 1 1
github.com/IBM/fp-go/v2/record/record.go:190.52,192.2 1 1
github.com/IBM/fp-go/v2/record/record.go:195.46,197.2 1 1
github.com/IBM/fp-go/v2/record/record.go:199.124,201.2 1 0
github.com/IBM/fp-go/v2/record/record.go:203.112,205.2 1 1
github.com/IBM/fp-go/v2/record/record.go:207.125,209.2 1 0
github.com/IBM/fp-go/v2/record/record.go:211.113,213.2 1 1
github.com/IBM/fp-go/v2/record/record.go:216.85,218.2 1 1
github.com/IBM/fp-go/v2/record/record.go:221.141,223.2 1 1
github.com/IBM/fp-go/v2/record/record.go:226.129,228.2 1 0
github.com/IBM/fp-go/v2/record/record.go:231.86,233.2 1 1
github.com/IBM/fp-go/v2/record/record.go:236.98,238.2 1 0
github.com/IBM/fp-go/v2/record/record.go:241.64,243.2 1 1
github.com/IBM/fp-go/v2/record/record.go:246.104,248.2 1 0
github.com/IBM/fp-go/v2/record/record.go:251.92,253.2 1 0
github.com/IBM/fp-go/v2/record/record.go:256.108,258.2 1 1
github.com/IBM/fp-go/v2/record/record.go:261.86,263.2 1 0
github.com/IBM/fp-go/v2/record/record.go:266.120,268.2 1 0
github.com/IBM/fp-go/v2/record/record.go:271.69,273.2 1 1
github.com/IBM/fp-go/v2/record/record.go:276.71,278.2 1 1
github.com/IBM/fp-go/v2/record/record.go:280.78,282.2 1 1
github.com/IBM/fp-go/v2/record/record.go:284.74,286.2 1 1
github.com/IBM/fp-go/v2/record/record.go:289.51,291.2 1 1
github.com/IBM/fp-go/v2/record/record.go:294.80,296.2 1 1
github.com/IBM/fp-go/v2/record/record.go:305.88,307.2 1 0
github.com/IBM/fp-go/v2/record/record.go:314.73,316.2 1 1
github.com/IBM/fp-go/v2/record/record.go:324.60,326.2 1 0
github.com/IBM/fp-go/v2/record/record.go:332.55,334.2 1 1
github.com/IBM/fp-go/v2/record/record.go:336.105,338.2 1 1
github.com/IBM/fp-go/v2/record/record.go:340.106,342.2 1 1
github.com/IBM/fp-go/v2/record/record.go:344.48,346.2 1 1
github.com/IBM/fp-go/v2/record/semigroup.go:53.81,55.2 1 1
github.com/IBM/fp-go/v2/record/semigroup.go:80.69,82.2 1 1
github.com/IBM/fp-go/v2/record/semigroup.go:107.70,109.2 1 1
github.com/IBM/fp-go/v2/record/traverse.go:27.41,29.2 1 1
github.com/IBM/fp-go/v2/record/traverse.go:39.38,41.2 1 1
github.com/IBM/fp-go/v2/record/traverse.go:50.23,53.2 1 1

View File

@@ -20,11 +20,11 @@ import (
G "github.com/IBM/fp-go/v2/record/generic" G "github.com/IBM/fp-go/v2/record/generic"
) )
func Eq[K comparable, V any](e E.Eq[V]) E.Eq[map[K]V] { func Eq[K comparable, V any](e E.Eq[V]) E.Eq[Record[K, V]] {
return G.Eq[map[K]V](e) return G.Eq[Record[K, V]](e)
} }
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function // FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
func FromStrictEquals[K, V comparable]() E.Eq[map[K]V] { func FromStrictEquals[K, V comparable]() E.Eq[Record[K, V]] {
return G.FromStrictEquals[map[K]V]() return G.FromStrictEquals[Record[K, V]]()
} }

View File

@@ -26,7 +26,7 @@ import (
Mo "github.com/IBM/fp-go/v2/monoid" Mo "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ord" "github.com/IBM/fp-go/v2/ord"
T "github.com/IBM/fp-go/v2/tuple" "github.com/IBM/fp-go/v2/pair"
) )
func IsEmpty[M ~map[K]V, K comparable, V any](r M) bool { func IsEmpty[M ~map[K]V, K comparable, V any](r M) bool {
@@ -59,9 +59,9 @@ func ValuesOrd[M ~map[K]V, GV ~[]V, K comparable, V any](o ord.Ord[K]) func(r M)
func collectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K], r M, f func(K, V) R) GR { func collectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K], r M, f func(K, V) R) GR {
// create the entries // create the entries
entries := toEntriesOrd[M, []T.Tuple2[K, V]](o, r) entries := toEntriesOrd[M, []pair.Pair[K, V]](o, r)
// collect this array // collect this array
ft := T.Tupled2(f) ft := pair.Paired(f)
count := len(entries) count := len(entries)
result := make(GR, count) result := make(GR, count)
for i := count - 1; i >= 0; i-- { for i := count - 1; i >= 0; i-- {
@@ -73,13 +73,13 @@ func collectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K], r M,
func reduceOrd[M ~map[K]V, K comparable, V, R any](o ord.Ord[K], r M, f func(K, R, V) R, initial R) R { func reduceOrd[M ~map[K]V, K comparable, V, R any](o ord.Ord[K], r M, f func(K, R, V) R, initial R) R {
// create the entries // create the entries
entries := toEntriesOrd[M, []T.Tuple2[K, V]](o, r) entries := toEntriesOrd[M, []pair.Pair[K, V]](o, r)
// collect this array // collect this array
current := initial current := initial
count := len(entries) count := len(entries)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
t := entries[i] t := entries[i]
current = f(T.First(t), current, T.Second(t)) current = f(pair.Head(t), current, pair.Tail(t))
} }
// done // done
return current return current
@@ -318,32 +318,32 @@ func Size[M ~map[K]V, K comparable, V any](r M) int {
return len(r) return len(r)
} }
func ToArray[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT { func ToArray[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](r M) GT {
return collect[M, GT](r, T.MakeTuple2[K, V]) return collect[M, GT](r, pair.MakePair[K, V])
} }
func toEntriesOrd[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](o ord.Ord[K], r M) GT { func toEntriesOrd[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](o ord.Ord[K], r M) GT {
// total number of elements // total number of elements
count := len(r) count := len(r)
// produce an array that we can sort by key // produce an array that we can sort by key
entries := make(GT, count) entries := make(GT, count)
idx := 0 idx := 0
for k, v := range r { for k, v := range r {
entries[idx] = T.MakeTuple2(k, v) entries[idx] = pair.MakePair(k, v)
idx++ idx++
} }
sort.Slice(entries, func(i, j int) bool { sort.Slice(entries, func(i, j int) bool {
return o.Compare(T.First(entries[i]), T.First(entries[j])) < 0 return o.Compare(pair.Head(entries[i]), pair.Head(entries[j])) < 0
}) })
// final entries // final entries
return entries return entries
} }
func ToEntriesOrd[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](o ord.Ord[K]) func(r M) GT { func ToEntriesOrd[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](o ord.Ord[K]) func(r M) GT {
return F.Bind1st(toEntriesOrd[M, GT, K, V], o) return F.Bind1st(toEntriesOrd[M, GT, K, V], o)
} }
func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT { func ToEntries[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](r M) GT {
return ToArray[M, GT](r) return ToArray[M, GT](r)
} }
@@ -351,7 +351,7 @@ func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
// its values into a tuple. The key and value are then used to populate the map. Duplicate // its values into a tuple. The key and value are then used to populate the map. Duplicate
// values are resolved via the provided [Mg.Magma] // values are resolved via the provided [Mg.Magma]
func FromFoldableMap[ func FromFoldableMap[
FCT ~func(A) T.Tuple2[K, V], FCT ~func(A) pair.Pair[K, V],
HKTA any, HKTA any,
FOLDABLE ~func(func(M, A) M, M) func(HKTA) M, FOLDABLE ~func(func(M, A) M, M) func(HKTA) M,
M ~map[K]V, M ~map[K]V,
@@ -364,12 +364,12 @@ func FromFoldableMap[
dst = make(M) dst = make(M)
} }
e := f(a) e := f(a)
k := T.First(e) k := pair.Head(e)
old, ok := dst[k] old, ok := dst[k]
if ok { if ok {
dst[k] = m.Concat(old, T.Second(e)) dst[k] = m.Concat(old, pair.Tail(e))
} else { } else {
dst[k] = T.Second(e) dst[k] = pair.Tail(e)
} }
return dst return dst
}, Empty[M]()) }, Empty[M]())
@@ -378,15 +378,15 @@ func FromFoldableMap[
func FromFoldable[ func FromFoldable[
HKTA any, HKTA any,
FOLDABLE ~func(func(M, T.Tuple2[K, V]) M, M) func(HKTA) M, FOLDABLE ~func(func(M, pair.Pair[K, V]) M, M) func(HKTA) M,
M ~map[K]V, M ~map[K]V,
K comparable, K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) M { V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) M {
return FromFoldableMap[func(T.Tuple2[K, V]) T.Tuple2[K, V]](m, red)(F.Identity[T.Tuple2[K, V]]) return FromFoldableMap[func(pair.Pair[K, V]) pair.Pair[K, V]](m, red)(F.Identity[pair.Pair[K, V]])
} }
func FromArrayMap[ func FromArrayMap[
FCT ~func(A) T.Tuple2[K, V], FCT ~func(A) pair.Pair[K, V],
GA ~[]A, GA ~[]A,
M ~map[K]V, M ~map[K]V,
A any, A any,
@@ -396,17 +396,17 @@ func FromArrayMap[
} }
func FromArray[ func FromArray[
GA ~[]T.Tuple2[K, V], GA ~[]pair.Pair[K, V],
M ~map[K]V, M ~map[K]V,
K comparable, K comparable,
V any](m Mg.Magma[V]) func(fa GA) M { V any](m Mg.Magma[V]) func(fa GA) M {
return FromFoldable(m, F.Bind23of3(RAG.Reduce[GA, T.Tuple2[K, V], M])) return FromFoldable(m, F.Bind23of3(RAG.Reduce[GA, pair.Pair[K, V], M]))
} }
func FromEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](fa GT) M { func FromEntries[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](fa GT) M {
m := make(M) m := make(M)
for _, t := range fa { for _, t := range fa {
upsertAtReadWrite(m, t.F1, t.F2) upsertAtReadWrite(m, pair.Head(t), pair.Tail(t))
} }
return m return m
} }

View File

@@ -16,27 +16,34 @@
package record package record
import ( import (
M "github.com/IBM/fp-go/v2/monoid"
G "github.com/IBM/fp-go/v2/record/generic" G "github.com/IBM/fp-go/v2/record/generic"
S "github.com/IBM/fp-go/v2/semigroup" S "github.com/IBM/fp-go/v2/semigroup"
) )
// UnionMonoid computes the union of two maps of the same type // UnionMonoid computes the union of two maps of the same type
func UnionMonoid[K comparable, V any](s S.Semigroup[V]) M.Monoid[map[K]V] { //
return G.UnionMonoid[map[K]V](s) //go:inline
func UnionMonoid[K comparable, V any](s S.Semigroup[V]) Monoid[Record[K, V]] {
return G.UnionMonoid[Record[K, V]](s)
} }
// UnionLastMonoid computes the union of two maps of the same type giving the last map precedence // UnionLastMonoid computes the union of two maps of the same type giving the last map precedence
func UnionLastMonoid[K comparable, V any]() M.Monoid[map[K]V] { //
return G.UnionLastMonoid[map[K]V]() //go:inline
func UnionLastMonoid[K comparable, V any]() Monoid[Record[K, V]] {
return G.UnionLastMonoid[Record[K, V]]()
} }
// UnionFirstMonoid computes the union of two maps of the same type giving the first map precedence // UnionFirstMonoid computes the union of two maps of the same type giving the first map precedence
func UnionFirstMonoid[K comparable, V any]() M.Monoid[map[K]V] { //
return G.UnionFirstMonoid[map[K]V]() //go:inline
func UnionFirstMonoid[K comparable, V any]() Monoid[Record[K, V]] {
return G.UnionFirstMonoid[Record[K, V]]()
} }
// MergeMonoid computes the union of two maps of the same type giving the last map precedence // MergeMonoid computes the union of two maps of the same type giving the last map precedence
func MergeMonoid[K comparable, V any]() M.Monoid[map[K]V] { //
return G.UnionLastMonoid[map[K]V]() //go:inline
func MergeMonoid[K comparable, V any]() Monoid[Record[K, V]] {
return G.UnionLastMonoid[Record[K, V]]()
} }

View File

@@ -16,295 +16,316 @@
package record package record
import ( import (
EM "github.com/IBM/fp-go/v2/endomorphism"
Mg "github.com/IBM/fp-go/v2/magma" Mg "github.com/IBM/fp-go/v2/magma"
Mo "github.com/IBM/fp-go/v2/monoid" "github.com/IBM/fp-go/v2/option"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ord" "github.com/IBM/fp-go/v2/ord"
G "github.com/IBM/fp-go/v2/record/generic" G "github.com/IBM/fp-go/v2/record/generic"
T "github.com/IBM/fp-go/v2/tuple"
) )
// IsEmpty tests if a map is empty // IsEmpty tests if a map is empty
func IsEmpty[K comparable, V any](r map[K]V) bool { func IsEmpty[K comparable, V any](r Record[K, V]) bool {
return G.IsEmpty(r) return G.IsEmpty(r)
} }
// IsNonEmpty tests if a map is not empty // IsNonEmpty tests if a map is not empty
func IsNonEmpty[K comparable, V any](r map[K]V) bool { func IsNonEmpty[K comparable, V any](r Record[K, V]) bool {
return G.IsNonEmpty(r) return G.IsNonEmpty(r)
} }
// Keys returns the key in a map // Keys returns the key in a map
func Keys[K comparable, V any](r map[K]V) []K { func Keys[K comparable, V any](r Record[K, V]) []K {
return G.Keys[map[K]V, []K](r) return G.Keys[Record[K, V], []K](r)
} }
// Values returns the values in a map // Values returns the values in a map
func Values[K comparable, V any](r map[K]V) []V { func Values[K comparable, V any](r Record[K, V]) []V {
return G.Values[map[K]V, []V](r) return G.Values[Record[K, V], []V](r)
} }
// Collect applies a collector function to the key value pairs in a map and returns the result as an array // Collect applies a collector function to the key value pairs in a map and returns the result as an array
func Collect[K comparable, V, R any](f func(K, V) R) func(map[K]V) []R { func Collect[K comparable, V, R any](f func(K, V) R) func(Record[K, V]) []R {
return G.Collect[map[K]V, []R](f) return G.Collect[Record[K, V], []R](f)
} }
// CollectOrd applies a collector function to the key value pairs in a map and returns the result as an array // CollectOrd applies a collector function to the key value pairs in a map and returns the result as an array
func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(map[K]V) []R { func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(Record[K, V]) []R {
return G.CollectOrd[map[K]V, []R](o) return G.CollectOrd[Record[K, V], []R](o)
} }
func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(map[K]V) R { // Reduce reduces a map to a single value by applying a reducer function to each value
return G.Reduce[map[K]V](f, initial) func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(Record[K, V]) R {
return G.Reduce[Record[K, V]](f, initial)
} }
func ReduceWithIndex[K comparable, V, R any](f func(K, R, V) R, initial R) func(map[K]V) R { // ReduceWithIndex reduces a map to a single value by applying a reducer function to each key-value pair
return G.ReduceWithIndex[map[K]V](f, initial) func ReduceWithIndex[K comparable, V, R any](f func(K, R, V) R, initial R) func(Record[K, V]) R {
return G.ReduceWithIndex[Record[K, V]](f, initial)
} }
func ReduceRef[K comparable, V, R any](f func(R, *V) R, initial R) func(map[K]V) R { // ReduceRef reduces a map to a single value by applying a reducer function to each value reference
return G.ReduceRef[map[K]V](f, initial) func ReduceRef[K comparable, V, R any](f func(R, *V) R, initial R) func(Record[K, V]) R {
return G.ReduceRef[Record[K, V]](f, initial)
} }
func ReduceRefWithIndex[K comparable, V, R any](f func(K, R, *V) R, initial R) func(map[K]V) R { // ReduceRefWithIndex reduces a map to a single value by applying a reducer function to each key-value pair with value references
return G.ReduceRefWithIndex[map[K]V](f, initial) func ReduceRefWithIndex[K comparable, V, R any](f func(K, R, *V) R, initial R) func(Record[K, V]) R {
return G.ReduceRefWithIndex[Record[K, V]](f, initial)
} }
func MonadMap[K comparable, V, R any](r map[K]V, f func(V) R) map[K]R { // MonadMap transforms each value in a map using the provided function
return G.MonadMap[map[K]V, map[K]R](r, f) func MonadMap[K comparable, V, R any](r Record[K, V], f func(V) R) Record[K, R] {
return G.MonadMap[Record[K, V], Record[K, R]](r, f)
} }
func MonadMapWithIndex[K comparable, V, R any](r map[K]V, f func(K, V) R) map[K]R { // MonadMapWithIndex transforms each key-value pair in a map using the provided function
return G.MonadMapWithIndex[map[K]V, map[K]R](r, f) func MonadMapWithIndex[K comparable, V, R any](r Record[K, V], f func(K, V) R) Record[K, R] {
return G.MonadMapWithIndex[Record[K, V], Record[K, R]](r, f)
} }
func MonadMapRefWithIndex[K comparable, V, R any](r map[K]V, f func(K, *V) R) map[K]R { // MonadMapRefWithIndex transforms each key-value pair in a map using the provided function with value references
return G.MonadMapRefWithIndex[map[K]V, map[K]R](r, f) func MonadMapRefWithIndex[K comparable, V, R any](r Record[K, V], f func(K, *V) R) Record[K, R] {
return G.MonadMapRefWithIndex[Record[K, V], Record[K, R]](r, f)
} }
func MonadMapRef[K comparable, V, R any](r map[K]V, f func(*V) R) map[K]R { // MonadMapRef transforms each value in a map using the provided function with value references
return G.MonadMapRef[map[K]V, map[K]R](r, f) func MonadMapRef[K comparable, V, R any](r Record[K, V], f func(*V) R) Record[K, R] {
return G.MonadMapRef[Record[K, V], Record[K, R]](r, f)
} }
func Map[K comparable, V, R any](f func(V) R) func(map[K]V) map[K]R { // Map returns a function that transforms each value in a map using the provided function
return G.Map[map[K]V, map[K]R](f) func Map[K comparable, V, R any](f func(V) R) Operator[K, V, R] {
return G.Map[Record[K, V], Record[K, R]](f)
} }
func MapRef[K comparable, V, R any](f func(*V) R) func(map[K]V) map[K]R { // MapRef returns a function that transforms each value in a map using the provided function with value references
return G.MapRef[map[K]V, map[K]R](f) func MapRef[K comparable, V, R any](f func(*V) R) Operator[K, V, R] {
return G.MapRef[Record[K, V], Record[K, R]](f)
} }
func MapWithIndex[K comparable, V, R any](f func(K, V) R) func(map[K]V) map[K]R { // MapWithIndex returns a function that transforms each key-value pair in a map using the provided function
return G.MapWithIndex[map[K]V, map[K]R](f) func MapWithIndex[K comparable, V, R any](f func(K, V) R) Operator[K, V, R] {
return G.MapWithIndex[Record[K, V], Record[K, R]](f)
} }
func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) func(map[K]V) map[K]R { // MapRefWithIndex returns a function that transforms each key-value pair in a map using the provided function with value references
return G.MapRefWithIndex[map[K]V, map[K]R](f) func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) Operator[K, V, R] {
return G.MapRefWithIndex[Record[K, V], Record[K, R]](f)
} }
// Lookup returns the entry for a key in a map if it exists // Lookup returns the entry for a key in a map if it exists
func Lookup[V any, K comparable](k K) func(map[K]V) O.Option[V] { func Lookup[V any, K comparable](k K) option.Kleisli[Record[K, V], V] {
return G.Lookup[map[K]V](k) return G.Lookup[Record[K, V]](k)
} }
// MonadLookup returns the entry for a key in a map if it exists // MonadLookup returns the entry for a key in a map if it exists
func MonadLookup[V any, K comparable](m map[K]V, k K) O.Option[V] { func MonadLookup[V any, K comparable](m Record[K, V], k K) Option[V] {
return G.MonadLookup(m, k) return G.MonadLookup(m, k)
} }
// Has tests if a key is contained in a map // Has tests if a key is contained in a map
func Has[K comparable, V any](k K, r map[K]V) bool { func Has[K comparable, V any](k K, r Record[K, V]) bool {
return G.Has(k, r) return G.Has(k, r)
} }
func Union[K comparable, V any](m Mg.Magma[V]) func(map[K]V) func(map[K]V) map[K]V { // Union combines two maps using the provided Magma to resolve conflicts for duplicate keys
return G.Union[map[K]V](m) func Union[K comparable, V any](m Mg.Magma[V]) func(Record[K, V]) Operator[K, V, V] {
return G.Union[Record[K, V]](m)
} }
// Merge combines two maps giving the values in the right one precedence. Also refer to [MergeMonoid] // Merge combines two maps giving the values in the right one precedence. Also refer to [MergeMonoid]
func Merge[K comparable, V any](right map[K]V) func(map[K]V) map[K]V { func Merge[K comparable, V any](right Record[K, V]) Operator[K, V, V] {
return G.Merge(right) return G.Merge(right)
} }
// Empty creates an empty map // Empty creates an empty map
func Empty[K comparable, V any]() map[K]V { func Empty[K comparable, V any]() Record[K, V] {
return G.Empty[map[K]V]() return G.Empty[Record[K, V]]()
} }
// Size returns the number of elements in a map // Size returns the number of elements in a map
func Size[K comparable, V any](r map[K]V) int { func Size[K comparable, V any](r Record[K, V]) int {
return G.Size(r) return G.Size(r)
} }
func ToArray[K comparable, V any](r map[K]V) []T.Tuple2[K, V] { // ToArray converts a map to an array of key-value pairs
return G.ToArray[map[K]V, []T.Tuple2[K, V]](r) func ToArray[K comparable, V any](r Record[K, V]) Entries[K, V] {
return G.ToArray[Record[K, V], Entries[K, V]](r)
} }
func ToEntries[K comparable, V any](r map[K]V) []T.Tuple2[K, V] { // ToEntries converts a map to an array of key-value pairs (alias for ToArray)
return G.ToEntries[map[K]V, []T.Tuple2[K, V]](r) func ToEntries[K comparable, V any](r Record[K, V]) Entries[K, V] {
return G.ToEntries[Record[K, V], Entries[K, V]](r)
} }
func FromEntries[K comparable, V any](fa []T.Tuple2[K, V]) map[K]V { // FromEntries creates a map from an array of key-value pairs
return G.FromEntries[map[K]V](fa) func FromEntries[K comparable, V any](fa Entries[K, V]) Record[K, V] {
return G.FromEntries[Record[K, V]](fa)
} }
func UpsertAt[K comparable, V any](k K, v V) func(map[K]V) map[K]V { // UpsertAt returns a function that inserts or updates a key-value pair in a map
return G.UpsertAt[map[K]V](k, v) func UpsertAt[K comparable, V any](k K, v V) Operator[K, V, V] {
return G.UpsertAt[Record[K, V]](k, v)
} }
func DeleteAt[K comparable, V any](k K) func(map[K]V) map[K]V { // DeleteAt returns a function that removes a key from a map
return G.DeleteAt[map[K]V](k) func DeleteAt[K comparable, V any](k K) Operator[K, V, V] {
return G.DeleteAt[Record[K, V]](k)
} }
// Singleton creates a new map with a single entry // Singleton creates a new map with a single entry
func Singleton[K comparable, V any](k K, v V) map[K]V { func Singleton[K comparable, V any](k K, v V) Record[K, V] {
return G.Singleton[map[K]V](k, v) return G.Singleton[Record[K, V]](k, v)
} }
// FilterMapWithIndex creates a new map with only the elements for which the transformation function creates a Some // FilterMapWithIndex creates a new map with only the elements for which the transformation function creates a Some
func FilterMapWithIndex[K comparable, V1, V2 any](f func(K, V1) O.Option[V2]) func(map[K]V1) map[K]V2 { func FilterMapWithIndex[K comparable, V1, V2 any](f func(K, V1) Option[V2]) Operator[K, V1, V2] {
return G.FilterMapWithIndex[map[K]V1, map[K]V2](f) return G.FilterMapWithIndex[Record[K, V1], Record[K, V2]](f)
} }
// FilterMap creates a new map with only the elements for which the transformation function creates a Some // FilterMap creates a new map with only the elements for which the transformation function creates a Some
func FilterMap[K comparable, V1, V2 any](f func(V1) O.Option[V2]) func(map[K]V1) map[K]V2 { func FilterMap[K comparable, V1, V2 any](f option.Kleisli[V1, V2]) Operator[K, V1, V2] {
return G.FilterMap[map[K]V1, map[K]V2](f) return G.FilterMap[Record[K, V1], Record[K, V2]](f)
} }
// Filter creates a new map with only the elements that match the predicate // Filter creates a new map with only the elements that match the predicate
func Filter[K comparable, V any](f func(K) bool) func(map[K]V) map[K]V { func Filter[K comparable, V any](f Predicate[K]) Operator[K, V, V] {
return G.Filter[map[K]V](f) return G.Filter[Record[K, V]](f)
} }
// FilterWithIndex creates a new map with only the elements that match the predicate // FilterWithIndex creates a new map with only the elements that match the predicate
func FilterWithIndex[K comparable, V any](f func(K, V) bool) func(map[K]V) map[K]V { func FilterWithIndex[K comparable, V any](f PredicateWithIndex[K, V]) Operator[K, V, V] {
return G.FilterWithIndex[map[K]V](f) return G.FilterWithIndex[Record[K, V]](f)
} }
// IsNil checks if the map is set to nil // IsNil checks if the map is set to nil
func IsNil[K comparable, V any](m map[K]V) bool { func IsNil[K comparable, V any](m Record[K, V]) bool {
return G.IsNil(m) return G.IsNil(m)
} }
// IsNonNil checks if the map is set to nil // IsNonNil checks if the map is set to nil
func IsNonNil[K comparable, V any](m map[K]V) bool { func IsNonNil[K comparable, V any](m Record[K, V]) bool {
return G.IsNonNil(m) return G.IsNonNil(m)
} }
// ConstNil return a nil map // ConstNil return a nil map
func ConstNil[K comparable, V any]() map[K]V { func ConstNil[K comparable, V any]() Record[K, V] {
return map[K]V(nil) return Record[K, V](nil)
} }
func MonadChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2], r map[K]V1, f func(K, V1) map[K]V2) map[K]V2 { // MonadChainWithIndex chains a map transformation function that produces maps, combining results using the provided Monoid
func MonadChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]], r Record[K, V1], f KleisliWithIndex[K, V1, V2]) Record[K, V2] {
return G.MonadChainWithIndex(m, r, f) return G.MonadChainWithIndex(m, r, f)
} }
func MonadChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2], r map[K]V1, f func(V1) map[K]V2) map[K]V2 { // MonadChain chains a map transformation function that produces maps, combining results using the provided Monoid
func MonadChain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]], r Record[K, V1], f Kleisli[K, V1, V2]) Record[K, V2] {
return G.MonadChain(m, r, f) return G.MonadChain(m, r, f)
} }
func ChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) map[K]V2) func(map[K]V1) map[K]V2 { // ChainWithIndex returns a function that chains a map transformation function that produces maps, combining results using the provided Monoid
return G.ChainWithIndex[map[K]V1](m) func ChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(KleisliWithIndex[K, V1, V2]) Operator[K, V1, V2] {
return G.ChainWithIndex[Record[K, V1]](m)
} }
func Chain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) map[K]V2) func(map[K]V1) map[K]V2 { // Chain returns a function that chains a map transformation function that produces maps, combining results using the provided Monoid
return G.Chain[map[K]V1](m) func Chain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(Kleisli[K, V1, V2]) Operator[K, V1, V2] {
return G.Chain[Record[K, V1]](m)
} }
// Flatten converts a nested map into a regular map // Flatten converts a nested map into a regular map
func Flatten[K comparable, V any](m Mo.Monoid[map[K]V]) func(map[K]map[K]V) map[K]V { func Flatten[K comparable, V any](m Monoid[Record[K, V]]) func(Record[K, Record[K, V]]) Record[K, V] {
return G.Flatten[map[K]map[K]V](m) return G.Flatten[Record[K, Record[K, V]]](m)
} }
// FilterChainWithIndex creates a new map with only the elements for which the transformation function creates a Some // FilterChainWithIndex creates a new map with only the elements for which the transformation function creates a Some
func FilterChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) O.Option[map[K]V2]) func(map[K]V1) map[K]V2 { func FilterChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(func(K, V1) Option[Record[K, V2]]) Operator[K, V1, V2] {
return G.FilterChainWithIndex[map[K]V1](m) return G.FilterChainWithIndex[Record[K, V1]](m)
} }
// FilterChain creates a new map with only the elements for which the transformation function creates a Some // FilterChain creates a new map with only the elements for which the transformation function creates a Some
func FilterChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) O.Option[map[K]V2]) func(map[K]V1) map[K]V2 { func FilterChain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(option.Kleisli[V1, Record[K, V2]]) Operator[K, V1, V2] {
return G.FilterChain[map[K]V1](m) return G.FilterChain[Record[K, V1]](m)
} }
// FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid. // FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid.
func FoldMap[K comparable, A, B any](m Mo.Monoid[B]) func(func(A) B) func(map[K]A) B { func FoldMap[K comparable, A, B any](m Monoid[B]) func(func(A) B) func(Record[K, A]) B {
return G.FoldMap[map[K]A](m) return G.FoldMap[Record[K, A]](m)
} }
// FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid. // FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid.
func FoldMapWithIndex[K comparable, A, B any](m Mo.Monoid[B]) func(func(K, A) B) func(map[K]A) B { func FoldMapWithIndex[K comparable, A, B any](m Monoid[B]) func(func(K, A) B) func(Record[K, A]) B {
return G.FoldMapWithIndex[map[K]A](m) return G.FoldMapWithIndex[Record[K, A]](m)
} }
// Fold folds the record using the provided Monoid. // Fold folds the record using the provided Monoid.
func Fold[K comparable, A any](m Mo.Monoid[A]) func(map[K]A) A { func Fold[K comparable, A any](m Monoid[A]) func(Record[K, A]) A {
return G.Fold[map[K]A](m) return G.Fold[Record[K, A]](m)
} }
// ReduceOrdWithIndex reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order // ReduceOrdWithIndex reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order
func ReduceOrdWithIndex[V, R any, K comparable](o ord.Ord[K]) func(func(K, R, V) R, R) func(map[K]V) R { func ReduceOrdWithIndex[V, R any, K comparable](o ord.Ord[K]) func(func(K, R, V) R, R) func(Record[K, V]) R {
return G.ReduceOrdWithIndex[map[K]V, K, V, R](o) return G.ReduceOrdWithIndex[Record[K, V], K, V, R](o)
} }
// ReduceOrd reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order // ReduceOrd reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order
func ReduceOrd[V, R any, K comparable](o ord.Ord[K]) func(func(R, V) R, R) func(map[K]V) R { func ReduceOrd[V, R any, K comparable](o ord.Ord[K]) func(func(R, V) R, R) func(Record[K, V]) R {
return G.ReduceOrd[map[K]V, K, V, R](o) return G.ReduceOrd[Record[K, V], K, V, R](o)
} }
// FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order // FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order
func FoldMapOrd[A, B any, K comparable](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(A) B) func(map[K]A) B { func FoldMapOrd[A, B any, K comparable](o ord.Ord[K]) func(m Monoid[B]) func(func(A) B) func(Record[K, A]) B {
return G.FoldMapOrd[map[K]A, K, A, B](o) return G.FoldMapOrd[Record[K, A], K, A, B](o)
} }
// Fold folds the record using the provided Monoid with the items passed in the given order // Fold folds the record using the provided Monoid with the items passed in the given order
func FoldOrd[A any, K comparable](o ord.Ord[K]) func(m Mo.Monoid[A]) func(map[K]A) A { func FoldOrd[A any, K comparable](o ord.Ord[K]) func(m Monoid[A]) func(Record[K, A]) A {
return G.FoldOrd[map[K]A](o) return G.FoldOrd[Record[K, A]](o)
} }
// FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order // FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order
func FoldMapOrdWithIndex[K comparable, A, B any](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(K, A) B) func(map[K]A) B { func FoldMapOrdWithIndex[K comparable, A, B any](o ord.Ord[K]) func(m Monoid[B]) func(func(K, A) B) func(Record[K, A]) B {
return G.FoldMapOrdWithIndex[map[K]A, K, A, B](o) return G.FoldMapOrdWithIndex[Record[K, A], K, A, B](o)
} }
// KeysOrd returns the keys in the map in their given order // KeysOrd returns the keys in the map in their given order
func KeysOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []K { func KeysOrd[V any, K comparable](o ord.Ord[K]) func(r Record[K, V]) []K {
return G.KeysOrd[map[K]V, []K](o) return G.KeysOrd[Record[K, V], []K](o)
} }
// ValuesOrd returns the values in the map ordered by their keys in the given order // ValuesOrd returns the values in the map ordered by their keys in the given order
func ValuesOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []V { func ValuesOrd[V any, K comparable](o ord.Ord[K]) func(r Record[K, V]) []V {
return G.ValuesOrd[map[K]V, []V](o) return G.ValuesOrd[Record[K, V], []V](o)
} }
func MonadFlap[B any, K comparable, A any](fab map[K]func(A) B, a A) map[K]B { // MonadFlap applies a value to a map of functions, producing a map of results
return G.MonadFlap[map[K]func(A) B, map[K]B](fab, a) func MonadFlap[B any, K comparable, A any](fab Record[K, func(A) B], a A) Record[K, B] {
return G.MonadFlap[Record[K, func(A) B], Record[K, B]](fab, a)
} }
func Flap[B any, K comparable, A any](a A) func(map[K]func(A) B) map[K]B { // Flap returns a function that applies a value to a map of functions, producing a map of results
return G.Flap[map[K]func(A) B, map[K]B](a) func Flap[B any, K comparable, A any](a A) Operator[K, func(A) B, B] {
return G.Flap[Record[K, func(A) B], Record[K, B]](a)
} }
// Copy creates a shallow copy of the map // Copy creates a shallow copy of the map
func Copy[K comparable, V any](m map[K]V) map[K]V { func Copy[K comparable, V any](m Record[K, V]) Record[K, V] {
return G.Copy(m) return G.Copy(m)
} }
// Clone creates a deep copy of the map using the provided endomorphism to clone the values // Clone creates a deep copy of the map using the provided endomorphism to clone the values
func Clone[K comparable, V any](f EM.Endomorphism[V]) EM.Endomorphism[map[K]V] { func Clone[K comparable, V any](f Endomorphism[V]) Endomorphism[Record[K, V]] {
return G.Clone[map[K]V](f) return G.Clone[Record[K, V]](f)
} }
// FromFoldableMap converts from a reducer to a map // FromFoldableMap converts from a reducer to a map
// Duplicate keys are resolved by the provided [Mg.Magma] // Duplicate keys are resolved by the provided [Mg.Magma]
func FromFoldableMap[ func FromFoldableMap[
FOLDABLE ~func(func(map[K]V, A) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function FOLDABLE ~func(func(Record[K, V], A) Record[K, V], Record[K, V]) func(HKTA) Record[K, V], // the reduce function
A any, A any,
HKTA any, HKTA any,
K comparable, K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(f func(A) T.Tuple2[K, V]) func(fa HKTA) map[K]V { V any](m Mg.Magma[V], red FOLDABLE) func(f func(A) Entry[K, V]) Kleisli[K, HKTA, V] {
return G.FromFoldableMap[func(A) T.Tuple2[K, V]](m, red) return G.FromFoldableMap[func(A) Entry[K, V]](m, red)
} }
// FromArrayMap converts from an array to a map // FromArrayMap converts from an array to a map
@@ -312,17 +333,17 @@ func FromFoldableMap[
func FromArrayMap[ func FromArrayMap[
A any, A any,
K comparable, K comparable,
V any](m Mg.Magma[V]) func(f func(A) T.Tuple2[K, V]) func(fa []A) map[K]V { V any](m Mg.Magma[V]) func(f func(A) Entry[K, V]) Kleisli[K, []A, V] {
return G.FromArrayMap[func(A) T.Tuple2[K, V], []A, map[K]V](m) return G.FromArrayMap[func(A) Entry[K, V], []A, Record[K, V]](m)
} }
// FromFoldable converts from a reducer to a map // FromFoldable converts from a reducer to a map
// Duplicate keys are resolved by the provided [Mg.Magma] // Duplicate keys are resolved by the provided [Mg.Magma]
func FromFoldable[ func FromFoldable[
HKTA any, HKTA any,
FOLDABLE ~func(func(map[K]V, T.Tuple2[K, V]) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function FOLDABLE ~func(func(Record[K, V], Entry[K, V]) Record[K, V], Record[K, V]) func(HKTA) Record[K, V], // the reduce function
K comparable, K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) map[K]V { V any](m Mg.Magma[V], red FOLDABLE) Kleisli[K, HKTA, V] {
return G.FromFoldable(m, red) return G.FromFoldable(m, red)
} }
@@ -330,14 +351,21 @@ func FromFoldable[
// Duplicate keys are resolved by the provided [Mg.Magma] // Duplicate keys are resolved by the provided [Mg.Magma]
func FromArray[ func FromArray[
K comparable, K comparable,
V any](m Mg.Magma[V]) func(fa []T.Tuple2[K, V]) map[K]V { V any](m Mg.Magma[V]) Kleisli[K, Entries[K, V], V] {
return G.FromArray[[]T.Tuple2[K, V], map[K]V](m) return G.FromArray[Entries[K, V], Record[K, V]](m)
} }
func MonadAp[A any, K comparable, B any](m Mo.Monoid[map[K]B], fab map[K]func(A) B, fa map[K]A) map[K]B { // MonadAp applies a map of functions to a map of values, combining results using the provided Monoid
func MonadAp[A any, K comparable, B any](m Monoid[Record[K, B]], fab Record[K, func(A) B], fa Record[K, A]) Record[K, B] {
return G.MonadAp(m, fab, fa) return G.MonadAp(m, fab, fa)
} }
func Ap[A any, K comparable, B any](m Mo.Monoid[map[K]B]) func(fa map[K]A) func(map[K]func(A) B) map[K]B { // Ap returns a function that applies a map of functions to a map of values, combining results using the provided Monoid
return G.Ap[map[K]B, map[K]func(A) B, map[K]A](m) func Ap[A any, K comparable, B any](m Monoid[Record[K, B]]) func(fa Record[K, A]) Operator[K, func(A) B, B] {
return G.Ap[Record[K, B], Record[K, func(A) B], Record[K, A]](m)
}
// Of creates a map with a single key-value pair
func Of[K comparable, A any](k K, a A) Record[K, A] {
return Record[K, A]{k: a}
} }

View File

@@ -25,8 +25,8 @@ import (
"github.com/IBM/fp-go/v2/internal/utils" "github.com/IBM/fp-go/v2/internal/utils"
Mg "github.com/IBM/fp-go/v2/magma" Mg "github.com/IBM/fp-go/v2/magma"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string" S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -156,7 +156,7 @@ func TestFromArrayMap(t *testing.T) {
src1 := A.From("a", "b", "c", "a") src1 := A.From("a", "b", "c", "a")
frm := FromArrayMap[string, string](Mg.Second[string]()) frm := FromArrayMap[string, string](Mg.Second[string]())
f := frm(T.Replicate2[string]) f := frm(P.Of[string])
res1 := f(src1) res1 := f(src1)
@@ -198,3 +198,555 @@ func TestHas(t *testing.T) {
assert.True(t, Has("a", nonEmpty)) assert.True(t, Has("a", nonEmpty))
assert.False(t, Has("c", nonEmpty)) assert.False(t, Has("c", nonEmpty))
} }
func TestCollect(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
collector := Collect(func(k string, v int) string {
return fmt.Sprintf("%s=%d", k, v)
})
result := collector(data)
sort.Strings(result)
assert.Equal(t, []string{"a=1", "b=2", "c=3"}, result)
}
func TestCollectOrd(t *testing.T) {
data := map[string]int{
"c": 3,
"a": 1,
"b": 2,
}
collector := CollectOrd[int, string](S.Ord)(func(k string, v int) string {
return fmt.Sprintf("%s=%d", k, v)
})
result := collector(data)
assert.Equal(t, []string{"a=1", "b=2", "c=3"}, result)
}
func TestReduce(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
sum := Reduce[string](func(acc, v int) int {
return acc + v
}, 0)
result := sum(data)
assert.Equal(t, 6, result)
}
func TestReduceWithIndex(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
concat := ReduceWithIndex(func(k string, acc string, v int) string {
if acc == "" {
return fmt.Sprintf("%s:%d", k, v)
}
return fmt.Sprintf("%s,%s:%d", acc, k, v)
}, "")
result := concat(data)
// Result order is non-deterministic, so check it contains all parts
assert.Contains(t, result, "a:1")
assert.Contains(t, result, "b:2")
assert.Contains(t, result, "c:3")
}
func TestMonadMap(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
result := MonadMap(data, func(v int) int { return v * 2 })
assert.Equal(t, map[string]int{"a": 2, "b": 4, "c": 6}, result)
}
func TestMonadMapWithIndex(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
}
result := MonadMapWithIndex(data, func(k string, v int) string {
return fmt.Sprintf("%s=%d", k, v)
})
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
}
func TestMapWithIndex(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
}
mapper := MapWithIndex(func(k string, v int) string {
return fmt.Sprintf("%s=%d", k, v)
})
result := mapper(data)
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
}
func TestMonadLookup(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
}
assert.Equal(t, O.Some(1), MonadLookup(data, "a"))
assert.Equal(t, O.None[int](), MonadLookup(data, "c"))
}
func TestMerge(t *testing.T) {
left := map[string]int{"a": 1, "b": 2}
right := map[string]int{"b": 3, "c": 4}
result := Merge(right)(left)
assert.Equal(t, map[string]int{"a": 1, "b": 3, "c": 4}, result)
}
func TestSize(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
assert.Equal(t, 3, Size(data))
assert.Equal(t, 0, Size(Empty[string, int]()))
}
func TestToArray(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := ToArray(data)
assert.Len(t, result, 2)
// Check both entries exist (order is non-deterministic)
found := make(map[string]int)
for _, entry := range result {
found[P.Head(entry)] = P.Tail(entry)
}
assert.Equal(t, data, found)
}
func TestToEntries(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := ToEntries(data)
assert.Len(t, result, 2)
}
func TestFromEntries(t *testing.T) {
entries := Entries[string, int]{
P.MakePair("a", 1),
P.MakePair("b", 2),
}
result := FromEntries(entries)
assert.Equal(t, map[string]int{"a": 1, "b": 2}, result)
}
func TestUpsertAt(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := UpsertAt("c", 3)(data)
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, result)
// Original should be unchanged
assert.Equal(t, map[string]int{"a": 1, "b": 2}, data)
// Update existing
result2 := UpsertAt("a", 10)(data)
assert.Equal(t, map[string]int{"a": 10, "b": 2}, result2)
}
func TestDeleteAt(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
result := DeleteAt[string, int]("b")(data)
assert.Equal(t, map[string]int{"a": 1, "c": 3}, result)
// Original should be unchanged
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, data)
}
func TestSingleton(t *testing.T) {
result := Singleton("key", 42)
assert.Equal(t, map[string]int{"key": 42}, result)
}
func TestFilterMapWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
filter := FilterMapWithIndex(func(k string, v int) O.Option[int] {
if v%2 == 0 {
return O.Some(v * 10)
}
return O.None[int]()
})
result := filter(data)
assert.Equal(t, map[string]int{"b": 20}, result)
}
func TestFilterMap(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
filter := FilterMap[string](func(v int) O.Option[int] {
if v%2 == 0 {
return O.Some(v * 10)
}
return O.None[int]()
})
result := filter(data)
assert.Equal(t, map[string]int{"b": 20}, result)
}
func TestFilter(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
filter := Filter[string, int](func(k string) bool {
return k != "b"
})
result := filter(data)
assert.Equal(t, map[string]int{"a": 1, "c": 3}, result)
}
func TestFilterWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
filter := FilterWithIndex(func(k string, v int) bool {
return v%2 == 0
})
result := filter(data)
assert.Equal(t, map[string]int{"b": 2}, result)
}
func TestIsNil(t *testing.T) {
var nilMap map[string]int
nonNilMap := map[string]int{}
assert.True(t, IsNil(nilMap))
assert.False(t, IsNil(nonNilMap))
}
func TestIsNonNil(t *testing.T) {
var nilMap map[string]int
nonNilMap := map[string]int{}
assert.False(t, IsNonNil(nilMap))
assert.True(t, IsNonNil(nonNilMap))
}
func TestConstNil(t *testing.T) {
result := ConstNil[string, int]()
assert.Nil(t, result)
assert.True(t, IsNil(result))
}
func TestMonadChain(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
monoid := MergeMonoid[string, int]()
result := MonadChain(monoid, data, func(v int) map[string]int {
return map[string]int{
fmt.Sprintf("x%d", v): v * 10,
}
})
assert.Equal(t, map[string]int{"x1": 10, "x2": 20}, result)
}
func TestChain(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
monoid := MergeMonoid[string, int]()
chain := Chain[int](monoid)(func(v int) map[string]int {
return map[string]int{
fmt.Sprintf("x%d", v): v * 10,
}
})
result := chain(data)
assert.Equal(t, map[string]int{"x1": 10, "x2": 20}, result)
}
func TestFlatten(t *testing.T) {
nested := map[string]map[string]int{
"a": {"x": 1, "y": 2},
"b": {"z": 3},
}
monoid := MergeMonoid[string, int]()
flatten := Flatten(monoid)
result := flatten(nested)
assert.Equal(t, map[string]int{"x": 1, "y": 2, "z": 3}, result)
}
func TestFoldMap(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
// Use string monoid for simplicity
fold := FoldMap[string, int](S.Monoid)(func(v int) string {
return fmt.Sprintf("%d", v)
})
result := fold(data)
// Result contains all digits but order is non-deterministic
assert.Contains(t, result, "1")
assert.Contains(t, result, "2")
assert.Contains(t, result, "3")
}
func TestFold(t *testing.T) {
data := map[string]string{"a": "A", "b": "B", "c": "C"}
fold := Fold[string](S.Monoid)
result := fold(data)
// Result contains all letters but order is non-deterministic
assert.Contains(t, result, "A")
assert.Contains(t, result, "B")
assert.Contains(t, result, "C")
}
func TestKeysOrd(t *testing.T) {
data := map[string]int{"c": 3, "a": 1, "b": 2}
keys := KeysOrd[int](S.Ord)(data)
assert.Equal(t, []string{"a", "b", "c"}, keys)
}
func TestMonadFlap(t *testing.T) {
fns := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
"triple": func(x int) int { return x * 3 },
}
result := MonadFlap(fns, 5)
assert.Equal(t, map[string]int{"double": 10, "triple": 15}, result)
}
func TestFlap(t *testing.T) {
fns := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
"triple": func(x int) int { return x * 3 },
}
flap := Flap[int, string](5)
result := flap(fns)
assert.Equal(t, map[string]int{"double": 10, "triple": 15}, result)
}
func TestFromArray(t *testing.T) {
entries := Entries[string, int]{
P.MakePair("a", 1),
P.MakePair("b", 2),
P.MakePair("a", 3), // Duplicate key
}
// Use Second magma to keep last value
from := FromArray[string](Mg.Second[int]())
result := from(entries)
assert.Equal(t, map[string]int{"a": 3, "b": 2}, result)
}
func TestMonadAp(t *testing.T) {
fns := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
}
vals := map[string]int{
"double": 5,
}
monoid := MergeMonoid[string, int]()
result := MonadAp(monoid, fns, vals)
assert.Equal(t, map[string]int{"double": 10}, result)
}
func TestAp(t *testing.T) {
fns := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
}
vals := map[string]int{
"double": 5,
}
monoid := MergeMonoid[string, int]()
ap := Ap[int](monoid)(vals)
result := ap(fns)
assert.Equal(t, map[string]int{"double": 10}, result)
}
func TestOf(t *testing.T) {
result := Of("key", 42)
assert.Equal(t, map[string]int{"key": 42}, result)
}
func TestReduceRef(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
sum := ReduceRef[string](func(acc int, v *int) int {
return acc + *v
}, 0)
result := sum(data)
assert.Equal(t, 6, result)
}
func TestReduceRefWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
concat := ReduceRefWithIndex(func(k string, acc string, v *int) string {
if acc == "" {
return fmt.Sprintf("%s:%d", k, *v)
}
return fmt.Sprintf("%s,%s:%d", acc, k, *v)
}, "")
result := concat(data)
assert.Contains(t, result, "a:1")
assert.Contains(t, result, "b:2")
}
func TestMonadMapRef(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := MonadMapRef(data, func(v *int) int { return *v * 2 })
assert.Equal(t, map[string]int{"a": 2, "b": 4}, result)
}
func TestMapRef(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
mapper := MapRef[string](func(v *int) int { return *v * 2 })
result := mapper(data)
assert.Equal(t, map[string]int{"a": 2, "b": 4}, result)
}
func TestMonadMapRefWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := MonadMapRefWithIndex(data, func(k string, v *int) string {
return fmt.Sprintf("%s=%d", k, *v)
})
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
}
func TestMapRefWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
mapper := MapRefWithIndex(func(k string, v *int) string {
return fmt.Sprintf("%s=%d", k, *v)
})
result := mapper(data)
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
}
func TestUnion(t *testing.T) {
left := map[string]int{"a": 1, "b": 2}
right := map[string]int{"b": 3, "c": 4}
// Union combines maps, with the magma resolving conflicts
// The order is union(left)(right), which means right is merged into left
// First magma keeps the first value (from right in this case)
union := Union[string](Mg.First[int]())
result := union(left)(right)
assert.Equal(t, map[string]int{"a": 1, "b": 3, "c": 4}, result)
// Second magma keeps the second value (from left in this case)
union2 := Union[string](Mg.Second[int]())
result2 := union2(left)(right)
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 4}, result2)
}
func TestMonadChainWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
monoid := MergeMonoid[string, string]()
result := MonadChainWithIndex(monoid, data, func(k string, v int) map[string]string {
return map[string]string{
fmt.Sprintf("%s%d", k, v): fmt.Sprintf("val%d", v),
}
})
assert.Equal(t, map[string]string{"a1": "val1", "b2": "val2"}, result)
}
func TestChainWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
monoid := MergeMonoid[string, string]()
chain := ChainWithIndex[int](monoid)(func(k string, v int) map[string]string {
return map[string]string{
fmt.Sprintf("%s%d", k, v): fmt.Sprintf("val%d", v),
}
})
result := chain(data)
assert.Equal(t, map[string]string{"a1": "val1", "b2": "val2"}, result)
}
func TestFilterChainWithIndex(t *testing.T) {
src := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
f := func(k string, value int) O.Option[map[string]string] {
if value%2 != 0 {
return O.Of(map[string]string{
k: fmt.Sprintf("%s%d", k, value),
})
}
return O.None[map[string]string]()
}
monoid := MergeMonoid[string, string]()
res := FilterChainWithIndex[int](monoid)(f)(src)
assert.Equal(t, map[string]string{
"a": "a1",
"c": "c3",
}, res)
}
func TestFoldMapWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
fold := FoldMapWithIndex[string, int](S.Monoid)(func(k string, v int) string {
return fmt.Sprintf("%s:%d", k, v)
})
result := fold(data)
// Result contains all pairs but order is non-deterministic
assert.Contains(t, result, "a:1")
assert.Contains(t, result, "b:2")
assert.Contains(t, result, "c:3")
}
func TestReduceOrd(t *testing.T) {
data := map[string]int{"c": 3, "a": 1, "b": 2}
sum := ReduceOrd[int, int](S.Ord)(func(acc, v int) int {
return acc + v
}, 0)
result := sum(data)
assert.Equal(t, 6, result)
}
func TestReduceOrdWithIndex(t *testing.T) {
data := map[string]int{"c": 3, "a": 1, "b": 2}
concat := ReduceOrdWithIndex[int, string](S.Ord)(func(k string, acc string, v int) string {
if acc == "" {
return fmt.Sprintf("%s:%d", k, v)
}
return fmt.Sprintf("%s,%s:%d", acc, k, v)
}, "")
result := concat(data)
// With Ord, keys should be in order
assert.Equal(t, "a:1,b:2,c:3", result)
}
func TestFoldMapOrdWithIndex(t *testing.T) {
data := map[string]int{"c": 3, "a": 1, "b": 2}
fold := FoldMapOrdWithIndex[string, int, string](S.Ord)(S.Monoid)(func(k string, v int) string {
return fmt.Sprintf("%s:%d,", k, v)
})
result := fold(data)
assert.Equal(t, "a:1,b:2,c:3,", result)
}
func TestFoldOrd(t *testing.T) {
data := map[string]string{"c": "C", "a": "A", "b": "B"}
fold := FoldOrd[string](S.Ord)(S.Monoid)
result := fold(data)
assert.Equal(t, "ABC", result)
}
func TestFromFoldableMap(t *testing.T) {
src := A.From("a", "b", "c", "a")
// Create a reducer function
reducer := A.Reduce[string, map[string]string]
from := FromFoldableMap(
Mg.Second[string](),
reducer,
)
f := from(P.Of[string])
result := f(src)
assert.Equal(t, map[string]string{
"a": "a",
"b": "b",
"c": "c",
}, result)
}
func TestFromFoldable(t *testing.T) {
entries := Entries[string, int]{
P.MakePair("a", 1),
P.MakePair("b", 2),
P.MakePair("a", 3), // Duplicate key
}
reducer := A.Reduce[Entry[string, int], map[string]int]
from := FromFoldable(
Mg.Second[int](),
reducer,
)
result := from(entries)
assert.Equal(t, map[string]int{"a": 3, "b": 2}, result)
}

View File

@@ -17,17 +17,98 @@ package record
import ( import (
G "github.com/IBM/fp-go/v2/record/generic" G "github.com/IBM/fp-go/v2/record/generic"
S "github.com/IBM/fp-go/v2/semigroup"
) )
func UnionSemigroup[K comparable, V any](s S.Semigroup[V]) S.Semigroup[map[K]V] { // UnionSemigroup creates a semigroup for maps that combines two maps using the provided
return G.UnionSemigroup[map[K]V](s) // semigroup for resolving conflicts when the same key exists in both maps.
//
// When concatenating two maps:
// - Keys that exist in only one map are included in the result
// - Keys that exist in both maps have their values combined using the provided semigroup
//
// This is useful when you want custom conflict resolution logic beyond simple "first wins"
// or "last wins" semantics.
//
// Example:
//
// // Create a semigroup that sums values for duplicate keys
// sumSemigroup := number.SemigroupSum[int]()
// mapSemigroup := UnionSemigroup[string, int](sumSemigroup)
//
// map1 := map[string]int{"a": 1, "b": 2}
// map2 := map[string]int{"b": 3, "c": 4}
// result := mapSemigroup.Concat(map1, map2)
// // result: {"a": 1, "b": 5, "c": 4} // b values are summed: 2 + 3 = 5
//
// Example with string concatenation:
//
// stringSemigroup := string.Semigroup
// mapSemigroup := UnionSemigroup[string, string](stringSemigroup)
//
// map1 := map[string]string{"a": "Hello", "b": "World"}
// map2 := map[string]string{"b": "!", "c": "Goodbye"}
// result := mapSemigroup.Concat(map1, map2)
// // result: {"a": "Hello", "b": "World!", "c": "Goodbye"}
//
//go:inline
func UnionSemigroup[K comparable, V any](s Semigroup[V]) Semigroup[Record[K, V]] {
return G.UnionSemigroup[Record[K, V]](s)
} }
func UnionLastSemigroup[K comparable, V any]() S.Semigroup[map[K]V] { // UnionLastSemigroup creates a semigroup for maps where the last (right) value wins
return G.UnionLastSemigroup[map[K]V]() // when the same key exists in both maps being concatenated.
//
// This is the most common conflict resolution strategy and is equivalent to using
// the standard map merge operation where right-side values take precedence.
//
// When concatenating two maps:
// - Keys that exist in only one map are included in the result
// - Keys that exist in both maps take the value from the second (right) map
//
// Example:
//
// semigroup := UnionLastSemigroup[string, int]()
//
// map1 := map[string]int{"a": 1, "b": 2}
// map2 := map[string]int{"b": 3, "c": 4}
// result := semigroup.Concat(map1, map2)
// // result: {"a": 1, "b": 3, "c": 4} // b takes value from map2 (last wins)
//
// This is useful for:
// - Configuration overrides (later configs override earlier ones)
// - Applying updates to a base map
// - Merging user preferences where newer values should win
//
//go:inline
func UnionLastSemigroup[K comparable, V any]() Semigroup[Record[K, V]] {
return G.UnionLastSemigroup[Record[K, V]]()
} }
func UnionFirstSemigroup[K comparable, V any]() S.Semigroup[map[K]V] { // UnionFirstSemigroup creates a semigroup for maps where the first (left) value wins
return G.UnionFirstSemigroup[map[K]V]() // when the same key exists in both maps being concatenated.
//
// This is useful when you want to preserve original values and ignore updates for
// keys that already exist.
//
// When concatenating two maps:
// - Keys that exist in only one map are included in the result
// - Keys that exist in both maps keep the value from the first (left) map
//
// Example:
//
// semigroup := UnionFirstSemigroup[string, int]()
//
// map1 := map[string]int{"a": 1, "b": 2}
// map2 := map[string]int{"b": 3, "c": 4}
// result := semigroup.Concat(map1, map2)
// // result: {"a": 1, "b": 2, "c": 4} // b keeps value from map1 (first wins)
//
// This is useful for:
// - Default values (defaults are set first, user values don't override)
// - Caching (first cached value is kept, subsequent updates ignored)
// - Immutable registries (first registration wins, duplicates are ignored)
//
//go:inline
func UnionFirstSemigroup[K comparable, V any]() Semigroup[Record[K, V]] {
return G.UnionFirstSemigroup[Record[K, V]]()
} }

227
v2/record/semigroup_test.go Normal file
View File

@@ -0,0 +1,227 @@
// 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 record
import (
"testing"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestUnionSemigroup(t *testing.T) {
// Test with sum semigroup - values should be added for duplicate keys
sumSemigroup := N.SemigroupSum[int]()
mapSemigroup := UnionSemigroup[string](sumSemigroup)
map1 := map[string]int{"a": 1, "b": 2}
map2 := map[string]int{"b": 3, "c": 4}
result := mapSemigroup.Concat(map1, map2)
expected := map[string]int{"a": 1, "b": 5, "c": 4}
assert.Equal(t, expected, result)
}
func TestUnionSemigroupString(t *testing.T) {
// Test with string semigroup - strings should be concatenated
stringSemigroup := S.Semigroup
mapSemigroup := UnionSemigroup[string](stringSemigroup)
map1 := map[string]string{"a": "Hello", "b": "World"}
map2 := map[string]string{"b": "!", "c": "Goodbye"}
result := mapSemigroup.Concat(map1, map2)
expected := map[string]string{"a": "Hello", "b": "World!", "c": "Goodbye"}
assert.Equal(t, expected, result)
}
func TestUnionSemigroupProduct(t *testing.T) {
// Test with product semigroup - values should be multiplied
prodSemigroup := N.SemigroupProduct[int]()
mapSemigroup := UnionSemigroup[string](prodSemigroup)
map1 := map[string]int{"a": 2, "b": 3}
map2 := map[string]int{"b": 4, "c": 5}
result := mapSemigroup.Concat(map1, map2)
expected := map[string]int{"a": 2, "b": 12, "c": 5}
assert.Equal(t, expected, result)
}
func TestUnionSemigroupEmpty(t *testing.T) {
// Test with empty maps
sumSemigroup := N.SemigroupSum[int]()
mapSemigroup := UnionSemigroup[string](sumSemigroup)
map1 := map[string]int{"a": 1}
empty := map[string]int{}
result1 := mapSemigroup.Concat(map1, empty)
assert.Equal(t, map1, result1)
result2 := mapSemigroup.Concat(empty, map1)
assert.Equal(t, map1, result2)
}
func TestUnionLastSemigroup(t *testing.T) {
// Test that last (right) value wins for duplicate keys
semigroup := UnionLastSemigroup[string, int]()
map1 := map[string]int{"a": 1, "b": 2}
map2 := map[string]int{"b": 3, "c": 4}
result := semigroup.Concat(map1, map2)
expected := map[string]int{"a": 1, "b": 3, "c": 4}
assert.Equal(t, expected, result)
}
func TestUnionLastSemigroupNoOverlap(t *testing.T) {
// Test with no overlapping keys
semigroup := UnionLastSemigroup[string, int]()
map1 := map[string]int{"a": 1, "b": 2}
map2 := map[string]int{"c": 3, "d": 4}
result := semigroup.Concat(map1, map2)
expected := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
assert.Equal(t, expected, result)
}
func TestUnionLastSemigroupAllOverlap(t *testing.T) {
// Test with all keys overlapping
semigroup := UnionLastSemigroup[string, int]()
map1 := map[string]int{"a": 1, "b": 2}
map2 := map[string]int{"a": 10, "b": 20}
result := semigroup.Concat(map1, map2)
expected := map[string]int{"a": 10, "b": 20}
assert.Equal(t, expected, result)
}
func TestUnionLastSemigroupEmpty(t *testing.T) {
// Test with empty maps
semigroup := UnionLastSemigroup[string, int]()
map1 := map[string]int{"a": 1}
empty := map[string]int{}
result1 := semigroup.Concat(map1, empty)
assert.Equal(t, map1, result1)
result2 := semigroup.Concat(empty, map1)
assert.Equal(t, map1, result2)
}
func TestUnionFirstSemigroup(t *testing.T) {
// Test that first (left) value wins for duplicate keys
semigroup := UnionFirstSemigroup[string, int]()
map1 := map[string]int{"a": 1, "b": 2}
map2 := map[string]int{"b": 3, "c": 4}
result := semigroup.Concat(map1, map2)
expected := map[string]int{"a": 1, "b": 2, "c": 4}
assert.Equal(t, expected, result)
}
func TestUnionFirstSemigroupNoOverlap(t *testing.T) {
// Test with no overlapping keys
semigroup := UnionFirstSemigroup[string, int]()
map1 := map[string]int{"a": 1, "b": 2}
map2 := map[string]int{"c": 3, "d": 4}
result := semigroup.Concat(map1, map2)
expected := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
assert.Equal(t, expected, result)
}
func TestUnionFirstSemigroupAllOverlap(t *testing.T) {
// Test with all keys overlapping
semigroup := UnionFirstSemigroup[string, int]()
map1 := map[string]int{"a": 1, "b": 2}
map2 := map[string]int{"a": 10, "b": 20}
result := semigroup.Concat(map1, map2)
expected := map[string]int{"a": 1, "b": 2}
assert.Equal(t, expected, result)
}
func TestUnionFirstSemigroupEmpty(t *testing.T) {
// Test with empty maps
semigroup := UnionFirstSemigroup[string, int]()
map1 := map[string]int{"a": 1}
empty := map[string]int{}
result1 := semigroup.Concat(map1, empty)
assert.Equal(t, map1, result1)
result2 := semigroup.Concat(empty, map1)
assert.Equal(t, map1, result2)
}
// Test associativity law for UnionSemigroup
func TestUnionSemigroupAssociativity(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
mapSemigroup := UnionSemigroup[string](sumSemigroup)
map1 := map[string]int{"a": 1}
map2 := map[string]int{"a": 2, "b": 3}
map3 := map[string]int{"b": 4, "c": 5}
// (map1 + map2) + map3
left := mapSemigroup.Concat(mapSemigroup.Concat(map1, map2), map3)
// map1 + (map2 + map3)
right := mapSemigroup.Concat(map1, mapSemigroup.Concat(map2, map3))
assert.Equal(t, left, right)
}
// Test associativity law for UnionLastSemigroup
func TestUnionLastSemigroupAssociativity(t *testing.T) {
semigroup := UnionLastSemigroup[string, int]()
map1 := map[string]int{"a": 1}
map2 := map[string]int{"a": 2, "b": 3}
map3 := map[string]int{"b": 4, "c": 5}
// (map1 + map2) + map3
left := semigroup.Concat(semigroup.Concat(map1, map2), map3)
// map1 + (map2 + map3)
right := semigroup.Concat(map1, semigroup.Concat(map2, map3))
assert.Equal(t, left, right)
}
// Test associativity law for UnionFirstSemigroup
func TestUnionFirstSemigroupAssociativity(t *testing.T) {
semigroup := UnionFirstSemigroup[string, int]()
map1 := map[string]int{"a": 1}
map2 := map[string]int{"a": 2, "b": 3}
map3 := map[string]int{"b": 4, "c": 5}
// (map1 + map2) + map3
left := semigroup.Concat(semigroup.Concat(map1, map2), map3)
// map1 + (map2 + map3)
right := semigroup.Concat(map1, semigroup.Concat(map2, map3))
assert.Equal(t, left, right)
}

View File

@@ -19,6 +19,36 @@ import (
G "github.com/IBM/fp-go/v2/internal/record" G "github.com/IBM/fp-go/v2/internal/record"
) )
// TraverseWithIndex transforms a map of values into a value of a map by applying an effectful function
// to each key-value pair. The function has access to both the key and value.
//
// This is useful when you need to perform an operation that may fail or have side effects on each
// element of a map, and you want to collect the results in the same applicative context.
//
// Type parameters:
// - K: The key type (must be comparable)
// - A: The input value type
// - B: The output value type
// - HKTB: Higher-kinded type representing the effect containing B (e.g., Option[B], Either[E, B])
// - HKTAB: Higher-kinded type representing a function from B to map[K]B in the effect
// - HKTRB: Higher-kinded type representing the effect containing map[K]B
//
// Parameters:
// - fof: Lifts a pure map[K]B into the effect (the "of" or "pure" function)
// - fmap: Maps a function over the effect (the "map" or "fmap" function)
// - fap: Applies an effectful function to an effectful value (the "ap" function)
// - f: The transformation function that takes a key and value and returns an effect
//
// Example with Option:
//
// f := func(k string, n int) O.Option[int] {
// if n > 0 {
// return O.Some(n * 2)
// }
// return O.None[int]()
// }
// traverse := TraverseWithIndex(O.Of[map[string]int], O.Map[...], O.Ap[...], f)
// result := traverse(map[string]int{"a": 1, "b": 2}) // O.Some(map[string]int{"a": 2, "b": 4})
func TraverseWithIndex[K comparable, A, B, HKTB, HKTAB, HKTRB any]( func TraverseWithIndex[K comparable, A, B, HKTB, HKTAB, HKTRB any](
fof func(map[K]B) HKTRB, fof func(map[K]B) HKTRB,
fmap func(func(map[K]B) func(B) map[K]B) func(HKTRB) HKTAB, fmap func(func(map[K]B) func(B) map[K]B) func(HKTRB) HKTAB,
@@ -28,10 +58,36 @@ func TraverseWithIndex[K comparable, A, B, HKTB, HKTAB, HKTRB any](
return G.TraverseWithIndex[map[K]A](fof, fmap, fap, f) return G.TraverseWithIndex[map[K]A](fof, fmap, fap, f)
} }
// HKTA = HKT<A> // Traverse transforms a map of values into a value of a map by applying an effectful function
// HKTB = HKT<B> // to each value. Unlike TraverseWithIndex, this function does not provide access to the keys.
// HKTAB = HKT<func(A)B> //
// HKTRB = HKT<map[K]B> // This is useful when you need to perform an operation that may fail or have side effects on each
// element of a map, and you want to collect the results in the same applicative context.
//
// Type parameters:
// - K: The key type (must be comparable)
// - A: The input value type
// - B: The output value type
// - HKTB: Higher-kinded type representing the effect containing B (e.g., Option[B], Either[E, B])
// - HKTAB: Higher-kinded type representing a function from B to map[K]B in the effect
// - HKTRB: Higher-kinded type representing the effect containing map[K]B
//
// Parameters:
// - fof: Lifts a pure map[K]B into the effect (the "of" or "pure" function)
// - fmap: Maps a function over the effect (the "map" or "fmap" function)
// - fap: Applies an effectful function to an effectful value (the "ap" function)
// - f: The transformation function that takes a value and returns an effect
//
// Example with Option:
//
// f := func(s string) O.Option[string] {
// if s != "" {
// return O.Some(strings.ToUpper(s))
// }
// return O.None[string]()
// }
// traverse := Traverse(O.Of[map[string]string], O.Map[...], O.Ap[...], f)
// result := traverse(map[string]string{"a": "hello"}) // O.Some(map[string]string{"a": "HELLO"})
func Traverse[K comparable, A, B, HKTB, HKTAB, HKTRB any]( func Traverse[K comparable, A, B, HKTB, HKTAB, HKTRB any](
fof func(map[K]B) HKTRB, fof func(map[K]B) HKTRB,
fmap func(func(map[K]B) func(B) map[K]B) func(HKTRB) HKTAB, fmap func(func(map[K]B) func(B) map[K]B) func(HKTRB) HKTAB,
@@ -40,9 +96,39 @@ func Traverse[K comparable, A, B, HKTB, HKTAB, HKTRB any](
return G.Traverse[map[K]A](fof, fmap, fap, f) return G.Traverse[map[K]A](fof, fmap, fap, f)
} }
// HKTA = HKT[A] // Sequence transforms a map of effects into an effect of a map.
// HKTAA = HKT[func(A)map[K]A] // This is the dual of Traverse where the transformation function is the identity.
// HKTRA = HKT[map[K]A] //
// This is useful when you have a map where each value is already in an effect context
// (like Option, Either, etc.) and you want to "flip" the nesting to get a single effect
// containing a map of plain values.
//
// If any value in the map is a "failure" (e.g., None, Left), the entire result will be
// a failure. If all values are "successes", the result will be a success containing a map
// of all the unwrapped values.
//
// Type parameters:
// - K: The key type (must be comparable)
// - A: The value type inside the effect
// - HKTA: Higher-kinded type representing the effect containing A (e.g., Option[A])
// - HKTAA: Higher-kinded type representing a function from A to map[K]A in the effect
// - HKTRA: Higher-kinded type representing the effect containing map[K]A
//
// Parameters:
// - fof: Lifts a pure map[K]A into the effect (the "of" or "pure" function)
// - fmap: Maps a function over the effect (the "map" or "fmap" function)
// - fap: Applies an effectful function to an effectful value (the "ap" function)
// - ma: The input map where each value is in an effect context
//
// Example with Option:
//
// input := map[string]O.Option[int]{"a": O.Some(1), "b": O.Some(2)}
// result := Sequence(O.Of[map[string]int], O.Map[...], O.Ap[...], input)
// // result: O.Some(map[string]int{"a": 1, "b": 2})
//
// input2 := map[string]O.Option[int]{"a": O.Some(1), "b": O.None[int]()}
// result2 := Sequence(O.Of[map[string]int], O.Map[...], O.Ap[...], input2)
// // result2: O.None[map[string]int]()
func Sequence[K comparable, A, HKTA, HKTAA, HKTRA any]( func Sequence[K comparable, A, HKTA, HKTAA, HKTRA any](
fof func(map[K]A) HKTRA, fof func(map[K]A) HKTRA,
fmap func(func(map[K]A) func(A) map[K]A) func(HKTRA) HKTAA, fmap func(func(map[K]A) func(A) map[K]A) func(HKTRA) HKTAA,

162
v2/record/types.go Normal file
View File

@@ -0,0 +1,162 @@
// 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 record
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/semigroup"
)
type (
Endomorphism[A any] = endomorphism.Endomorphism[A]
Monoid[A any] = monoid.Monoid[A]
Semigroup[A any] = semigroup.Semigroup[A]
Option[A any] = option.Option[A]
// Record represents a map with comparable keys and values of any type.
// This is the primary data structure for the record package, providing
// functional operations over Go's native map type.
//
// Example:
//
// type UserRecord = Record[string, User]
// users := UserRecord{
// "alice": User{Name: "Alice", Age: 30},
// "bob": User{Name: "Bob", Age: 25},
// }
Record[K comparable, V any] = map[K]V
// Predicate is a function that tests whether a key satisfies a condition.
// Used in filtering operations to determine which entries to keep.
//
// Example:
//
// isVowel := func(k string) bool {
// return strings.ContainsAny(k, "aeiou")
// }
Predicate[K any] = predicate.Predicate[K]
// PredicateWithIndex is a function that tests whether a key-value pair satisfies a condition.
// Used in filtering operations that need access to both key and value.
//
// Example:
//
// isAdult := func(name string, user User) bool {
// return user.Age >= 18
// }
PredicateWithIndex[K comparable, V any] = func(K, V) bool
// Operator transforms a record from one value type to another while preserving keys.
// This is the fundamental transformation type for record operations.
//
// Example:
//
// doubleValues := Map(func(x int) int { return x * 2 })
// result := doubleValues(Record[string, int]{"a": 1, "b": 2})
// // result: {"a": 2, "b": 4}
Operator[K comparable, V1, V2 any] = func(Record[K, V1]) Record[K, V2]
// OperatorWithIndex transforms a record using both key and value information.
// Useful when the transformation depends on the key.
//
// Example:
//
// prefixWithKey := MapWithIndex(func(k string, v string) string {
// return k + ":" + v
// })
OperatorWithIndex[K comparable, V1, V2 any] = func(func(K, V1) V2) Operator[K, V1, V2]
// Kleisli represents a monadic function that transforms a value into a record.
// Used in chain operations for composing record-producing functions.
//
// Example:
//
// expand := func(x int) Record[string, int] {
// return Record[string, int]{
// "double": x * 2,
// "triple": x * 3,
// }
// }
Kleisli[K comparable, V1, V2 any] = func(V1) Record[K, V2]
// KleisliWithIndex is a monadic function that uses both key and value to produce a record.
//
// Example:
//
// expandWithKey := func(k string, v int) Record[string, int] {
// return Record[string, int]{
// k + "_double": v * 2,
// k + "_triple": v * 3,
// }
// }
KleisliWithIndex[K comparable, V1, V2 any] = func(K, V1) Record[K, V2]
// Reducer accumulates values from a record into a single result.
// The function receives the accumulator and current value, returning the new accumulator.
//
// Example:
//
// sum := Reduce(func(acc int, v int) int {
// return acc + v
// }, 0)
Reducer[V, R any] = func(R, V) R
// ReducerWithIndex accumulates values using both key and value information.
//
// Example:
//
// weightedSum := ReduceWithIndex(func(k string, acc int, v int) int {
// weight := len(k)
// return acc + (v * weight)
// }, 0)
ReducerWithIndex[K comparable, V, R any] = func(K, R, V) R
// Collector transforms key-value pairs into a result type and collects them into an array.
//
// Example:
//
// toStrings := Collect(func(k string, v int) string {
// return fmt.Sprintf("%s=%d", k, v)
// })
Collector[K comparable, V, R any] = func(K, V) R
// Entry represents a single key-value pair from a record.
// This is an alias for Tuple2 to provide semantic clarity.
//
// Example:
//
// entries := ToEntries(record)
// for _, entry := range entries {
// key := entry.F1
// value := entry.F2
// }
Entry[K comparable, V any] = pair.Pair[K, V]
// Entries is a slice of key-value pairs.
//
// Example:
//
// entries := Entries[string, int]{
// T.MakeTuple2("a", 1),
// T.MakeTuple2("b", 2),
// }
// record := FromEntries(entries)
Entries[K comparable, V any] = []Entry[K, V]
)