1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-20 13:58:04 +02:00

Compare commits

..

8 Commits

Author SHA1 Message Date
Dr. Carsten Leue
d3ffc71808 fix: add ModifyF
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-17 15:23:10 +01:00
Dr. Carsten Leue
62844b7030 fix: add Filter and FilterMap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-15 23:33:08 +01:00
Dr. Carsten Leue
99a0ddd4b6 fix: implement filter and filtermap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-15 23:18:14 +01:00
Dr. Carsten Leue
02acbae8f6 fix: add lenses for Hostname and Port
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-15 22:49:11 +01:00
Dr. Carsten Leue
eb27ecdc01 fix: clarify behaviour of array.Concat
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-13 18:39:55 +01:00
Dr. Carsten Leue
e5eb7d343c fix: add inline flags
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-13 09:26:39 +01:00
Dr. Carsten Leue
d5a3217251 fix: add FromIso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-12 22:38:26 +01:00
Dr. Carsten Leue
c5cbdaad68 fix: add FromIso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-12 22:38:01 +01:00
22 changed files with 2989 additions and 621 deletions

View File

@@ -529,71 +529,116 @@ func Push[A any](a A) Operator[A, A] {
return G.Push[Operator[A, A]](a)
}
// Concat concatenates two arrays, appending the provided array to the end of the input array.
// This is a curried function that takes an array to append and returns a function that
// takes the base array and returns the concatenated result.
// Concat concatenates two arrays by appending a suffix array to a base array.
//
// The function creates a new array containing all elements from the base array followed
// by all elements from the appended array. Neither input array is modified.
// This is a curried function that takes a suffix array and returns a function
// that takes a base array and produces a new array with the suffix appended.
// It follows the "data last" pattern, where the data to be operated on (base array)
// is provided last, making it ideal for use in functional pipelines.
//
// Semantic: Concat(suffix)(base) produces [base... suffix...]
//
// The function creates a new array containing all elements from the base array
// followed by all elements from the suffix array. Neither input array is modified.
//
// Type Parameters:
//
// - A: The type of elements in the arrays
//
// Parameters:
// - as: The array to append to the end of the base array
//
// - suffix: The array to append to the end of the base array
//
// Returns:
// - A function that takes a base array and returns a new array with `as` appended to its end
//
// - A function that takes a base array and returns [base... suffix...]
//
// Behavior:
// - Creates a new array with length equal to the sum of both input arrays
// - Copies all elements from the base array first
// - Appends all elements from the `as` array at the end
// - Returns the base array unchanged if `as` is empty
// - Returns `as` unchanged if the base array is empty
// - Does not modify either input array
//
// Example:
// - Creates a new array with length equal to len(base) + len(suffix)
// - Copies all elements from the base array first
// - Appends all elements from the suffix array at the end
// - Returns the base array unchanged if suffix is empty
// - Returns suffix unchanged if the base array is empty
// - Does not modify either input array
// - Preserves element order within each array
//
// Example - Basic concatenation:
//
// base := []int{1, 2, 3}
// toAppend := []int{4, 5, 6}
// result := array.Concat(toAppend)(base)
// suffix := []int{4, 5, 6}
// concat := array.Concat(suffix)
// result := concat(base)
// // result: []int{1, 2, 3, 4, 5, 6}
// // base: []int{1, 2, 3} (unchanged)
// // toAppend: []int{4, 5, 6} (unchanged)
// // suffix: []int{4, 5, 6} (unchanged)
//
// Example with empty arrays:
// Example - Direct application:
//
// result := array.Concat([]int{4, 5, 6})([]int{1, 2, 3})
// // result: []int{1, 2, 3, 4, 5, 6}
// // Demonstrates: Concat(b)(a) = [a... b...]
//
// Example - Empty arrays:
//
// base := []int{1, 2, 3}
// empty := []int{}
// result := array.Concat(empty)(base)
// // result: []int{1, 2, 3}
//
// Example with strings:
// Example - Strings:
//
// words1 := []string{"hello", "world"}
// words2 := []string{"foo", "bar"}
// result := array.Concat(words2)(words1)
// // result: []string{"hello", "world", "foo", "bar"}
//
// Example with functional composition:
// Example - Functional composition:
//
// numbers := []int{1, 2, 3}
// result := F.Pipe2(
// numbers,
// array.Map(N.Mul(2)),
// array.Concat([]int{10, 20}),
// array.Map(N.Mul(2)), // [2, 4, 6]
// array.Concat([]int{10, 20}), // [2, 4, 6, 10, 20]
// )
// // result: []int{2, 4, 6, 10, 20}
//
// Example - Multiple concatenations:
//
// result := F.Pipe2(
// []int{1},
// array.Concat([]int{2, 3}), // [1, 2, 3]
// array.Concat([]int{4, 5}), // [1, 2, 3, 4, 5]
// )
//
// Example - Building arrays incrementally:
//
// header := []string{"Name", "Age"}
// data := []string{"Alice", "30"}
// footer := []string{"Total: 1"}
// result := F.Pipe2(
// header,
// array.Concat(data),
// array.Concat(footer),
// )
// // result: []string{"Name", "Age", "Alice", "30", "Total: 1"}
//
// Use cases:
//
// - Combining multiple arrays into one
// - Building arrays incrementally
// - Building arrays incrementally in pipelines
// - Implementing array-based data structures (queues, buffers)
// - Merging results from multiple operations
// - Creating array pipelines with functional composition
// - Creating array transformation pipelines
// - Appending batches of elements
//
// Mathematical properties:
//
// - Associativity: Concat(c)(Concat(b)(a)) == Concat(Concat(c)(b))(a)
// - Identity: Concat([])(a) == a and Concat(a)([]) == a
// - Length: len(Concat(b)(a)) == len(a) + len(b)
//
// Performance:
//
// - Time complexity: O(n + m) where n and m are the lengths of the arrays
// - Space complexity: O(n + m) for the new array
// - Optimized to avoid allocation when one array is empty
@@ -601,9 +646,15 @@ func Push[A any](a A) Operator[A, A] {
// Note: This function is immutable - it creates a new array rather than modifying
// the input arrays. For appending a single element, consider using Append or Push.
//
// See Also:
//
// - Append: For appending a single element
// - Push: Curried version of Append
// - Flatten: For flattening nested arrays
//
//go:inline
func Concat[A any](as []A) Operator[A, A] {
return F.Bind2nd(array.Concat[[]A, A], as)
func Concat[A any](suffix []A) Operator[A, A] {
return F.Bind2nd(array.Concat[[]A, A], suffix)
}
// MonadFlap applies a value to an array of functions, producing an array of results.

View File

@@ -767,6 +767,25 @@ func TestExtendUseCases(t *testing.T) {
// TestConcat tests the Concat function
func TestConcat(t *testing.T) {
t.Run("Semantic: Concat(b)(a) produces [a... b...]", func(t *testing.T) {
a := []int{1, 2, 3}
b := []int{4, 5, 6}
// Concat(b)(a) should produce [a... b...]
result := Concat(b)(a)
expected := []int{1, 2, 3, 4, 5, 6}
assert.Equal(t, expected, result, "Concat(b)(a) should produce [a... b...]")
// Verify order: a's elements come first, then b's elements
assert.Equal(t, a[0], result[0], "First element should be from a")
assert.Equal(t, a[1], result[1], "Second element should be from a")
assert.Equal(t, a[2], result[2], "Third element should be from a")
assert.Equal(t, b[0], result[3], "Fourth element should be from b")
assert.Equal(t, b[1], result[4], "Fifth element should be from b")
assert.Equal(t, b[2], result[5], "Sixth element should be from b")
})
t.Run("Concat two non-empty arrays", func(t *testing.T) {
base := []int{1, 2, 3}
toAppend := []int{4, 5, 6}
@@ -870,6 +889,54 @@ func TestConcat(t *testing.T) {
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
t.Run("Explicit append semantic demonstration", func(t *testing.T) {
// Given a base array
base := []string{"A", "B", "C"}
// And a suffix to append
suffix := []string{"D", "E", "F"}
// When we apply Concat(suffix) to base
appendSuffix := Concat(suffix)
result := appendSuffix(base)
// Then the result should be base followed by suffix
expected := []string{"A", "B", "C", "D", "E", "F"}
assert.Equal(t, expected, result)
// And the base should be unchanged
assert.Equal(t, []string{"A", "B", "C"}, base)
// And the suffix should be unchanged
assert.Equal(t, []string{"D", "E", "F"}, suffix)
})
t.Run("Append semantic with different types", func(t *testing.T) {
// Integers
intResult := Concat([]int{4, 5})([]int{1, 2, 3})
assert.Equal(t, []int{1, 2, 3, 4, 5}, intResult)
// Strings
strResult := Concat([]string{"world"})([]string{"hello"})
assert.Equal(t, []string{"hello", "world"}, strResult)
// Floats
floatResult := Concat([]float64{3.3, 4.4})([]float64{1.1, 2.2})
assert.Equal(t, []float64{1.1, 2.2, 3.3, 4.4}, floatResult)
})
t.Run("Append semantic in pipeline", func(t *testing.T) {
// Start with [1, 2, 3]
// Append [4, 5] to get [1, 2, 3, 4, 5]
// Append [6, 7] to get [1, 2, 3, 4, 5, 6, 7]
result := F.Pipe2(
[]int{1, 2, 3},
Concat([]int{4, 5}),
Concat([]int{6, 7}),
)
expected := []int{1, 2, 3, 4, 5, 6, 7}
assert.Equal(t, expected, result)
})
}
// TestConcatComposition tests Concat with other array operations

View File

@@ -18,6 +18,10 @@ package readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/internal/witherable"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/option"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
@@ -49,3 +53,43 @@ import (
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
return RIOR.FilterOrElse[context.Context](pred, onFalse)
}
//go:inline
func Filter[HKTA, A any](
filter func(Predicate[A]) Endomorphism[HKTA],
) func(Predicate[A]) Operator[HKTA, HKTA] {
return witherable.Filter(
Map,
filter,
)
}
//go:inline
func FilterArray[A any](p Predicate[A]) Operator[[]A, []A] {
return Filter(array.Filter[A])(p)
}
//go:inline
func FilterIter[A any](p Predicate[A]) Operator[Seq[A], Seq[A]] {
return Filter(iter.Filter[A])(p)
}
//go:inline
func FilterMap[HKTA, HKTB, A, B any](
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
) func(option.Kleisli[A, B]) Operator[HKTA, HKTB] {
return witherable.FilterMap(
Map,
filter,
)
}
//go:inline
func FilterMapArray[A, B any](p option.Kleisli[A, B]) Operator[[]A, []B] {
return FilterMap(array.FilterMap[A, B])(p)
}
//go:inline
func FilterMapIter[A, B any](p option.Kleisli[A, B]) Operator[Seq[A], Seq[B]] {
return FilterMap(iter.FilterMap[A, B])(p)
}

View File

@@ -17,6 +17,7 @@ package readerioresult
import (
"context"
"iter"
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/context/ioresult"
@@ -220,4 +221,10 @@ type (
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
// Seq is an iterator over sequences of individual values.
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
Seq[A any] = iter.Seq[A]
)

View File

@@ -0,0 +1,48 @@
package readerreaderioresult
import (
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/internal/witherable"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/option"
)
//go:inline
func Filter[C, HKTA, A any](
filter func(Predicate[A]) Endomorphism[HKTA],
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
return witherable.Filter(
Map[C],
filter,
)
}
//go:inline
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
return Filter[C](array.Filter[A])(p)
}
//go:inline
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
return Filter[C](iter.Filter[A])(p)
}
//go:inline
func FilterMap[C, HKTA, HKTB, A, B any](
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
return witherable.FilterMap(
Map[C],
filter,
)
}
//go:inline
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
return FilterMap[C](array.FilterMap[A, B])(p)
}
//go:inline
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
return FilterMap[C](iter.FilterMap[A, B])(p)
}

View File

@@ -834,7 +834,7 @@ func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endomorphism[error]) ReaderReaderIOResult[R, A] {
return RRIOE.MonadMapLeft(fa, f)
}
@@ -843,7 +843,7 @@ func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error])
// This is the curried version that returns an operator.
//
//go:inline
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
func MapLeft[R, A any](f Endomorphism[error]) Operator[R, A, A] {
return RRIOE.MapLeft[R, context.Context, A](f)
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/traversal/result"
@@ -146,9 +147,15 @@ type (
// It's an alias for predicate.Predicate[A].
Predicate[A any] = predicate.Predicate[A]
// Endmorphism represents a function from type A to type A.
// Endomorphism represents a function from type A to type A.
// It's an alias for endomorphism.Endomorphism[A].
Endmorphism[A any] = endomorphism.Endomorphism[A]
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Seq is an iterator over sequences of individual values.
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
Seq[A any] = iter.Seq[A]
Void = function.Void
)

296
v2/effect/filter.go Normal file
View File

@@ -0,0 +1,296 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/option"
)
// Filter lifts a filtering operation on a higher-kinded type into an Effect operator.
// This is a generic function that works with any filterable data structure by taking
// a filter function and returning an operator that can be used in effect chains.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - HKTA: The higher-kinded type being filtered (e.g., []A, Seq[A])
// - A: The element type being filtered
//
// # Parameters
//
// - filter: A function that takes a predicate and returns an endomorphism on HKTA
//
// # Returns
//
// - func(Predicate[A]) Operator[C, HKTA, HKTA]: A function that takes a predicate
// and returns an operator that filters effects containing HKTA values
//
// # Example Usage
//
// import A "github.com/IBM/fp-go/v2/array"
//
// // Create a custom filter operator for arrays
// filterOp := Filter[MyContext](A.Filter[int])
// isEven := func(n int) bool { return n%2 == 0 }
//
// pipeline := F.Pipe2(
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
// filterOp(isEven),
// Map[MyContext](func(arr []int) int { return len(arr) }),
// )
// // Result: Effect that produces 2 (count of even numbers)
//
// # See Also
//
// - FilterArray: Specialized version for array filtering
// - FilterIter: Specialized version for iterator filtering
// - FilterMap: For filtering and mapping simultaneously
//
//go:inline
func Filter[C, HKTA, A any](
filter func(Predicate[A]) Endomorphism[HKTA],
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
return readerreaderioresult.Filter[C](filter)
}
// FilterArray creates an operator that filters array elements within an Effect based on a predicate.
// Elements that satisfy the predicate are kept, while others are removed.
// This is a specialized version of Filter for arrays.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The element type in the array
//
// # Parameters
//
// - p: A predicate function that tests each element
//
// # Returns
//
// - Operator[C, []A, []A]: An operator that filters array elements in an effect
//
// # Example Usage
//
// isPositive := func(n int) bool { return n > 0 }
// filterPositive := FilterArray[MyContext](isPositive)
//
// pipeline := F.Pipe1(
// Succeed[MyContext]([]int{-2, -1, 0, 1, 2, 3}),
// filterPositive,
// )
// // Result: Effect that produces []int{1, 2, 3}
//
// # See Also
//
// - Filter: Generic version for any filterable type
// - FilterIter: For filtering iterators
// - FilterMapArray: For filtering and mapping arrays simultaneously
//
//go:inline
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
return readerreaderioresult.FilterArray[C](p)
}
// FilterIter creates an operator that filters iterator elements within an Effect based on a predicate.
// Elements that satisfy the predicate are kept in the resulting iterator, while others are removed.
// This is a specialized version of Filter for iterators (Seq).
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The element type in the iterator
//
// # Parameters
//
// - p: A predicate function that tests each element
//
// # Returns
//
// - Operator[C, Seq[A], Seq[A]]: An operator that filters iterator elements in an effect
//
// # Example Usage
//
// isEven := func(n int) bool { return n%2 == 0 }
// filterEven := FilterIter[MyContext](isEven)
//
// pipeline := F.Pipe1(
// Succeed[MyContext](slices.Values([]int{1, 2, 3, 4, 5, 6})),
// filterEven,
// )
// // Result: Effect that produces an iterator over [2, 4, 6]
//
// # See Also
//
// - Filter: Generic version for any filterable type
// - FilterArray: For filtering arrays
// - FilterMapIter: For filtering and mapping iterators simultaneously
//
//go:inline
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
return readerreaderioresult.FilterIter[C](p)
}
// FilterMap lifts a filter-map operation on a higher-kinded type into an Effect operator.
// This combines filtering and mapping in a single operation: elements are transformed
// using a function that returns Option, and only Some values are kept in the result.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - HKTA: The input higher-kinded type (e.g., []A, Seq[A])
// - HKTB: The output higher-kinded type (e.g., []B, Seq[B])
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - filter: A function that takes an option.Kleisli and returns a transformation from HKTA to HKTB
//
// # Returns
//
// - func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB]: A function that takes a Kleisli arrow
// and returns an operator that filter-maps effects
//
// # Example Usage
//
// import A "github.com/IBM/fp-go/v2/array"
// import O "github.com/IBM/fp-go/v2/option"
//
// // Parse and filter positive integers
// parsePositive := func(s string) O.Option[int] {
// var n int
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n > 0 {
// return O.Some(n)
// }
// return O.None[int]()
// }
//
// filterMapOp := FilterMap[MyContext](A.FilterMap[string, int])
// pipeline := F.Pipe1(
// Succeed[MyContext]([]string{"1", "-2", "3", "invalid", "5"}),
// filterMapOp(parsePositive),
// )
// // Result: Effect that produces []int{1, 3, 5}
//
// # See Also
//
// - FilterMapArray: Specialized version for arrays
// - FilterMapIter: Specialized version for iterators
// - Filter: For filtering without transformation
//
//go:inline
func FilterMap[C, HKTA, HKTB, A, B any](
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
return readerreaderioresult.FilterMap[C](filter)
}
// FilterMapArray creates an operator that filters and maps array elements within an Effect.
// Each element is transformed using a function that returns Option[B]. Elements that
// produce Some(b) are kept in the result array, while None values are filtered out.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
//
// # Returns
//
// - Operator[C, []A, []B]: An operator that filter-maps array elements in an effect
//
// # Example Usage
//
// import O "github.com/IBM/fp-go/v2/option"
//
// // Double even numbers, filter out odd numbers
// doubleEven := func(n int) O.Option[int] {
// if n%2 == 0 {
// return O.Some(n * 2)
// }
// return O.None[int]()
// }
//
// pipeline := F.Pipe1(
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
// FilterMapArray[MyContext](doubleEven),
// )
// // Result: Effect that produces []int{4, 8}
//
// # See Also
//
// - FilterMap: Generic version for any filterable type
// - FilterMapIter: For filter-mapping iterators
// - FilterArray: For filtering without transformation
//
//go:inline
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
return readerreaderioresult.FilterMapArray[C](p)
}
// FilterMapIter creates an operator that filters and maps iterator elements within an Effect.
// Each element is transformed using a function that returns Option[B]. Elements that
// produce Some(b) are kept in the resulting iterator, while None values are filtered out.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
//
// # Returns
//
// - Operator[C, Seq[A], Seq[B]]: An operator that filter-maps iterator elements in an effect
//
// # Example Usage
//
// import O "github.com/IBM/fp-go/v2/option"
//
// // Parse strings to integers, keeping only valid ones
// parseInt := func(s string) O.Option[int] {
// var n int
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
//
// pipeline := F.Pipe1(
// Succeed[MyContext](slices.Values([]string{"1", "2", "invalid", "3"})),
// FilterMapIter[MyContext](parseInt),
// )
// // Result: Effect that produces an iterator over [1, 2, 3]
//
// # See Also
//
// - FilterMap: Generic version for any filterable type
// - FilterMapArray: For filter-mapping arrays
// - FilterIter: For filtering without transformation
//
//go:inline
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
return readerreaderioresult.FilterMapIter[C](p)
}

653
v2/effect/filter_test.go Normal file
View File

@@ -0,0 +1,653 @@
// 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 effect
import (
"errors"
"fmt"
"slices"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
type FilterTestConfig struct {
MaxValue int
MinValue int
}
// Helper to collect iterator results from an effect
func collectSeqEffect[C, A any](eff Effect[C, Seq[A]], cfg C) []A {
result, err := runEffect(eff, cfg)
if err != nil {
return nil
}
return slices.Collect(result)
}
func TestFilterArray_Success(t *testing.T) {
t.Run("filters array keeping matching elements", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterArray[FilterTestConfig](isPositive)
input := Succeed[FilterTestConfig]([]int{1, -2, 3, -4, 5})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{1, 3, 5}, result)
})
t.Run("returns empty array when no elements match", func(t *testing.T) {
// Arrange
isNegative := N.LessThan(0)
filterOp := FilterArray[FilterTestConfig](isNegative)
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{}, result)
})
t.Run("returns all elements when all match", func(t *testing.T) {
// Arrange
alwaysTrue := func(n int) bool { return true }
filterOp := FilterArray[FilterTestConfig](alwaysTrue)
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, result)
})
}
func TestFilterIter_Success(t *testing.T) {
t.Run("filters iterator keeping matching elements", func(t *testing.T) {
// Arrange
isEven := func(n int) bool { return n%2 == 0 }
filterOp := FilterIter[FilterTestConfig](isEven)
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5, 6}))
// Act
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Equal(t, []int{2, 4, 6}, collected)
})
t.Run("returns empty iterator when no elements match", func(t *testing.T) {
// Arrange
isNegative := N.LessThan(0)
filterOp := FilterIter[FilterTestConfig](isNegative)
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3}))
// Act
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Empty(t, collected)
})
}
func TestFilterArray_WithContext(t *testing.T) {
t.Run("uses context for filtering", func(t *testing.T) {
// Arrange
cfg := FilterTestConfig{MaxValue: 100, MinValue: 0}
inRange := func(n int) bool { return n >= cfg.MinValue && n <= cfg.MaxValue }
filterOp := FilterArray[FilterTestConfig](inRange)
input := Succeed[FilterTestConfig]([]int{-10, 50, 150, 75})
// Act
result, err := runEffect(filterOp(input), cfg)
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{50, 75}, result)
})
}
func TestFilterArray_EdgeCases(t *testing.T) {
t.Run("handles empty array", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterArray[FilterTestConfig](isPositive)
input := Succeed[FilterTestConfig]([]int{})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{}, result)
})
t.Run("preserves error from input", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterArray[FilterTestConfig](isPositive)
inputErr := errors.New("input error")
input := Fail[FilterTestConfig, []int](inputErr)
// Act
_, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, inputErr, err)
})
}
func TestFilterIter_EdgeCases(t *testing.T) {
t.Run("handles empty iterator", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterIter[FilterTestConfig](isPositive)
input := Succeed[FilterTestConfig](slices.Values([]int{}))
// Act
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Empty(t, collected)
})
t.Run("preserves error from input", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterIter[FilterTestConfig](isPositive)
inputErr := errors.New("input error")
input := Fail[FilterTestConfig, Seq[int]](inputErr)
// Act
_, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, inputErr, err)
})
}
func TestFilter_GenericFilter(t *testing.T) {
t.Run("works with custom filter function", func(t *testing.T) {
// Arrange
customFilter := func(p Predicate[int]) Endomorphism[[]int] {
return A.Filter(p)
}
filterOp := Filter[FilterTestConfig](customFilter)
isEven := func(n int) bool { return n%2 == 0 }
input := Succeed[FilterTestConfig]([]int{1, 2, 3, 4, 5})
// Act
result, err := runEffect(filterOp(isEven)(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{2, 4}, result)
})
}
func TestFilterMapArray_Success(t *testing.T) {
t.Run("filters and maps array elements", func(t *testing.T) {
// Arrange
parsePositive := func(n int) O.Option[string] {
if n > 0 {
return O.Some(fmt.Sprintf("positive:%d", n))
}
return O.None[string]()
}
filterMapOp := FilterMapArray[FilterTestConfig](parsePositive)
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4, 5})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []string{"positive:2", "positive:4", "positive:5"}, result)
})
t.Run("returns empty when no elements match", func(t *testing.T) {
// Arrange
neverMatch := func(n int) O.Option[int] {
return O.None[int]()
}
filterMapOp := FilterMapArray[FilterTestConfig](neverMatch)
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{}, result)
})
t.Run("maps all elements when all match", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapArray[FilterTestConfig](double)
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6}, result)
})
}
func TestFilterMapIter_Success(t *testing.T) {
t.Run("filters and maps iterator elements", func(t *testing.T) {
// Arrange
doubleEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * 2)
}
return O.None[int]()
}
filterMapOp := FilterMapIter[FilterTestConfig](doubleEven)
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
// Act
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.Equal(t, []int{4, 8}, collected)
})
}
func TestFilterMapArray_TypeConversion(t *testing.T) {
t.Run("converts int to string", func(t *testing.T) {
// Arrange
intToString := func(n int) O.Option[string] {
if n > 0 {
return O.Some(fmt.Sprintf("%d", n))
}
return O.None[string]()
}
filterMapOp := FilterMapArray[FilterTestConfig](intToString)
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []string{"2", "4"}, result)
})
t.Run("converts string to int", func(t *testing.T) {
// Arrange
parseEven := func(s string) O.Option[int] {
var n int
if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n%2 == 0 {
return O.Some(n)
}
return O.None[int]()
}
filterMapOp := FilterMapArray[FilterTestConfig](parseEven)
input := Succeed[FilterTestConfig]([]string{"1", "2", "3", "4", "invalid"})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{2, 4}, result)
})
}
func TestFilterMapArray_EdgeCases(t *testing.T) {
t.Run("handles empty array", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapArray[FilterTestConfig](double)
input := Succeed[FilterTestConfig]([]int{})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{}, result)
})
t.Run("preserves error from input", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapArray[FilterTestConfig](double)
inputErr := errors.New("input error")
input := Fail[FilterTestConfig, []int](inputErr)
// Act
_, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, inputErr, err)
})
}
func TestFilterMapIter_EdgeCases(t *testing.T) {
t.Run("handles empty iterator", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapIter[FilterTestConfig](double)
input := Succeed[FilterTestConfig](slices.Values([]int{}))
// Act
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.Empty(t, collected)
})
}
func TestFilterMap_GenericFilterMap(t *testing.T) {
t.Run("works with custom filterMap function", func(t *testing.T) {
// Arrange
customFilterMap := func(f O.Kleisli[int, string]) Reader[[]int, []string] {
return A.FilterMap(f)
}
filterMapOp := FilterMap[FilterTestConfig](customFilterMap)
intToString := func(n int) O.Option[string] {
if n > 0 {
return O.Some(fmt.Sprintf("%d", n))
}
return O.None[string]()
}
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
// Act
result, err := runEffect(filterMapOp(intToString)(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []string{"2", "4"}, result)
})
}
func TestFilter_Composition(t *testing.T) {
t.Run("chains multiple filters", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
isEven := func(n int) bool { return n%2 == 0 }
filterPositive := FilterArray[FilterTestConfig](isPositive)
filterEven := FilterArray[FilterTestConfig](isEven)
input := Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6})
// Act
result, err := runEffect(filterEven(filterPositive(input)), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6}, result)
})
t.Run("chains filter and filterMap", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
doubleEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * 2)
}
return O.None[int]()
}
filterOp := FilterArray[FilterTestConfig](isPositive)
filterMapOp := FilterMapArray[FilterTestConfig](doubleEven)
input := Succeed[FilterTestConfig]([]int{-2, 1, 2, 3, 4, 5})
// Act
result, err := runEffect(filterMapOp(filterOp(input)), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{4, 8}, result)
})
}
func TestFilter_WithComplexTypes(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("filters structs", func(t *testing.T) {
// Arrange
isAdult := func(u User) bool { return u.Age >= 18 }
filterOp := FilterArray[FilterTestConfig](isAdult)
users := []User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 16},
{Name: "Charlie", Age: 30},
}
input := Succeed[FilterTestConfig](users)
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
expected := []User{
{Name: "Alice", Age: 25},
{Name: "Charlie", Age: 30},
}
assert.Equal(t, expected, result)
})
t.Run("filterMaps structs to different type", func(t *testing.T) {
// Arrange
extractAdultName := func(u User) O.Option[string] {
if u.Age >= 18 {
return O.Some(u.Name)
}
return O.None[string]()
}
filterMapOp := FilterMapArray[FilterTestConfig](extractAdultName)
users := []User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 16},
{Name: "Charlie", Age: 30},
}
input := Succeed[FilterTestConfig](users)
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []string{"Alice", "Charlie"}, result)
})
}
func TestFilter_BoundaryConditions(t *testing.T) {
t.Run("filters with boundary predicate", func(t *testing.T) {
// Arrange
inRange := func(n int) bool { return n >= 0 && n <= 100 }
filterOp := FilterArray[FilterTestConfig](inRange)
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{0, 50, 100}, result)
})
t.Run("filterMap with boundary conditions", func(t *testing.T) {
// Arrange
clampToRange := func(n int) O.Option[int] {
if n >= 0 && n <= 100 {
return O.Some(n)
}
return O.None[int]()
}
filterMapOp := FilterMapArray[FilterTestConfig](clampToRange)
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{0, 50, 100}, result)
})
}
func TestFilter_WithIterators(t *testing.T) {
t.Run("filters large iterator efficiently", func(t *testing.T) {
// Arrange
isEven := func(n int) bool { return n%2 == 0 }
filterOp := FilterIter[FilterTestConfig](isEven)
// Create iterator for range 0-99
makeSeq := func(yield func(int) bool) {
for i := range 100 {
if !yield(i) {
return
}
}
}
input := Succeed[FilterTestConfig](Seq[int](makeSeq))
// Act
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Equal(t, 50, len(collected))
assert.Equal(t, 0, collected[0])
assert.Equal(t, 98, collected[49])
})
t.Run("filterMap with iterator", func(t *testing.T) {
// Arrange
squareEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * n)
}
return O.None[int]()
}
filterMapOp := FilterMapIter[FilterTestConfig](squareEven)
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
// Act
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.Equal(t, []int{4, 16}, collected)
})
}
func TestFilter_ErrorPropagation(t *testing.T) {
t.Run("filter propagates Left through chain", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterArray[FilterTestConfig](isPositive)
originalErr := errors.New("original error")
// Create an effect that fails
failedEffect := F.Pipe1(
Succeed[FilterTestConfig]([]int{1, 2, 3}),
Chain(func([]int) Effect[FilterTestConfig, []int] {
return Fail[FilterTestConfig, []int](originalErr)
}),
)
// Act
_, err := runEffect(filterOp(failedEffect), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, originalErr, err)
})
t.Run("filterMap propagates Left through chain", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapArray[FilterTestConfig](double)
originalErr := errors.New("original error")
// Create an effect that fails
failedEffect := F.Pipe1(
Succeed[FilterTestConfig]([]int{1, 2, 3}),
Chain(func([]int) Effect[FilterTestConfig, []int] {
return Fail[FilterTestConfig, []int](originalErr)
}),
)
// Act
_, err := runEffect(filterMapOp(failedEffect), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, originalErr, err)
})
}
func TestFilter_Integration(t *testing.T) {
t.Run("complex filtering pipeline", func(t *testing.T) {
// Arrange: Filter positive numbers, then double evens, then filter > 5
isPositive := N.MoreThan(0)
doubleEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * 2)
}
return O.None[int]()
}
isGreaterThan5 := N.MoreThan(5)
pipeline := F.Pipe3(
Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6}),
FilterArray[FilterTestConfig](isPositive),
FilterMapArray[FilterTestConfig](doubleEven),
FilterArray[FilterTestConfig](isGreaterThan5),
)
// Act
result, err := runEffect(pipeline, FilterTestConfig{})
// Assert
assert.NoError(t, err)
// Positive: [1,2,3,4,5,6] -> DoubleEven: [4,8,12] -> >5: [8,12]
assert.Equal(t, []int{8, 12}, result)
})
}
// Made with Bob

View File

@@ -19,9 +19,11 @@ import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
@@ -89,4 +91,14 @@ type (
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
// It's used for lifting operations over effects.
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
// Endomorphism represents a function from type A to type A.
// It's an alias for endomorphism.Endomorphism[A].
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Seq is an iterator over sequences of individual values.
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
Seq[A any] = iter.Seq[A]
)

View File

@@ -236,6 +236,7 @@ func Pipe4[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, T
// The final return value is the result of the last function application
//go:inline
func Flow4[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, T0, T1, T2, T3, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(T0) T4 {
//go:inline
return func(t0 T0) T4 {
return Pipe4(t0, f1, f2, f3, f4)
}
@@ -302,6 +303,7 @@ func Pipe5[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
// The final return value is the result of the last function application
//go:inline
func Flow5[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, T0, T1, T2, T3, T4, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(T0) T5 {
//go:inline
return func(t0 T0) T5 {
return Pipe5(t0, f1, f2, f3, f4, f5)
}
@@ -370,6 +372,7 @@ func Pipe6[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
// The final return value is the result of the last function application
//go:inline
func Flow6[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, T0, T1, T2, T3, T4, T5, T6 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6) func(T0) T6 {
//go:inline
return func(t0 T0) T6 {
return Pipe6(t0, f1, f2, f3, f4, f5, f6)
}
@@ -440,6 +443,7 @@ func Pipe7[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
// The final return value is the result of the last function application
//go:inline
func Flow7[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, T0, T1, T2, T3, T4, T5, T6, T7 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7) func(T0) T7 {
//go:inline
return func(t0 T0) T7 {
return Pipe7(t0, f1, f2, f3, f4, f5, f6, f7)
}
@@ -512,6 +516,7 @@ func Pipe8[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
// The final return value is the result of the last function application
//go:inline
func Flow8[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, T0, T1, T2, T3, T4, T5, T6, T7, T8 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8) func(T0) T8 {
//go:inline
return func(t0 T0) T8 {
return Pipe8(t0, f1, f2, f3, f4, f5, f6, f7, f8)
}
@@ -586,6 +591,7 @@ func Pipe9[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
// The final return value is the result of the last function application
//go:inline
func Flow9[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9) func(T0) T9 {
//go:inline
return func(t0 T0) T9 {
return Pipe9(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9)
}
@@ -662,6 +668,7 @@ func Pipe10[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow10[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10) func(T0) T10 {
//go:inline
return func(t0 T0) T10 {
return Pipe10(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10)
}
@@ -740,6 +747,7 @@ func Pipe11[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow11[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11) func(T0) T11 {
//go:inline
return func(t0 T0) T11 {
return Pipe11(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11)
}
@@ -820,6 +828,7 @@ func Pipe12[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow12[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12) func(T0) T12 {
//go:inline
return func(t0 T0) T12 {
return Pipe12(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12)
}
@@ -902,6 +911,7 @@ func Pipe13[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow13[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13) func(T0) T13 {
//go:inline
return func(t0 T0) T13 {
return Pipe13(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13)
}
@@ -986,6 +996,7 @@ func Pipe14[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow14[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14) func(T0) T14 {
//go:inline
return func(t0 T0) T14 {
return Pipe14(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14)
}
@@ -1072,6 +1083,7 @@ func Pipe15[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow15[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15) func(T0) T15 {
//go:inline
return func(t0 T0) T15 {
return Pipe15(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15)
}
@@ -1160,6 +1172,7 @@ func Pipe16[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow16[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16) func(T0) T16 {
//go:inline
return func(t0 T0) T16 {
return Pipe16(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16)
}
@@ -1250,6 +1263,7 @@ func Pipe17[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow17[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17) func(T0) T17 {
//go:inline
return func(t0 T0) T17 {
return Pipe17(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17)
}
@@ -1342,6 +1356,7 @@ func Pipe18[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow18[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18) func(T0) T18 {
//go:inline
return func(t0 T0) T18 {
return Pipe18(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18)
}
@@ -1436,6 +1451,7 @@ func Pipe19[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow19[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, F19 ~func(T18) T19, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18, f19 F19) func(T0) T19 {
//go:inline
return func(t0 T0) T19 {
return Pipe19(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19)
}
@@ -1532,6 +1548,7 @@ func Pipe20[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
// The final return value is the result of the last function application
//go:inline
func Flow20[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, F19 ~func(T18) T19, F20 ~func(T19) T20, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18, f19 F19, f20 F20) func(T0) T20 {
//go:inline
return func(t0 T0) T20 {
return Pipe20(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20)
}

View File

@@ -0,0 +1,15 @@
package filterable
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
)
type (
Option[A any] = option.Option[A]
Separated[A, B any] = pair.Pair[A, B]
FilterType[A, HKTA any] = func(func(A) bool) func(HKTA) HKTA
FilterMapType[A, B, HKTA, HKTB any] = func(func(A) Option[B]) func(HKTA) HKTB
)

View File

@@ -0,0 +1,27 @@
package witherable
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/filterable"
"github.com/IBM/fp-go/v2/internal/functor"
)
func Filter[A, HKT_G_A, HKT_F_HKT_G_A any](
fmap functor.MapType[HKT_G_A, HKT_G_A, HKT_F_HKT_G_A, HKT_F_HKT_G_A],
ffilter filterable.FilterType[A, HKT_G_A],
) func(func(A) bool) func(HKT_F_HKT_G_A) HKT_F_HKT_G_A {
return function.Flow2(
ffilter,
fmap,
)
}
func FilterMap[A, B, HKT_G_A, HKT_G_B, HKT_F_HKT_G_A, HKT_F_HKT_G_B any](
fmap functor.MapType[HKT_G_A, HKT_G_B, HKT_F_HKT_G_A, HKT_F_HKT_G_B],
ffilter filterable.FilterMapType[A, B, HKT_G_A, HKT_G_B],
) func(func(A) Option[B]) func(HKT_F_HKT_G_A) HKT_F_HKT_G_B {
return function.Flow2(
ffilter,
fmap,
)
}

View File

@@ -0,0 +1 @@
package witherable

View File

@@ -0,0 +1,7 @@
package witherable
import "github.com/IBM/fp-go/v2/option"
type (
Option[A any] = option.Option[A]
)

169
v2/optics/codec/iso.go Normal file
View File

@@ -0,0 +1,169 @@
// 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 codec
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/decode"
)
// FromIso creates a Type codec from an Iso (isomorphism).
//
// An isomorphism represents a bidirectional transformation between types I and A
// without any loss of information. This function converts an Iso[I, A] into a
// Type[A, I, I] codec that can validate, decode, and encode values using the
// isomorphism's transformations.
//
// The resulting codec:
// - Decode: Uses iso.Get to transform I → A, always succeeds (no validation)
// - Encode: Uses iso.ReverseGet to transform A → I
// - Validation: Always succeeds since isomorphisms are lossless transformations
// - Type checking: Uses standard type checking for type A
//
// This is particularly useful for:
// - Creating codecs for newtype patterns (wrapping/unwrapping types)
// - Building codecs for types with lossless conversions
// - Composing with other codecs using Pipe or other operators
// - Implementing bidirectional transformations in codec pipelines
//
// # Type Parameters
//
// - A: The target type (what we decode to and encode from)
// - I: The input/output type (what we decode from and encode to)
//
// # Parameters
//
// - iso: An Iso[I, A] that defines the bidirectional transformation:
// - Get: I → A (converts input to target type)
// - ReverseGet: A → I (converts target back to input type)
//
// # Returns
//
// - A Type[A, I, I] codec where:
// - Decode: I → Validation[A] - transforms using iso.Get, always succeeds
// - Encode: A → I - transforms using iso.ReverseGet
// - Is: Checks if a value is of type A
// - Name: Returns "FromIso[iso_string_representation]"
//
// # Behavior
//
// Decoding:
// - Applies iso.Get to transform the input value
// - Wraps the result in decode.Of (always successful validation)
// - No validation errors can occur since isomorphisms are lossless
//
// Encoding:
// - Applies iso.ReverseGet to transform back to the input type
// - Always succeeds as isomorphisms guarantee reversibility
//
// # Example Usage
//
// Creating a codec for a newtype pattern:
//
// type UserId int
//
// // Define an isomorphism between int and UserId
// userIdIso := iso.MakeIso(
// func(id UserId) int { return int(id) },
// func(i int) UserId { return UserId(i) },
// )
//
// // Create a codec from the isomorphism
// userIdCodec := codec.FromIso[int, UserId](userIdIso)
//
// // Decode: UserId → int
// result := userIdCodec.Decode(UserId(42)) // Success: Right(42)
//
// // Encode: int → UserId
// encoded := userIdCodec.Encode(42) // Returns: UserId(42)
//
// Using with temperature conversions:
//
// type Celsius float64
// type Fahrenheit float64
//
// celsiusToFahrenheit := iso.MakeIso(
// func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
// func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
// )
//
// tempCodec := codec.FromIso[Fahrenheit, Celsius](celsiusToFahrenheit)
//
// // Decode: Celsius → Fahrenheit
// result := tempCodec.Decode(Celsius(20)) // Success: Right(68°F)
//
// // Encode: Fahrenheit → Celsius
// encoded := tempCodec.Encode(Fahrenheit(68)) // Returns: 20°C
//
// Composing with other codecs:
//
// type Email string
// type ValidatedEmail struct{ value Email }
//
// emailIso := iso.MakeIso(
// func(ve ValidatedEmail) Email { return ve.value },
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
// )
//
// // Compose with string codec for validation
// emailCodec := F.Pipe2(
// codec.String(), // Type[string, string, any]
// codec.Pipe(codec.FromIso[Email, string]( // Add string → Email iso
// iso.MakeIso(
// func(s string) Email { return Email(s) },
// func(e Email) string { return string(e) },
// ),
// )),
// codec.Pipe(codec.FromIso[ValidatedEmail, Email](emailIso)), // Add Email → ValidatedEmail iso
// )
//
// # Use Cases
//
// - Newtype patterns: Wrapping primitive types for type safety
// - Unit conversions: Temperature, distance, time, etc.
// - Format transformations: Between equivalent representations
// - Type aliasing: Creating semantic types from base types
// - Codec composition: Building complex codecs from simple isomorphisms
//
// # Notes
//
// - Isomorphisms must satisfy the round-trip laws:
// - iso.ReverseGet(iso.Get(i)) == i
// - iso.Get(iso.ReverseGet(a)) == a
// - Validation always succeeds since isomorphisms are lossless
// - The codec name includes the isomorphism's string representation
// - Type checking is performed using the standard Is[A]() function
// - This codec is ideal for lossless transformations without validation logic
//
// # See Also
//
// - iso.Iso: The isomorphism type used by this function
// - iso.MakeIso: Constructor for creating isomorphisms
// - Pipe: For composing this codec with other codecs
// - MakeType: For creating codecs with custom validation logic
func FromIso[A, I any](iso Iso[I, A]) Type[A, I, I] {
return MakeType(
fmt.Sprintf("FromIso[%s]", iso),
Is[A](),
F.Flow2(
iso.Get,
decode.Of[Context],
),
iso.ReverseGet,
)
}

504
v2/optics/codec/iso_test.go Normal file
View File

@@ -0,0 +1,504 @@
// 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 codec
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/stretchr/testify/assert"
)
// Test types for newtype pattern
type UserId int
type Email string
type Celsius float64
type Fahrenheit float64
func TestFromIso_Success(t *testing.T) {
t.Run("decodes using iso.Get", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
result := codec.Decode(UserId(42))
// Assert
assert.Equal(t, validation.Success(42), result)
})
t.Run("encodes using iso.ReverseGet", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
encoded := codec.Encode(42)
// Assert
assert.Equal(t, UserId(42), encoded)
})
t.Run("round-trip preserves value", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
original := UserId(123)
// Act
decoded := codec.Decode(original)
// Assert
assert.True(t, either.IsRight(decoded))
roundTrip := either.Fold[validation.Errors, int, UserId](
func(validation.Errors) UserId { return UserId(0) },
codec.Encode,
)(decoded)
assert.Equal(t, original, roundTrip)
})
}
func TestFromIso_StringTypes(t *testing.T) {
t.Run("handles string newtype", func(t *testing.T) {
// Arrange
emailIso := iso.MakeIso(
func(e Email) string { return string(e) },
func(s string) Email { return Email(s) },
)
codec := FromIso[string, Email](emailIso)
// Act
result := codec.Decode(Email("user@example.com"))
// Assert
assert.Equal(t, validation.Success("user@example.com"), result)
})
t.Run("encodes string newtype", func(t *testing.T) {
// Arrange
emailIso := iso.MakeIso(
func(e Email) string { return string(e) },
func(s string) Email { return Email(s) },
)
codec := FromIso[string, Email](emailIso)
// Act
encoded := codec.Encode("admin@example.com")
// Assert
assert.Equal(t, Email("admin@example.com"), encoded)
})
t.Run("handles empty string", func(t *testing.T) {
// Arrange
emailIso := iso.MakeIso(
func(e Email) string { return string(e) },
func(s string) Email { return Email(s) },
)
codec := FromIso[string, Email](emailIso)
// Act
result := codec.Decode(Email(""))
// Assert
assert.Equal(t, validation.Success(""), result)
})
}
func TestFromIso_NumericConversions(t *testing.T) {
t.Run("converts Celsius to Fahrenheit", func(t *testing.T) {
// Arrange
tempIso := iso.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
codec := FromIso[Fahrenheit, Celsius](tempIso)
// Act
result := codec.Decode(Celsius(0))
// Assert
assert.Equal(t, validation.Success(Fahrenheit(32)), result)
})
t.Run("converts Fahrenheit to Celsius", func(t *testing.T) {
// Arrange
tempIso := iso.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
codec := FromIso[Fahrenheit, Celsius](tempIso)
// Act
encoded := codec.Encode(Fahrenheit(68))
// Assert
assert.Equal(t, Celsius(20), encoded)
})
t.Run("handles negative temperatures", func(t *testing.T) {
// Arrange
tempIso := iso.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
codec := FromIso[Fahrenheit, Celsius](tempIso)
// Act
result := codec.Decode(Celsius(-40))
// Assert
assert.Equal(t, validation.Success(Fahrenheit(-40)), result)
})
t.Run("temperature round-trip", func(t *testing.T) {
// Arrange
tempIso := iso.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
codec := FromIso[Fahrenheit, Celsius](tempIso)
original := Celsius(25)
// Act
decoded := codec.Decode(original)
// Assert
assert.True(t, either.IsRight(decoded))
roundTrip := either.Fold[validation.Errors, Fahrenheit, Celsius](
func(validation.Errors) Celsius { return Celsius(0) },
codec.Encode,
)(decoded)
// Allow small floating point error
diff := float64(original - roundTrip)
if diff < 0 {
diff = -diff
}
assert.True(t, diff < 0.0001)
})
}
func TestFromIso_EdgeCases(t *testing.T) {
t.Run("handles zero values", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
result := codec.Decode(UserId(0))
// Assert
assert.Equal(t, validation.Success(0), result)
})
t.Run("handles negative values", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
result := codec.Decode(UserId(-1))
// Assert
assert.Equal(t, validation.Success(-1), result)
})
t.Run("handles large values", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
result := codec.Decode(UserId(999999999))
// Assert
assert.Equal(t, validation.Success(999999999), result)
})
}
func TestFromIso_TypeChecking(t *testing.T) {
t.Run("Is checks target type", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
isResult := codec.Is(42)
// Assert
assert.True(t, either.IsRight(isResult))
})
t.Run("Is rejects wrong type", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
isResult := codec.Is("not an int")
// Assert
assert.True(t, either.IsLeft(isResult))
})
}
func TestFromIso_Name(t *testing.T) {
t.Run("includes iso in name", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
name := codec.Name()
// Assert
assert.True(t, len(name) > 0)
assert.True(t, name[:7] == "FromIso")
})
}
func TestFromIso_Composition(t *testing.T) {
t.Run("composes with Pipe", func(t *testing.T) {
// Arrange
type PositiveInt int
// First iso: UserId -> int
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
// Second iso: int -> PositiveInt (no validation, just type conversion)
positiveIso := iso.MakeIso(
func(i int) PositiveInt { return PositiveInt(i) },
func(p PositiveInt) int { return int(p) },
)
// Compose codecs
codec := F.Pipe1(
FromIso[int, UserId](userIdIso),
Pipe[UserId, UserId](FromIso[PositiveInt, int](positiveIso)),
)
// Act
result := codec.Decode(UserId(42))
// Assert
assert.Equal(t, validation.Of(PositiveInt(42)), result)
})
t.Run("composed codec encodes correctly", func(t *testing.T) {
// Arrange
type PositiveInt int
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
positiveIso := iso.MakeIso(
func(i int) PositiveInt { return PositiveInt(i) },
func(p PositiveInt) int { return int(p) },
)
codec := F.Pipe1(
FromIso[int, UserId](userIdIso),
Pipe[UserId, UserId](FromIso[PositiveInt, int](positiveIso)),
)
// Act
encoded := codec.Encode(PositiveInt(42))
// Assert
assert.Equal(t, UserId(42), encoded)
})
}
func TestFromIso_Integration(t *testing.T) {
t.Run("works with Array codec", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
userIdCodec := FromIso[int, UserId](userIdIso)
arrayCodec := TranscodeArray(userIdCodec)
// Act
result := arrayCodec.Decode([]UserId{UserId(1), UserId(2), UserId(3)})
// Assert
assert.Equal(t, validation.Success([]int{1, 2, 3}), result)
})
t.Run("encodes array correctly", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
userIdCodec := FromIso[int, UserId](userIdIso)
arrayCodec := TranscodeArray(userIdCodec)
// Act
encoded := arrayCodec.Encode([]int{1, 2, 3})
// Assert
assert.Equal(t, []UserId{UserId(1), UserId(2), UserId(3)}, encoded)
})
t.Run("handles empty array", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
userIdCodec := FromIso[int, UserId](userIdIso)
arrayCodec := TranscodeArray(userIdCodec)
// Act
result := arrayCodec.Decode([]UserId{})
// Assert
assert.True(t, either.IsRight(result))
decoded := either.Fold[validation.Errors, []int, []int](
func(validation.Errors) []int { return nil },
func(arr []int) []int { return arr },
)(result)
assert.Equal(t, 0, len(decoded))
})
}
func TestFromIso_ComplexTypes(t *testing.T) {
t.Run("handles struct wrapping", func(t *testing.T) {
// Arrange
type Wrapper struct{ Value int }
wrapperIso := iso.MakeIso(
func(w Wrapper) int { return w.Value },
func(i int) Wrapper { return Wrapper{Value: i} },
)
codec := FromIso[int, Wrapper](wrapperIso)
// Act
result := codec.Decode(Wrapper{Value: 42})
// Assert
assert.Equal(t, validation.Success(42), result)
})
t.Run("encodes struct wrapping", func(t *testing.T) {
// Arrange
type Wrapper struct{ Value int }
wrapperIso := iso.MakeIso(
func(w Wrapper) int { return w.Value },
func(i int) Wrapper { return Wrapper{Value: i} },
)
codec := FromIso[int, Wrapper](wrapperIso)
// Act
encoded := codec.Encode(42)
// Assert
assert.Equal(t, Wrapper{Value: 42}, encoded)
})
}
func TestFromIso_AsDecoder(t *testing.T) {
t.Run("returns decoder interface", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
decoder := codec.AsDecoder()
// Assert
result := decoder.Decode(UserId(42))
assert.Equal(t, validation.Success(42), result)
})
}
func TestFromIso_AsEncoder(t *testing.T) {
t.Run("returns encoder interface", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
encoder := codec.AsEncoder()
// Assert
encoded := encoder.Encode(42)
assert.Equal(t, UserId(42), encoded)
})
}
func TestFromIso_Validate(t *testing.T) {
t.Run("validate method works correctly", func(t *testing.T) {
// Arrange
userIdIso := iso.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
codec := FromIso[int, UserId](userIdIso)
// Act
validateFn := codec.Validate(UserId(42))
result := validateFn([]validation.ContextEntry{})
// Assert
assert.Equal(t, validation.Success(42), result)
})
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/decoder"
"github.com/IBM/fp-go/v2/optics/encoder"
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
@@ -494,4 +495,7 @@ type (
// - function.VOID: The single value of type Void
// - Empty: Codec function that uses Void for unit types
Void = function.Void
// Iso represents an isomorphism - a bidirectional transformation between two types.
Iso[S, A any] = iso.Iso[S, A]
)

View File

@@ -22,6 +22,7 @@ import (
"github.com/IBM/fp-go/v2/endomorphism"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
)
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
@@ -909,6 +910,83 @@ func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S
}
}
// ModifyF transforms a value through a lens using a function that returns a value in a functor context.
//
// This is the functorial version of Modify, allowing transformations that produce effects
// (like Option, Either, IO, etc.) while updating the focused value. The functor's map operation
// is used to apply the lens's setter to the transformed value, preserving the computational context.
//
// This function corresponds to modifyF from monocle-ts, enabling effectful updates through lenses.
//
// # Type Parameters
//
// - S: Structure type
// - A: Focus type (the value being transformed)
// - HKTA: Higher-kinded type containing the transformed value (e.g., Option[A], Either[E, A])
// - HKTS: Higher-kinded type containing the updated structure (e.g., Option[S], Either[E, S])
//
// # Parameters
//
// - fmap: A functor map operation that transforms A to S within the functor context
//
// # Returns
//
// - A curried function that takes:
// 1. A transformation function (A → HKTA)
// 2. A Lens[S, A]
// 3. A structure S
// And returns the updated structure in the functor context (HKTS)
//
// # Example Usage
//
// type Person struct {
// Name string
// Age int
// }
//
// ageLens := lens.MakeLens(
// func(p Person) int { return p.Age },
// func(p Person, age int) Person { p.Age = age; return p },
// )
//
// // Validate age is positive, returning Option
// validateAge := func(age int) option.Option[int] {
// if age > 0 {
// return option.Some(age)
// }
// return option.None[int]()
// }
//
// // Create a modifier that validates while updating
// modifyAge := lens.ModifyF[Person, int](option.Functor[int, Person]().Map)
//
// person := Person{Name: "Alice", Age: 30}
// result := modifyAge(validateAge)(ageLens)(person)
// // result is Some(Person{Name: "Alice", Age: 30})
//
// invalidResult := modifyAge(func(age int) option.Option[int] {
// return option.None[int]()
// })(ageLens)(person)
// // invalidResult is None[Person]()
//
// # See Also
//
// - Modify: Non-functorial version for simple transformations
// - functor.Functor: The functor interface used for mapping
func ModifyF[S, A, HKTA, HKTS any](
fmap functor.MapType[A, S, HKTA, HKTS],
) func(func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
return func(f func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
return func(sa Lens[S, A]) func(S) HKTS {
return func(s S) HKTS {
return fmap(func(a A) S {
return sa.Set(a)(s)
})(f(sa.Get(s)))
}
}
}
}
// IMap transforms the focus type of a lens using an isomorphism.
//
// An isomorphism is a pair of functions (A → B, B → A) that are inverses of each other.

View File

@@ -16,6 +16,7 @@
package lens
import (
"errors"
"testing"
EQ "github.com/IBM/fp-go/v2/eq"
@@ -937,3 +938,367 @@ func TestMakeLensWithEq_WithNilState_MultipleOperations(t *testing.T) {
assert.NotNil(t, street4)
assert.Equal(t, "", street4.name)
}
// TestModifyF_Success tests ModifyF with a simple Maybe-like functor for successful transformations
func TestModifyF_Success(t *testing.T) {
// Define a simple Maybe type for testing
type Maybe[A any] struct {
value *A
}
some := func(a int) Maybe[int] {
return Maybe[int]{value: &a}
}
none := func() Maybe[int] {
return Maybe[int]{value: nil}
}
// Functor map for Maybe
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
return func(ma Maybe[int]) Maybe[Inner] {
if ma.value == nil {
return Maybe[Inner]{value: nil}
}
result := f(*ma.value)
return Maybe[Inner]{value: &result}
}
}
t.Run("transforms value with successful result", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
// Function that returns Some for positive values
validatePositive := func(n int) Maybe[int] {
if n > 0 {
return some(n * 2)
}
return none()
}
modifyAge := ModifyF[Inner, int](maybeMap)
person := Inner{Value: 5, Foo: "test"}
result := modifyAge(validatePositive)(ageLens)(person)
assert.NotNil(t, result.value)
updated := *result.value
assert.Equal(t, 10, updated.Value)
assert.Equal(t, "test", updated.Foo)
})
t.Run("preserves structure with identity transformation", func(t *testing.T) {
type MaybeStr struct {
value *string
}
someStr := func(s string) MaybeStr {
return MaybeStr{value: &s}
}
maybeStrMap := func(f func(string) Street) func(MaybeStr) struct{ value *Street } {
return func(ma MaybeStr) struct{ value *Street } {
if ma.value == nil {
return struct{ value *Street }{value: nil}
}
result := f(*ma.value)
return struct{ value *Street }{value: &result}
}
}
nameLens := MakeLens(
func(s Street) string { return s.name },
func(s Street, name string) Street { s.name = name; return s },
)
identity := func(s string) MaybeStr {
return someStr(s)
}
modifyName := ModifyF[Street, string](maybeStrMap)
street := Street{num: 1, name: "Main"}
result := modifyName(identity)(nameLens)(street)
assert.NotNil(t, result.value)
assert.Equal(t, street, *result.value)
})
}
// TestModifyF_Failure tests ModifyF with failures
func TestModifyF_Failure(t *testing.T) {
type Maybe[A any] struct {
value *A
}
some := func(a int) Maybe[int] {
return Maybe[int]{value: &a}
}
none := func() Maybe[int] {
return Maybe[int]{value: nil}
}
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
return func(ma Maybe[int]) Maybe[Inner] {
if ma.value == nil {
return Maybe[Inner]{value: nil}
}
result := f(*ma.value)
return Maybe[Inner]{value: &result}
}
}
t.Run("returns None when transformation fails", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
validatePositive := func(n int) Maybe[int] {
if n > 0 {
return some(n)
}
return none()
}
modifyAge := ModifyF[Inner, int](maybeMap)
person := Inner{Value: -5, Foo: "test"}
result := modifyAge(validatePositive)(ageLens)(person)
assert.Nil(t, result.value)
})
}
// TestModifyF_WithResult tests ModifyF with Result/Either-like functor
func TestModifyF_WithResult(t *testing.T) {
type Result[A any] struct {
value *A
err error
}
ok := func(a int) Result[int] {
return Result[int]{value: &a, err: nil}
}
fail := func(e error) Result[int] {
return Result[int]{value: nil, err: e}
}
resultMap := func(f func(int) Inner) func(Result[int]) Result[Inner] {
return func(r Result[int]) Result[Inner] {
if r.err != nil {
return Result[Inner]{value: nil, err: r.err}
}
result := f(*r.value)
return Result[Inner]{value: &result, err: nil}
}
}
t.Run("returns success for valid transformation", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
validateAge := func(n int) Result[int] {
if n >= 0 && n <= 150 {
return ok(n + 1)
}
return fail(errors.New("age out of range"))
}
modifyAge := ModifyF[Inner, int](resultMap)
person := Inner{Value: 30, Foo: "test"}
result := modifyAge(validateAge)(ageLens)(person)
assert.Nil(t, result.err)
assert.NotNil(t, result.value)
assert.Equal(t, 31, result.value.Value)
assert.Equal(t, "test", result.value.Foo)
})
t.Run("returns error for failed validation", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
validateAge := func(n int) Result[int] {
if n >= 0 && n <= 150 {
return ok(n)
}
return fail(errors.New("age out of range"))
}
modifyAge := ModifyF[Inner, int](resultMap)
person := Inner{Value: 200, Foo: "test"}
result := modifyAge(validateAge)(ageLens)(person)
assert.NotNil(t, result.err)
assert.Equal(t, "age out of range", result.err.Error())
assert.Nil(t, result.value)
})
}
// TestModifyF_EdgeCases tests edge cases for ModifyF
func TestModifyF_EdgeCases(t *testing.T) {
type Maybe[A any] struct {
value *A
}
some := func(a int) Maybe[int] {
return Maybe[int]{value: &a}
}
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
return func(ma Maybe[int]) Maybe[Inner] {
if ma.value == nil {
return Maybe[Inner]{value: nil}
}
result := f(*ma.value)
return Maybe[Inner]{value: &result}
}
}
t.Run("handles zero values", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
identity := func(n int) Maybe[int] {
return some(n)
}
modifyAge := ModifyF[Inner, int](maybeMap)
person := Inner{Value: 0, Foo: ""}
result := modifyAge(identity)(ageLens)(person)
assert.NotNil(t, result.value)
assert.Equal(t, person, *result.value)
})
t.Run("works with composed lenses", func(t *testing.T) {
innerLens := MakeLens(
Outer.GetInner,
Outer.SetInner,
)
valueLens := MakeLensRef(
(*Inner).GetValue,
(*Inner).SetValue,
)
composedLens := Compose[Outer](valueLens)(innerLens)
maybeMapOuter := func(f func(int) Outer) func(Maybe[int]) Maybe[Outer] {
return func(ma Maybe[int]) Maybe[Outer] {
if ma.value == nil {
return Maybe[Outer]{value: nil}
}
result := f(*ma.value)
return Maybe[Outer]{value: &result}
}
}
validatePositive := func(n int) Maybe[int] {
if n > 0 {
return some(n * 2)
}
return Maybe[int]{value: nil}
}
modifyValue := ModifyF[Outer, int](maybeMapOuter)
outer := Outer{inner: &Inner{Value: 5, Foo: "test"}}
result := modifyValue(validatePositive)(composedLens)(outer)
assert.NotNil(t, result.value)
assert.Equal(t, 10, result.value.inner.Value)
assert.Equal(t, "test", result.value.inner.Foo)
})
}
// TestModifyF_Integration tests integration scenarios
func TestModifyF_Integration(t *testing.T) {
type Maybe[A any] struct {
value *A
}
some := func(a int) Maybe[int] {
return Maybe[int]{value: &a}
}
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
return func(ma Maybe[int]) Maybe[Inner] {
if ma.value == nil {
return Maybe[Inner]{value: nil}
}
result := f(*ma.value)
return Maybe[Inner]{value: &result}
}
}
t.Run("chains multiple ModifyF operations", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
increment := func(n int) Maybe[int] {
return some(n + 1)
}
modifyAge := ModifyF[Inner, int](maybeMap)
person := Inner{Value: 5, Foo: "test"}
// Apply transformation twice
result1 := modifyAge(increment)(ageLens)(person)
assert.NotNil(t, result1.value)
result2 := modifyAge(increment)(ageLens)(*result1.value)
assert.NotNil(t, result2.value)
assert.Equal(t, 7, result2.value.Value)
})
t.Run("combines with regular Modify", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
// First use regular Modify
person := Inner{Value: 5, Foo: "test"}
modified := F.Pipe2(
ageLens,
Modify[Inner](func(n int) int { return n * 2 }),
func(endoFn func(Inner) Inner) Inner {
return endoFn(person)
},
)
assert.Equal(t, 10, modified.Value)
// Then use ModifyF with validation
validateRange := func(n int) Maybe[int] {
if n >= 0 && n <= 100 {
return some(n)
}
return Maybe[int]{value: nil}
}
modifyAge := ModifyF[Inner, int](maybeMap)
result := modifyAge(validateRange)(ageLens)(modified)
assert.NotNil(t, result.value)
})
}

View File

@@ -5,6 +5,7 @@ package lenses
// 2026-01-27 16:08:47.5483589 +0100 CET m=+0.003380301
import (
"net"
url "net/url"
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
@@ -119,6 +120,8 @@ type URLLenses struct {
RawQuery __lens.Lens[url.URL, string]
Fragment __lens.Lens[url.URL, string]
RawFragment __lens.Lens[url.URL, string]
Hostname __lens.Lens[url.URL, string]
Port __lens.Lens[url.URL, string]
// optional fields
SchemeO __lens_option.LensO[url.URL, string]
OpaqueO __lens_option.LensO[url.URL, string]
@@ -131,6 +134,8 @@ type URLLenses struct {
RawQueryO __lens_option.LensO[url.URL, string]
FragmentO __lens_option.LensO[url.URL, string]
RawFragmentO __lens_option.LensO[url.URL, string]
HostnameO __lens_option.LensO[url.URL, string]
PortO __lens_option.LensO[url.URL, string]
}
// URLRefLenses provides lenses for accessing fields of url.URL via a reference to url.URL
@@ -147,6 +152,8 @@ type URLRefLenses struct {
RawQuery __lens.Lens[*url.URL, string]
Fragment __lens.Lens[*url.URL, string]
RawFragment __lens.Lens[*url.URL, string]
Hostname __lens.Lens[*url.URL, string]
Port __lens.Lens[*url.URL, string]
// optional fields
SchemeO __lens_option.LensO[*url.URL, string]
OpaqueO __lens_option.LensO[*url.URL, string]
@@ -159,6 +166,8 @@ type URLRefLenses struct {
RawQueryO __lens_option.LensO[*url.URL, string]
FragmentO __lens_option.LensO[*url.URL, string]
RawFragmentO __lens_option.LensO[*url.URL, string]
HostnameO __lens_option.LensO[*url.URL, string]
PortO __lens_option.LensO[*url.URL, string]
}
// MakeURLLenses creates a new URLLenses with lenses for all fields
@@ -219,6 +228,38 @@ func MakeURLLenses() URLLenses {
func(s url.URL, v string) url.URL { s.RawFragment = v; return s },
"URL.RawFragment",
)
lensHostname := __lens.MakeLensWithName(
func(s url.URL) string {
host, _, err := net.SplitHostPort(s.Host)
if err != nil {
return s.Host
}
return host
},
func(s url.URL, v string) url.URL {
_, port, err := net.SplitHostPort(s.Host)
if err != nil {
s.Host = v
} else {
s.Host = net.JoinHostPort(v, port)
}
return s
},
"URL.Hostname",
)
lensPort := __lens.MakeLensWithName(
func(s url.URL) string { return s.Port() },
func(s url.URL, v string) url.URL {
host, _, err := net.SplitHostPort(s.Host)
if err != nil {
s.Host = net.JoinHostPort(s.Host, v)
} else {
s.Host = net.JoinHostPort(host, v)
}
return s
},
"URL.Port",
)
// optional lenses
lensSchemeO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensScheme)
lensOpaqueO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensOpaque)
@@ -231,6 +272,8 @@ func MakeURLLenses() URLLenses {
lensRawQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawQuery)
lensFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensFragment)
lensRawFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawFragment)
lensHostnameO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensHostname)
lensPortO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensPort)
return URLLenses{
// mandatory lenses
Scheme: lensScheme,
@@ -244,6 +287,8 @@ func MakeURLLenses() URLLenses {
RawQuery: lensRawQuery,
Fragment: lensFragment,
RawFragment: lensRawFragment,
Hostname: lensHostname,
Port: lensPort,
// optional lenses
SchemeO: lensSchemeO,
OpaqueO: lensOpaqueO,
@@ -256,6 +301,8 @@ func MakeURLLenses() URLLenses {
RawQueryO: lensRawQueryO,
FragmentO: lensFragmentO,
RawFragmentO: lensRawFragmentO,
HostnameO: lensHostnameO,
PortO: lensPortO,
}
}
@@ -317,6 +364,38 @@ func MakeURLRefLenses() URLRefLenses {
func(s *url.URL, v string) *url.URL { s.RawFragment = v; return s },
"(*url.URL).RawFragment",
)
lensHostname := __lens.MakeLensStrictWithName(
func(s *url.URL) string {
host, _, err := net.SplitHostPort(s.Host)
if err != nil {
return s.Host
}
return host
},
func(s *url.URL, v string) *url.URL {
_, port, err := net.SplitHostPort(s.Host)
if err != nil {
s.Host = v
} else {
s.Host = net.JoinHostPort(v, port)
}
return s
},
"URL.Hostname",
)
lensPort := __lens.MakeLensStrictWithName(
(*url.URL).Port,
func(s *url.URL, v string) *url.URL {
host, _, err := net.SplitHostPort(s.Host)
if err != nil {
s.Host = net.JoinHostPort(s.Host, v)
} else {
s.Host = net.JoinHostPort(host, v)
}
return s
},
"URL.Port",
)
// optional lenses
lensSchemeO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensScheme)
lensOpaqueO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensOpaque)
@@ -329,6 +408,8 @@ func MakeURLRefLenses() URLRefLenses {
lensRawQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawQuery)
lensFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensFragment)
lensRawFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawFragment)
lensHostnameO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensHostname)
lensPortO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensPort)
return URLRefLenses{
// mandatory lenses
Scheme: lensScheme,
@@ -342,6 +423,8 @@ func MakeURLRefLenses() URLRefLenses {
RawQuery: lensRawQuery,
Fragment: lensFragment,
RawFragment: lensRawFragment,
Hostname: lensHostname,
Port: lensPort,
// optional lenses
SchemeO: lensSchemeO,
OpaqueO: lensOpaqueO,
@@ -354,6 +437,8 @@ func MakeURLRefLenses() URLRefLenses {
RawQueryO: lensRawQueryO,
FragmentO: lensFragmentO,
RawFragmentO: lensRawFragmentO,
HostnameO: lensHostnameO,
PortO: lensPortO,
}
}

File diff suppressed because it is too large Load Diff