mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
fix: add result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
"Bash(go test:*)",
|
||||
"Bash(go tool cover:*)",
|
||||
"Bash(sort:*)",
|
||||
"Bash(timeout 30 go test:*)"
|
||||
"Bash(timeout 30 go test:*)",
|
||||
"Bash(cut:*)",
|
||||
"Bash(go build:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -203,7 +204,7 @@ func TestLetL(t *testing.T) {
|
||||
)
|
||||
|
||||
t.Run("LetL with pure transformation", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
double := N.Mul(2)
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 21}),
|
||||
@@ -215,7 +216,7 @@ func TestLetL(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("LetL with Left input", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
double := N.Mul(2)
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[Counter](assert.AnError),
|
||||
@@ -227,8 +228,8 @@ func TestLetL(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("LetL with multiple transformations", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
addTen := func(v int) int { return v + 10 }
|
||||
double := N.Mul(2)
|
||||
addTen := N.Add(10)
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 5}),
|
||||
@@ -241,7 +242,7 @@ func TestLetL(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("LetL with identity transformation", func(t *testing.T) {
|
||||
identity := func(v int) int { return v }
|
||||
identity := F.Identity[int]
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 42}),
|
||||
@@ -315,7 +316,7 @@ func TestLensOperationsCombined(t *testing.T) {
|
||||
)
|
||||
|
||||
t.Run("Combine LetToL and LetL", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
double := N.Mul(2)
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 100}),
|
||||
@@ -328,7 +329,7 @@ func TestLensOperationsCombined(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Combine LetL and BindL", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
double := N.Mul(2)
|
||||
validate := func(v int) Either[error, int] {
|
||||
if v > 100 {
|
||||
return Left[int](assert.AnError)
|
||||
|
||||
6
v2/idiomatic/result/applicative.go
Normal file
6
v2/idiomatic/result/applicative.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package result
|
||||
|
||||
type Applicative[A, B any] interface {
|
||||
Apply[A, B]
|
||||
Pointed[A]
|
||||
}
|
||||
50
v2/idiomatic/result/apply._go
Normal file
50
v2/idiomatic/result/apply._go
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// ApplySemigroup lifts a Semigroup over the Right values of Either.
|
||||
// Combines two Right values using the provided Semigroup.
|
||||
// If either value is Left, returns the first Left encountered.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intAdd := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
// eitherSemi := either.ApplySemigroup[error](intAdd)
|
||||
// result := eitherSemi.Concat(either.Right[error](2), either.Right[error](3)) // Right(5)
|
||||
//
|
||||
//go:inline
|
||||
func ApplySemigroup[A any](s S.Semigroup[A]) S.Semigroup[Either[A]] {
|
||||
return S.ApplySemigroup(MonadMap[A, func(A) A], MonadAp[A, E, A], s)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid returns a Monoid that concatenates Either instances via their applicative.
|
||||
// Provides an empty Either (Right with monoid's empty value) and combines Right values using the monoid.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intAddMonoid := monoid.MakeMonoid(0, func(a, b int) int { return a + b })
|
||||
// eitherMon := either.ApplicativeMonoid[error](intAddMonoid)
|
||||
// empty := eitherMon.Empty() // Right(0)
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[A any](m M.Monoid[A]) M.Monoid[Either[A]] {
|
||||
return M.ApplicativeMonoid(Of[A], MonadMap[A, func(A) A], MonadAp[A, E, A], m)
|
||||
}
|
||||
6
v2/idiomatic/result/apply.go
Normal file
6
v2/idiomatic/result/apply.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package result
|
||||
|
||||
type Apply[A, B any] interface {
|
||||
Functor[A, B]
|
||||
Ap(A, error) Operator[func(A) B, B]
|
||||
}
|
||||
63
v2/idiomatic/result/apply_test._go
Normal file
63
v2/idiomatic/result/apply_test._go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
M "github.com/IBM/fp-go/v2/monoid/testing"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplySemigroup(t *testing.T) {
|
||||
|
||||
sg := ApplySemigroup[string](N.SemigroupSum[int]())
|
||||
|
||||
la := Left[int]("a")
|
||||
lb := Left[int]("b")
|
||||
r1 := Right[string](1)
|
||||
r2 := Right[string](2)
|
||||
r3 := Right[string](3)
|
||||
|
||||
assert.Equal(t, la, sg.Concat(la, lb))
|
||||
assert.Equal(t, lb, sg.Concat(r1, lb))
|
||||
assert.Equal(t, la, sg.Concat(la, r2))
|
||||
assert.Equal(t, lb, sg.Concat(r1, lb))
|
||||
assert.Equal(t, r3, sg.Concat(r1, r2))
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
|
||||
m := ApplicativeMonoid[string](N.MonoidSum[int]())
|
||||
|
||||
la := Left[int]("a")
|
||||
lb := Left[int]("b")
|
||||
r1 := Right[string](1)
|
||||
r2 := Right[string](2)
|
||||
r3 := Right[string](3)
|
||||
|
||||
assert.Equal(t, la, m.Concat(la, m.Empty()))
|
||||
assert.Equal(t, lb, m.Concat(m.Empty(), lb))
|
||||
assert.Equal(t, r1, m.Concat(r1, m.Empty()))
|
||||
assert.Equal(t, r2, m.Concat(m.Empty(), r2))
|
||||
assert.Equal(t, r3, m.Concat(r1, r2))
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidLaws(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](N.MonoidSum[int]())
|
||||
M.AssertLaws(t, m)([]Either[string, int]{Left[int]("a"), Right[string](1)})
|
||||
}
|
||||
184
v2/idiomatic/result/array.go
Normal file
184
v2/idiomatic/result/array.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// 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 result
|
||||
|
||||
// TraverseArrayG transforms an array by applying a function that returns a Result (value, error) to each element.
|
||||
// It processes elements from left to right, applying the function to each.
|
||||
// If any element produces an error, the entire operation short-circuits and returns that error.
|
||||
// Otherwise, it returns a successful result containing the array of all transformed values.
|
||||
//
|
||||
// The G suffix indicates support for generic slice types (e.g., custom slice types based on []T).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Source slice type (must be based on []A)
|
||||
// - GB: Target slice type (must be based on []B)
|
||||
// - A: Source element type
|
||||
// - B: Target element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow (A) -> (B, error) that transforms each element
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow (GA) -> (GB, error) that transforms the entire array
|
||||
//
|
||||
// Behavior:
|
||||
// - Short-circuits on the first error encountered
|
||||
// - Preserves the order of elements
|
||||
// - Returns an empty slice for empty input
|
||||
//
|
||||
// Example - Parse strings to integers:
|
||||
//
|
||||
// parse := func(s string) (int, error) {
|
||||
// return strconv.Atoi(s)
|
||||
// }
|
||||
// result := result.TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"})
|
||||
// // result is ([]int{1, 2, 3}, nil)
|
||||
//
|
||||
// Example - Short-circuit on error:
|
||||
//
|
||||
// result := result.TraverseArrayG[[]string, []int](parse)([]string{"1", "bad", "3"})
|
||||
// // result is ([]int(nil), error) - stops at "bad"
|
||||
func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
|
||||
return func(ga GA) (GB, error) {
|
||||
bs := make(GB, len(ga))
|
||||
for i, a := range ga {
|
||||
b, err := f(a)
|
||||
if err != nil {
|
||||
return Left[GB](err)
|
||||
}
|
||||
bs[i] = b
|
||||
}
|
||||
return Of(bs)
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseArray transforms an array by applying a function that returns a Result (value, error) to each element.
|
||||
// It processes elements from left to right, applying the function to each.
|
||||
// If any element produces an error, the entire operation short-circuits and returns that error.
|
||||
// Otherwise, it returns a successful result containing the array of all transformed values.
|
||||
//
|
||||
// This is a convenience wrapper around [TraverseArrayG] for standard slice types.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: Source element type
|
||||
// - B: Target element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow (A) -> (B, error) that transforms each element
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow ([]A) -> ([]B, error) that transforms the entire array
|
||||
//
|
||||
// Example - Validate and transform:
|
||||
//
|
||||
// validate := func(s string) (int, error) {
|
||||
// n, err := strconv.Atoi(s)
|
||||
// if err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
// if n < 0 {
|
||||
// return 0, errors.New("negative number")
|
||||
// }
|
||||
// return n * 2, nil
|
||||
// }
|
||||
// result := result.TraverseArray(validate)([]string{"1", "2", "3"})
|
||||
// // result is ([]int{2, 4, 6}, nil)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return TraverseArrayG[[]A, []B](f)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexG transforms an array by applying an indexed function that returns a Result (value, error).
|
||||
// The function receives both the zero-based index and the element for each iteration.
|
||||
// If any element produces an error, the entire operation short-circuits and returns that error.
|
||||
// Otherwise, it returns a successful result containing the array of all transformed values.
|
||||
//
|
||||
// The G suffix indicates support for generic slice types (e.g., custom slice types based on []T).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Source slice type (must be based on []A)
|
||||
// - GB: Target slice type (must be based on []B)
|
||||
// - A: Source element type
|
||||
// - B: Target element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An indexed function (int, A) -> (B, error) that transforms each element
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow (GA) -> (GB, error) that transforms the entire array
|
||||
//
|
||||
// Behavior:
|
||||
// - Processes elements from left to right with their indices (0, 1, 2, ...)
|
||||
// - Short-circuits on the first error encountered
|
||||
// - Preserves the order of elements
|
||||
//
|
||||
// Example - Annotate with index:
|
||||
//
|
||||
// annotate := func(i int, s string) (string, error) {
|
||||
// if len(s) == 0 {
|
||||
// return "", fmt.Errorf("empty string at index %d", i)
|
||||
// }
|
||||
// return fmt.Sprintf("[%d]=%s", i, s), nil
|
||||
// }
|
||||
// result := result.TraverseArrayWithIndexG[[]string, []string](annotate)([]string{"a", "b", "c"})
|
||||
// // result is ([]string{"[0]=a", "[1]=b", "[2]=c"}, nil)
|
||||
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) (B, error)) Kleisli[GA, GB] {
|
||||
return func(ga GA) (GB, error) {
|
||||
bs := make(GB, len(ga))
|
||||
for i, a := range ga {
|
||||
b, err := f(i, a)
|
||||
if err != nil {
|
||||
return Left[GB](err)
|
||||
}
|
||||
bs[i] = b
|
||||
}
|
||||
return Of(bs)
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex transforms an array by applying an indexed function that returns a Result (value, error).
|
||||
// The function receives both the zero-based index and the element for each iteration.
|
||||
// If any element produces an error, the entire operation short-circuits and returns that error.
|
||||
// Otherwise, it returns a successful result containing the array of all transformed values.
|
||||
//
|
||||
// This is a convenience wrapper around [TraverseArrayWithIndexG] for standard slice types.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: Source element type
|
||||
// - B: Target element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An indexed function (int, A) -> (B, error) that transforms each element
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow ([]A) -> ([]B, error) that transforms the entire array
|
||||
//
|
||||
// Example - Validate with position info:
|
||||
//
|
||||
// check := func(i int, s string) (string, error) {
|
||||
// if len(s) == 0 {
|
||||
// return "", fmt.Errorf("empty value at position %d", i)
|
||||
// }
|
||||
// return strings.ToUpper(s), nil
|
||||
// }
|
||||
// result := result.TraverseArrayWithIndex(check)([]string{"a", "b", "c"})
|
||||
// // result is ([]string{"A", "B", "C"}, nil)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) (B, error)) Kleisli[[]A, []B] {
|
||||
return TraverseArrayWithIndexG[[]A, []B](f)
|
||||
}
|
||||
419
v2/idiomatic/result/array_test.go
Normal file
419
v2/idiomatic/result/array_test.go
Normal file
@@ -0,0 +1,419 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestTraverseArrayG_Success tests successful traversal of an array with all valid elements
|
||||
func TestTraverseArrayG_Success(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
result, err := TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayG_Error tests that traversal short-circuits on first error
|
||||
func TestTraverseArrayG_Error(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
result, err := TraverseArrayG[[]string, []int](parse)([]string{"1", "bad", "3"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayG_EmptyArray tests traversal of an empty array
|
||||
func TestTraverseArrayG_EmptyArray(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
result, err := TraverseArrayG[[]string, []int](parse)([]string{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
assert.NotNil(t, result) // Should be an empty slice, not nil
|
||||
}
|
||||
|
||||
// TestTraverseArrayG_SingleElement tests traversal with a single element
|
||||
func TestTraverseArrayG_SingleElement(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
result, err := TraverseArrayG[[]string, []int](parse)([]string{"42"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayG_ShortCircuit tests that processing stops at first error
|
||||
func TestTraverseArrayG_ShortCircuit(t *testing.T) {
|
||||
callCount := 0
|
||||
parse := func(s string) (int, error) {
|
||||
callCount++
|
||||
if s == "error" {
|
||||
return 0, errors.New("parse error")
|
||||
}
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
_, err := TraverseArrayG[[]string, []int](parse)([]string{"ok", "error", "should-not-process"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, 2, callCount, "should stop after encountering error")
|
||||
}
|
||||
|
||||
// TestTraverseArrayG_CustomSliceType tests with custom slice types
|
||||
func TestTraverseArrayG_CustomSliceType(t *testing.T) {
|
||||
type StringSlice []string
|
||||
type IntSlice []int
|
||||
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
input := StringSlice{"1", "2", "3"}
|
||||
result, err := TraverseArrayG[StringSlice, IntSlice](parse)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, IntSlice{1, 2, 3}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArray_Success tests successful traversal
|
||||
func TestTraverseArray_Success(t *testing.T) {
|
||||
validate := func(s string) (int, error) {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n < 0 {
|
||||
return 0, errors.New("negative number")
|
||||
}
|
||||
return n * 2, nil
|
||||
}
|
||||
|
||||
result, err := TraverseArray(validate)([]string{"1", "2", "3"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArray_ValidationError tests validation failure
|
||||
func TestTraverseArray_ValidationError(t *testing.T) {
|
||||
validate := func(s string) (int, error) {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n < 0 {
|
||||
return 0, errors.New("negative number")
|
||||
}
|
||||
return n * 2, nil
|
||||
}
|
||||
|
||||
result, err := TraverseArray(validate)([]string{"1", "-5", "3"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "negative number")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArray_ParseError tests parse failure
|
||||
func TestTraverseArray_ParseError(t *testing.T) {
|
||||
validate := func(s string) (int, error) {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
result, err := TraverseArray(validate)([]string{"1", "not-a-number", "3"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArray_EmptyArray tests empty array
|
||||
func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
identity := func(n int) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
result, err := TraverseArray(identity)([]int{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArray_DifferentTypes tests transformation between different types
|
||||
func TestTraverseArray_DifferentTypes(t *testing.T) {
|
||||
toLength := func(s string) (int, error) {
|
||||
if len(s) == 0 {
|
||||
return 0, errors.New("empty string")
|
||||
}
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
result, err := TraverseArray(toLength)([]string{"a", "bb", "ccc"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndexG_Success tests successful indexed traversal
|
||||
func TestTraverseArrayWithIndexG_Success(t *testing.T) {
|
||||
annotate := func(i int, s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
return "", fmt.Errorf("empty string at index %d", i)
|
||||
}
|
||||
return fmt.Sprintf("[%d]=%s", i, s), nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndexG[[]string, []string](annotate)([]string{"a", "b", "c"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"[0]=a", "[1]=b", "[2]=c"}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndexG_Error tests error handling with index
|
||||
func TestTraverseArrayWithIndexG_Error(t *testing.T) {
|
||||
annotate := func(i int, s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
return "", fmt.Errorf("empty string at index %d", i)
|
||||
}
|
||||
return fmt.Sprintf("[%d]=%s", i, s), nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndexG[[]string, []string](annotate)([]string{"a", "", "c"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "index 1")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndexG_EmptyArray tests empty array
|
||||
func TestTraverseArrayWithIndexG_EmptyArray(t *testing.T) {
|
||||
annotate := func(i int, s string) (string, error) {
|
||||
return fmt.Sprintf("%d:%s", i, s), nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndexG[[]string, []string](annotate)([]string{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndexG_IndexValidation tests that indices are correct
|
||||
func TestTraverseArrayWithIndexG_IndexValidation(t *testing.T) {
|
||||
indices := []int{}
|
||||
collect := func(i int, s string) (string, error) {
|
||||
indices = append(indices, i)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
_, err := TraverseArrayWithIndexG[[]string, []string](collect)([]string{"a", "b", "c", "d"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, indices)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndexG_ShortCircuit tests short-circuit behavior
|
||||
func TestTraverseArrayWithIndexG_ShortCircuit(t *testing.T) {
|
||||
maxIndex := -1
|
||||
process := func(i int, s string) (string, error) {
|
||||
maxIndex = i
|
||||
if i == 2 {
|
||||
return "", errors.New("stop at index 2")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
_, err := TraverseArrayWithIndexG[[]string, []string](process)([]string{"a", "b", "c", "d", "e"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, 2, maxIndex, "should stop at index 2")
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndexG_CustomSliceType tests with custom slice types
|
||||
func TestTraverseArrayWithIndexG_CustomSliceType(t *testing.T) {
|
||||
type StringSlice []string
|
||||
type ResultSlice []string
|
||||
|
||||
annotate := func(i int, s string) (string, error) {
|
||||
return fmt.Sprintf("%d:%s", i, s), nil
|
||||
}
|
||||
|
||||
input := StringSlice{"x", "y", "z"}
|
||||
result, err := TraverseArrayWithIndexG[StringSlice, ResultSlice](annotate)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ResultSlice{"0:x", "1:y", "2:z"}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndex_Success tests successful indexed traversal
|
||||
func TestTraverseArrayWithIndex_Success(t *testing.T) {
|
||||
check := func(i int, s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
return "", fmt.Errorf("empty value at position %d", i)
|
||||
}
|
||||
return fmt.Sprintf("%d_%s", i, s), nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndex(check)([]string{"a", "b", "c"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"0_a", "1_b", "2_c"}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndex_Error tests error with position info
|
||||
func TestTraverseArrayWithIndex_Error(t *testing.T) {
|
||||
check := func(i int, s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
return "", fmt.Errorf("empty value at position %d", i)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndex(check)([]string{"ok", "ok", "", "ok"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "position 2")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndex_TypeTransformation tests transforming types with index
|
||||
func TestTraverseArrayWithIndex_TypeTransformation(t *testing.T) {
|
||||
multiply := func(i int, n int) (int, error) {
|
||||
return n * (i + 1), nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndex(multiply)([]int{10, 20, 30})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{10, 40, 90}, result) // [10*(0+1), 20*(1+1), 30*(2+1)]
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndex_EmptyArray tests empty array
|
||||
func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
|
||||
process := func(i int, s string) (int, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndex(process)([]string{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndex_SingleElement tests single element processing
|
||||
func TestTraverseArrayWithIndex_SingleElement(t *testing.T) {
|
||||
annotate := func(i int, s string) (string, error) {
|
||||
return fmt.Sprintf("item_%d:%s", i, s), nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndex(annotate)([]string{"solo"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"item_0:solo"}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndex_ConditionalLogic tests using index for conditional logic
|
||||
func TestTraverseArrayWithIndex_ConditionalLogic(t *testing.T) {
|
||||
// Only accept even indices
|
||||
process := func(i int, s string) (string, error) {
|
||||
if i%2 != 0 {
|
||||
return "", fmt.Errorf("odd index %d not allowed", i)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayWithIndex(process)([]string{"ok", "fail"})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "odd index 1")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseArray_LargeArray tests traversal with a larger array
|
||||
func TestTraverseArray_LargeArray(t *testing.T) {
|
||||
// Create array with 1000 elements
|
||||
input := make([]int, 1000)
|
||||
for i := range input {
|
||||
input[i] = i
|
||||
}
|
||||
|
||||
double := func(n int) (int, error) {
|
||||
return n * 2, nil
|
||||
}
|
||||
|
||||
result, err := TraverseArray(double)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, 1000)
|
||||
assert.Equal(t, 0, result[0])
|
||||
assert.Equal(t, 1998, result[999])
|
||||
}
|
||||
|
||||
// TestTraverseArrayG_PreservesOrder tests that order is preserved
|
||||
func TestTraverseArrayG_PreservesOrder(t *testing.T) {
|
||||
reverse := func(s string) (string, error) {
|
||||
runes := []rune(s)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
return string(runes), nil
|
||||
}
|
||||
|
||||
result, err := TraverseArrayG[[]string, []string](reverse)([]string{"abc", "def", "ghi"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"cba", "fed", "ihg"}, result)
|
||||
}
|
||||
|
||||
// TestTraverseArrayWithIndex_BoundaryCheck tests boundary conditions with index
|
||||
func TestTraverseArrayWithIndex_BoundaryCheck(t *testing.T) {
|
||||
// Reject if index exceeds a threshold
|
||||
limitedProcess := func(i int, s string) (string, error) {
|
||||
if i >= 100 {
|
||||
return "", errors.New("index too large")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Should succeed with index < 100
|
||||
result, err := TraverseArrayWithIndex(limitedProcess)([]string{"a", "b", "c"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
}
|
||||
18
v2/idiomatic/result/assert_test.go
Normal file
18
v2/idiomatic/result/assert_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package result
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func AssertEq[A any](l A, lerr error) func(A, error) func(*testing.T) {
|
||||
return func(r A, rerr error) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
assert.Equal(t, lerr, rerr)
|
||||
if (lerr != nil) && (rerr != nil) {
|
||||
assert.Equal(t, l, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
384
v2/idiomatic/result/bind.go
Normal file
384
v2/idiomatic/result/bind.go
Normal file
@@ -0,0 +1,384 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type S to be used with the Bind operation.
|
||||
// This is the starting point for do-notation style computations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x, y int }
|
||||
// result := either.Do[error](State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) (S, error) {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context S1 to produce a context S2.
|
||||
// This enables building up complex computations in a pipeline.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// result := F.Pipe2(
|
||||
// either.Do[error](State{}),
|
||||
// either.Bind(
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { return State{value: v} }
|
||||
// },
|
||||
// func(s State) either.Either[error, int] {
|
||||
// return either.Right[error](42)
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return func(s S1, err error) (S2, error) {
|
||||
if err != nil {
|
||||
return Left[S2](err)
|
||||
}
|
||||
t, err := f(s)
|
||||
if err != nil {
|
||||
return Left[S2](err)
|
||||
}
|
||||
return Of(setter(t)(s))
|
||||
}
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Similar to Bind but for pure (non-Either) computations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](State{value: 10}),
|
||||
// either.Let(
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { return State{value: s.value + v} }
|
||||
// },
|
||||
// func(s State) int { return 32 },
|
||||
// ),
|
||||
// ) // Right(State{value: 42})
|
||||
func Let[S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[S1, S2] {
|
||||
return func(s S1, err error) (S2, error) {
|
||||
if err != nil {
|
||||
return Left[S2](err)
|
||||
}
|
||||
return Of(key(f(s))(s))
|
||||
}
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { name string }
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](State{}),
|
||||
// either.LetTo(
|
||||
// func(n string) func(State) State {
|
||||
// return func(s State) State { return State{name: n} }
|
||||
// },
|
||||
// "Alice",
|
||||
// ),
|
||||
// ) // Right(State{name: "Alice"})
|
||||
func LetTo[S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
t T,
|
||||
) Operator[S1, S2] {
|
||||
return func(s S1, err error) (S2, error) {
|
||||
if err != nil {
|
||||
return Left[S2](err)
|
||||
}
|
||||
return Of(key(t)(s))
|
||||
}
|
||||
}
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically used to start a bind chain.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](42),
|
||||
// either.BindTo(func(v int) State { return State{value: v} }),
|
||||
// ) // Right(State{value: 42})
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
return func(t T, err error) (S1, error) {
|
||||
if err != nil {
|
||||
return Left[S1](err)
|
||||
}
|
||||
return Of(setter(t))
|
||||
}
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
|
||||
// Uses applicative semantics rather than monadic sequencing.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x, y int }
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](State{x: 10}),
|
||||
// either.ApS(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { return State{x: s.x, y: y} }
|
||||
// },
|
||||
// either.Right[error](32),
|
||||
// ),
|
||||
// ) // Right(State{x: 10, y: 32})
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
) func(T, error) Operator[S1, S2] {
|
||||
return func(t T, terr error) Operator[S1, S2] {
|
||||
return func(s S1, serr error) (S2, error) {
|
||||
if terr != nil {
|
||||
return Left[S2](terr)
|
||||
}
|
||||
if serr != nil {
|
||||
return Left[S2](serr)
|
||||
}
|
||||
return Of(setter(t)(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions and enables working with
|
||||
// nested fields in a type-safe manner.
|
||||
//
|
||||
// Unlike BindL, ApSL uses applicative semantics, meaning the computation fa is independent
|
||||
// of the current state and can be evaluated concurrently.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - fa: An Either[T] computation that produces the value to set
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field in the Either context
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, a int) Person { p.Age = a; return p },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](Person{Name: "Alice", Age: 25}),
|
||||
// either.ApSL(ageLens, either.Right[error](30)),
|
||||
// ) // Right(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens Lens[S, T],
|
||||
) func(T, error) Operator[S, S] {
|
||||
return ApS(lens.Set)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an Either that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - f: A function that takes the current field value and returns an Either[T]
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field based on its current value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) either.Either[error, int] {
|
||||
// if v >= 100 {
|
||||
// return either.Left[int](errors.New("counter overflow"))
|
||||
// }
|
||||
// return either.Right[error](v + 1)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Counter{Value: 42}),
|
||||
// either.BindL(valueLens, increment),
|
||||
// ) // Right(Counter{Value: 43})
|
||||
func BindL[S, T any](
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return func(s S, serr error) (S, error) {
|
||||
t, terr := f(lens.Get(s))
|
||||
if terr != nil {
|
||||
return Left[S](terr)
|
||||
}
|
||||
if serr != nil {
|
||||
return Left[S](serr)
|
||||
}
|
||||
return Of(lens.Set(t)(s))
|
||||
}
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Either).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - f: An endomorphism (T → T) that transforms the current field value
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field with the transformed value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Counter{Value: 21}),
|
||||
// either.LetL(valueLens, double),
|
||||
// ) // Right(Counter{Value: 42})
|
||||
func LetL[S, T any](
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[S, S] {
|
||||
mod := L.Modify[S](f)(lens)
|
||||
return func(s S, err error) (S, error) {
|
||||
if err != nil {
|
||||
return Left[S](err)
|
||||
}
|
||||
return Of(mod(s))
|
||||
}
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - b: The constant value to set the field to
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that sets the focused field to the constant value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Config{Debug: true, Timeout: 30}),
|
||||
// either.LetToL(debugLens, false),
|
||||
// ) // Right(Config{Debug: false, Timeout: 30})
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return LetTo(lens.Set, b)
|
||||
}
|
||||
363
v2/idiomatic/result/bind_test.go
Normal file
363
v2/idiomatic/result/bind_test.go
Normal file
@@ -0,0 +1,363 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getLastName(s utils.Initial) (string, error) {
|
||||
return Of("Doe")
|
||||
}
|
||||
|
||||
func getGivenName(s utils.WithLastName) (string, error) {
|
||||
return Of("John")
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
|
||||
res, err := Pipe4(
|
||||
utils.Empty,
|
||||
Do,
|
||||
Bind(utils.SetLastName, getLastName),
|
||||
Bind(utils.SetGivenName, getGivenName),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
AssertEq(Of("John Doe"))(res, err)(t)
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
|
||||
res, err := Pipe4(
|
||||
utils.Empty,
|
||||
Do,
|
||||
ApS(utils.SetLastName)(Of("Doe")),
|
||||
ApS(utils.SetGivenName)(Of("John")),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
AssertEq(Of("John Doe"))(res, err)(t)
|
||||
}
|
||||
|
||||
// Test types for lens-based operations
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
// Create a lens for the Age field
|
||||
ageLens := L.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, a int) Person { p.Age = a; return p },
|
||||
)
|
||||
|
||||
t.Run("ApSL with Right value", func(t *testing.T) {
|
||||
result, err := Pipe2(
|
||||
Person{Name: "Alice", Age: 25},
|
||||
Right,
|
||||
ApSL(ageLens)(Right(30)),
|
||||
)
|
||||
|
||||
AssertEq(Right(Person{Name: "Alice", Age: 30}))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("ApSL with Left in context", func(t *testing.T) {
|
||||
result, err := Pipe2(
|
||||
assert.AnError,
|
||||
Left[Person],
|
||||
ApSL(ageLens)(Right(30)),
|
||||
)
|
||||
|
||||
AssertEq(Left[Person](assert.AnError))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("ApSL with Left in value", func(t *testing.T) {
|
||||
result, err := Pipe2(
|
||||
Person{Name: "Alice", Age: 25},
|
||||
Right,
|
||||
ApSL(ageLens)(Left[int](assert.AnError)),
|
||||
)
|
||||
|
||||
AssertEq(Left[Person](assert.AnError))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("ApSL with both Left", func(t *testing.T) {
|
||||
result, err := Pipe2(
|
||||
assert.AnError,
|
||||
Left[Person],
|
||||
ApSL(ageLens)(Left[int](assert.AnError)),
|
||||
)
|
||||
|
||||
AssertEq(Left[Person](assert.AnError))(result, err)(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
// Create a lens for the Value field
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("BindL with successful transformation", func(t *testing.T) {
|
||||
// Increment the counter, but fail if it would exceed 100
|
||||
increment := func(v int) (int, error) {
|
||||
if v >= 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right(v + 1)
|
||||
}
|
||||
|
||||
result, err := Pipe2(
|
||||
Counter{Value: 42},
|
||||
Right,
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
AssertEq(Right(Counter{Value: 43}))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("BindL with failing transformation", func(t *testing.T) {
|
||||
increment := func(v int) (int, error) {
|
||||
if v >= 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right(v + 1)
|
||||
}
|
||||
|
||||
result, err := Pipe2(
|
||||
Counter{Value: 100},
|
||||
Right,
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
AssertEq(Left[Counter](assert.AnError))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("BindL with Left input", func(t *testing.T) {
|
||||
increment := func(v int) (int, error) {
|
||||
return Right(v + 1)
|
||||
}
|
||||
|
||||
result, err := Pipe2(
|
||||
assert.AnError,
|
||||
Left[Counter],
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
AssertEq(Left[Counter](assert.AnError))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("BindL with multiple operations", func(t *testing.T) {
|
||||
double := func(v int) (int, error) {
|
||||
return Right(v * 2)
|
||||
}
|
||||
|
||||
addTen := func(v int) (int, error) {
|
||||
return Right(v + 10)
|
||||
}
|
||||
|
||||
result, err := Pipe3(
|
||||
Counter{Value: 5},
|
||||
Right,
|
||||
BindL(valueLens, double),
|
||||
BindL(valueLens, addTen),
|
||||
)
|
||||
|
||||
AssertEq(Right(Counter{Value: 20}))(result, err)(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
// Create a lens for the Value field
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("LetL with pure transformation", func(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
|
||||
result, err := Pipe2(
|
||||
Counter{Value: 21},
|
||||
Right,
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
AssertEq(Right(Counter{Value: 42}))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("LetL with Left input", func(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
|
||||
result, err := Pipe2(
|
||||
assert.AnError,
|
||||
Left[Counter],
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
|
||||
AssertEq(Left[Counter](assert.AnError))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("LetL with multiple transformations", func(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
addTen := N.Add(10)
|
||||
|
||||
result, err := Pipe3(
|
||||
Counter{Value: 5},
|
||||
Right,
|
||||
LetL(valueLens, double),
|
||||
LetL(valueLens, addTen),
|
||||
)
|
||||
|
||||
AssertEq(Right(Counter{Value: 20}))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("LetL with identity transformation", func(t *testing.T) {
|
||||
identity := F.Identity[int]
|
||||
|
||||
result, err := Pipe2(
|
||||
Counter{Value: 42},
|
||||
Right,
|
||||
LetL(valueLens, identity),
|
||||
)
|
||||
|
||||
AssertEq(Right(Counter{Value: 42}))(result, err)(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
// Create a lens for the Debug field
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("LetToL with constant value", func(t *testing.T) {
|
||||
result, err := Pipe2(
|
||||
Config{Debug: true, Timeout: 30},
|
||||
Right,
|
||||
LetToL(debugLens, false),
|
||||
)
|
||||
|
||||
AssertEq(Right(Config{Debug: false, Timeout: 30}))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("LetToL with Left input", func(t *testing.T) {
|
||||
result, err := Pipe2(
|
||||
assert.AnError,
|
||||
Left[Config],
|
||||
LetToL(debugLens, false),
|
||||
)
|
||||
|
||||
AssertEq(Left[Config](assert.AnError))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("LetToL with multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
result, err := Pipe3(
|
||||
Config{Debug: true, Timeout: 30},
|
||||
Right,
|
||||
LetToL(debugLens, false),
|
||||
LetToL(timeoutLens, 60),
|
||||
)
|
||||
|
||||
AssertEq(Right(Config{Debug: false, Timeout: 60}))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("LetToL setting same value", func(t *testing.T) {
|
||||
result, err := Pipe2(
|
||||
Config{Debug: false, Timeout: 30},
|
||||
Right,
|
||||
LetToL(debugLens, false),
|
||||
)
|
||||
|
||||
AssertEq(Right(Config{Debug: false, Timeout: 30}))(result, err)(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLensOperationsCombined(t *testing.T) {
|
||||
// Test combining different lens operations
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("Combine LetToL and LetL", func(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
|
||||
result, err := Pipe3(
|
||||
Counter{Value: 100},
|
||||
Right,
|
||||
LetToL(valueLens, 10),
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
|
||||
AssertEq(Right(Counter{Value: 20}))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("Combine LetL and BindL", func(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
validate := func(v int) (int, error) {
|
||||
if v > 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right(v)
|
||||
}
|
||||
|
||||
result, err := Pipe3(
|
||||
Counter{Value: 25},
|
||||
Right,
|
||||
LetL(valueLens, double),
|
||||
BindL(valueLens, validate),
|
||||
)
|
||||
|
||||
AssertEq(Right(Counter{Value: 50}))(result, err)(t)
|
||||
})
|
||||
|
||||
t.Run("Combine ApSL and LetL", func(t *testing.T) {
|
||||
addFive := func(v int) int { return v + 5 }
|
||||
|
||||
result, err := Pipe3(
|
||||
Counter{Value: 10},
|
||||
Right,
|
||||
ApSL(valueLens)(Right(20)),
|
||||
LetL(valueLens, addFive),
|
||||
)
|
||||
|
||||
AssertEq(Right(Counter{Value: 25}))(result, err)(t)
|
||||
})
|
||||
}
|
||||
6
v2/idiomatic/result/chain.go
Normal file
6
v2/idiomatic/result/chain.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package result
|
||||
|
||||
type Chainable[A, B any] interface {
|
||||
Apply[A, B]
|
||||
Chain(Kleisli[A, B]) Operator[A, B]
|
||||
}
|
||||
80
v2/idiomatic/result/core.go
Normal file
80
v2/idiomatic/result/core.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// String prints some debug info for the object
|
||||
func ToString[A any](a A, err error) string {
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Left(%v)", err)
|
||||
}
|
||||
return fmt.Sprintf("Right[%T](%v)", a, a)
|
||||
}
|
||||
|
||||
// IsLeft tests if the Either is a Left value.
|
||||
// Rather use [Fold] or [MonadFold] if you need to access the values.
|
||||
// Inverse is [IsRight].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// either.IsLeft(either.Left[int](errors.New("err"))) // true
|
||||
// either.IsLeft(either.Right[error](42)) // false
|
||||
//
|
||||
//go:inline
|
||||
func IsLeft[A any](_ A, err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// IsRight tests if the Either is a Right value.
|
||||
// Rather use [Fold] or [MonadFold] if you need to access the values.
|
||||
// Inverse is [IsLeft].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// either.IsRight(either.Right[error](42)) // true
|
||||
// either.IsRight(either.Left[int](errors.New("err"))) // false
|
||||
//
|
||||
//go:inline
|
||||
func IsRight[A any](_ A, err error) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Left creates a new Either representing a Left (error/failure) value.
|
||||
// By convention, Left represents the error case.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.Left[int](errors.New("something went wrong"))
|
||||
//
|
||||
//go:inline
|
||||
func Left[A any](err error) (A, error) {
|
||||
return *new(A), err
|
||||
}
|
||||
|
||||
// Right creates a new Either representing a Right (success) value.
|
||||
// By convention, Right represents the success case.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.Right[error](42)
|
||||
//
|
||||
//go:inline
|
||||
func Right[A any](a A) (A, error) {
|
||||
return a, nil
|
||||
}
|
||||
154
v2/idiomatic/result/core_any.go
Normal file
154
v2/idiomatic/result/core_any.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// 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.
|
||||
//go:build either_any
|
||||
|
||||
package result
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
either struct {
|
||||
value any
|
||||
isRight bool
|
||||
}
|
||||
|
||||
// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases
|
||||
Either[A any] either
|
||||
)
|
||||
|
||||
// String prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func eitherString(s *either) string {
|
||||
if s.isRight {
|
||||
return fmt.Sprintf("Right[%T](%v)", s.value, s.value)
|
||||
}
|
||||
return fmt.Sprintf("Left[%T](%v)", s.value, s.value)
|
||||
}
|
||||
|
||||
// Format prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func eitherFormat(e *either, f fmt.State, c rune) {
|
||||
switch c {
|
||||
case 's':
|
||||
fmt.Fprint(f, eitherString(e))
|
||||
default:
|
||||
fmt.Fprint(f, eitherString(e))
|
||||
}
|
||||
}
|
||||
|
||||
// String prints some debug info for the object
|
||||
func (s Either[A]) String() string {
|
||||
return eitherString((*either)(&s))
|
||||
}
|
||||
|
||||
// Format prints some debug info for the object
|
||||
func (s Either[A]) Format(f fmt.State, c rune) {
|
||||
eitherFormat((*either)(&s), f, c)
|
||||
}
|
||||
|
||||
// IsLeft tests if the Either is a Left value.
|
||||
// Rather use [Fold] or [MonadFold] if you need to access the values.
|
||||
// Inverse is [IsRight].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// either.IsLeft(either.Left[int](errors.New("err"))) // true
|
||||
// either.IsLeft(either.Right[error](42)) // false
|
||||
//
|
||||
//go:inline
|
||||
func IsLeft[A any](val Either[A]) bool {
|
||||
return !val.isRight
|
||||
}
|
||||
|
||||
// IsRight tests if the Either is a Right value.
|
||||
// Rather use [Fold] or [MonadFold] if you need to access the values.
|
||||
// Inverse is [IsLeft].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// either.IsRight(either.Right[error](42)) // true
|
||||
// either.IsRight(either.Left[int](errors.New("err"))) // false
|
||||
//
|
||||
//go:inline
|
||||
func IsRight[A any](val Either[A]) bool {
|
||||
return val.isRight
|
||||
}
|
||||
|
||||
// Left creates a new Either representing a Left (error/failure) value.
|
||||
// By convention, Left represents the error case.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.Left[int](errors.New("something went wrong"))
|
||||
//
|
||||
//go:inline
|
||||
func Left[A, E any](value E) Either[A] {
|
||||
return Either[A]{value, false}
|
||||
}
|
||||
|
||||
// Right creates a new Either representing a Right (success) value.
|
||||
// By convention, Right represents the success case.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.Right[error](42)
|
||||
//
|
||||
//go:inline
|
||||
func Right[A any](value A) Either[A] {
|
||||
return Either[A]{value, true}
|
||||
}
|
||||
|
||||
// MonadFold extracts the value from an Either by providing handlers for both cases.
|
||||
// This is the fundamental pattern matching operation for Either.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.MonadFold(
|
||||
// either.Right[error](42),
|
||||
// func(err error) string { return "Error: " + err.Error() },
|
||||
// func(n int) string { return fmt.Sprintf("Value: %d", n) },
|
||||
// ) // "Value: 42"
|
||||
//
|
||||
//go:inline
|
||||
func MonadFold[A, B any](ma Either[A], onLeft func(e E) B, onRight func(a A) B) B {
|
||||
if ma.isRight {
|
||||
return onRight(ma.value.(A))
|
||||
}
|
||||
return onLeft(ma.value.(E))
|
||||
}
|
||||
|
||||
// Unwrap converts an Either into the idiomatic Go tuple (value, error).
|
||||
// For Right values, returns (value, zero-error).
|
||||
// For Left values, returns (zero-value, error).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// val, err := either.Unwrap(either.Right[error](42)) // 42, nil
|
||||
// val, err := either.Unwrap(either.Left[int](errors.New("fail"))) // 0, error
|
||||
//
|
||||
//go:inline
|
||||
func Unwrap[A any](ma Either[A]) (A, E) {
|
||||
if ma.isRight {
|
||||
var e E
|
||||
return ma.value.(A), e
|
||||
} else {
|
||||
var a A
|
||||
return a, ma.value.(E)
|
||||
}
|
||||
}
|
||||
94
v2/idiomatic/result/core_pointers.go
Normal file
94
v2/idiomatic/result/core_pointers.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// 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.
|
||||
//go:build either_pointers
|
||||
|
||||
package result
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Either[A any] struct {
|
||||
left *E
|
||||
right *A
|
||||
}
|
||||
|
||||
// String prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func eitherString[A any](s *Either[A]) string {
|
||||
if s.right != nil {
|
||||
return fmt.Sprintf("Right[%T](%v)", *s.right, *s.right)
|
||||
}
|
||||
return fmt.Sprintf("Left[%T](%v)", *s.left, *s.left)
|
||||
}
|
||||
|
||||
// Format prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func eitherFormat[A any](e *Either[A], f fmt.State, c rune) {
|
||||
switch c {
|
||||
case 's':
|
||||
fmt.Fprint(f, eitherString(e))
|
||||
default:
|
||||
fmt.Fprint(f, eitherString(e))
|
||||
}
|
||||
}
|
||||
|
||||
// String prints some debug info for the object
|
||||
func (s Either[A]) String() string {
|
||||
return eitherString(&s)
|
||||
}
|
||||
|
||||
// Format prints some debug info for the object
|
||||
func (s Either[A]) Format(f fmt.State, c rune) {
|
||||
eitherFormat(&s, f, c)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Left[A, E any](value E) Either[A] {
|
||||
return Either[A]{left: &value}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Right[A any](value A) Either[A] {
|
||||
return Either[A]{right: &value}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func IsLeft[A any](e Either[A]) bool {
|
||||
return e.left != nil
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func IsRight[A any](e Either[A]) bool {
|
||||
return e.right != nil
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadFold[A, B any](ma Either[A], onLeft func(E) B, onRight func(A) B) B {
|
||||
if ma.right != nil {
|
||||
return onRight(*ma.right)
|
||||
}
|
||||
return onLeft(*ma.left)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Unwrap[A any](ma Either[A]) (A, E) {
|
||||
if ma.right != nil {
|
||||
var e E
|
||||
return *ma.right, e
|
||||
}
|
||||
var a A
|
||||
return a, *ma.left
|
||||
}
|
||||
142
v2/idiomatic/result/coverage.out
Normal file
142
v2/idiomatic/result/coverage.out
Normal file
@@ -0,0 +1,142 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:54.82,55.33 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:55.33,57.24 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:57.24,59.18 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:59.18,61.5 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:62.4,62.13 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:64.3,64.16 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:101.65,103.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:139.101,140.33 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:140.33,142.24 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:142.24,144.18 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:144.18,146.5 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:147.4,147.13 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:149.3,149.16 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/array.go:182.84,184.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/core.go:23.45,24.16 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/core.go:24.16,26.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/core.go:27.2,27.43 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/core.go:40.41,42.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/core.go:54.42,56.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/core.go:66.40,68.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/core.go:78.35,80.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:36.32,38.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:42.61,43.54 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:43.54,44.20 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:44.20,46.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:47.3,47.19 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:47.19,49.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:50.3,50.21 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:56.75,57.41 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:57.41,58.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:58.17,60.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:61.3,61.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:66.42,67.41 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:67.41,69.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:74.48,75.41 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:75.41,76.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:76.17,78.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:79.3,79.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:85.59,86.41 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:86.41,87.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:87.17,89.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:90.3,90.15 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:95.92,96.50 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:96.50,97.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:97.42,98.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:98.18,100.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:101.4,101.25 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:101.25,103.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:104.4,104.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:110.56,111.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:111.17,112.40 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:112.40,114.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:116.2,116.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:116.42,117.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:117.18,119.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:120.3,120.15 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:126.54,127.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:127.42,128.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:128.18,130.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:131.3,131.14 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:136.59,137.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:137.42,138.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:138.18,140.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:141.3,142.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:152.70,153.40 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:153.40,154.11 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:154.11,156.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:157.3,157.15 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:169.49,171.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:183.56,184.30 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:184.30,186.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:195.43,197.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:208.79,209.33 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:209.33,210.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:210.18,212.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:213.3,213.20 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:228.83,229.30 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:229.30,230.14 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:230.14,232.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:233.3,233.29 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:245.51,246.32 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:246.32,247.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:247.17,249.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:250.3,250.15 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:260.62,261.32 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:261.32,262.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:262.17,264.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:265.3,265.11 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:271.67,272.32 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:272.32,273.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:273.17,275.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:276.3,276.23 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:288.56,289.41 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:289.41,290.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:290.17,292.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:293.3,293.15 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:305.61,306.41 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:306.41,307.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:307.17,309.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:310.3,310.15 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:323.61,324.32 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:324.32,325.25 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:325.25,327.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:328.3,328.29 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:333.48,335.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:338.49,339.54 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:339.54,340.20 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:340.20,342.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/either.go:343.3,343.20 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:7.76,9.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:15.92,17.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:23.120,25.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:31.136,32.45 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:32.45,34.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:41.164,43.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:49.180,50.45 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:50.45,52.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:59.208,61.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:67.224,68.45 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:68.45,70.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:77.252,79.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:85.268,86.45 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/function.go:86.45,88.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:35.43,36.51 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:36.51,38.37 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:38.37,39.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:39.18,41.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:42.4,42.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:64.36,66.42 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:66.42,67.17 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:67.17,69.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:70.3,70.21 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:84.81,85.54 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:85.54,86.55 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:86.55,88.19 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:88.19,89.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:89.22,91.6 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:93.5,93.25 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:96.4,96.21 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:96.21,98.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:100.4,100.21 1 0
|
||||
127
v2/idiomatic/result/curry.go
Normal file
127
v2/idiomatic/result/curry.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// 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 result
|
||||
|
||||
// Curry0 converts a Go function that returns (R, error) into a curried version that returns (R, error).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getConfig := func() (string, error) { return "config", nil }
|
||||
// curried := either.Curry0(getConfig)
|
||||
// result := curried() // Right("config")
|
||||
func Curry0[R any](f func() (R, error)) func() (R, error) {
|
||||
return f
|
||||
}
|
||||
|
||||
// Curry1 converts a Go function that returns (R, error) into a curried version that returns (R, error).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parse := func(s string) (int, error) { return strconv.Atoi(s) }
|
||||
// curried := either.Curry1(parse)
|
||||
// result := curried("42") // Right(42)
|
||||
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) (R, error) {
|
||||
return f
|
||||
}
|
||||
|
||||
// Curry2 converts a 2-argument Go function that returns (R, error) into a curried version.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// divide := func(a, b int) (int, error) {
|
||||
// if b == 0 { return 0, errors.New("div by zero") }
|
||||
// return a / b, nil
|
||||
// }
|
||||
// curried := either.Curry2(divide)
|
||||
// result := curried(10)(2) // Right(5)
|
||||
func Curry2[T1, T2, R any](f func(T1, T2) (R, error)) func(T1) func(T2) (R, error) {
|
||||
return func(t1 T1) func(T2) (R, error) {
|
||||
return func(t2 T2) (R, error) {
|
||||
return f(t1, t2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Curry3 converts a 3-argument Go function that returns (R, error) into a curried version.
|
||||
func Curry3[T1, T2, T3, R any](f func(T1, T2, T3) (R, error)) func(T1) func(T2) func(T3) (R, error) {
|
||||
return func(t1 T1) func(T2) func(T3) (R, error) {
|
||||
return func(t2 T2) func(T3) (R, error) {
|
||||
return func(t3 T3) (R, error) {
|
||||
return f(t1, t2, t3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Curry4 converts a 4-argument Go function that returns (R, error) into a curried version.
|
||||
func Curry4[T1, T2, T3, T4, R any](f func(T1, T2, T3, T4) (R, error)) func(T1) func(T2) func(T3) func(T4) (R, error) {
|
||||
return func(t1 T1) func(T2) func(T3) func(T4) (R, error) {
|
||||
return func(t2 T2) func(T3) func(T4) (R, error) {
|
||||
return func(t3 T3) func(T4) (R, error) {
|
||||
return func(t4 T4) (R, error) {
|
||||
return f(t1, t2, t3, t4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry0 converts a function returning (R, error) back to Go's (R, error) style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// curried := func() either.Either[error, string] { return either.Right[error]("value") }
|
||||
// uncurried := either.Uncurry0(curried)
|
||||
// result, err := uncurried() // "value", nil
|
||||
func Uncurry0[R any](f func() (R, error)) func() (R, error) {
|
||||
return func() (R, error) {
|
||||
return f()
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry1 converts a function returning (R, error) back to Go's (R, error) style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// curried := func(x int) either.Either[error, string] { return either.Right[error](strconv.Itoa(x)) }
|
||||
// uncurried := either.Uncurry1(curried)
|
||||
// result, err := uncurried(42) // "42", nil
|
||||
func Uncurry1[T1, R any](f func(T1) (R, error)) func(T1) (R, error) {
|
||||
return func(t1 T1) (R, error) {
|
||||
return f(t1)
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry2 converts a curried function returning (R, error) back to Go's (R, error) style.
|
||||
func Uncurry2[T1, T2, R any](f func(T1) func(T2) (R, error)) func(T1, T2) (R, error) {
|
||||
return func(t1 T1, t2 T2) (R, error) {
|
||||
return f(t1)(t2)
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry3 converts a curried function returning (R, error) back to Go's (R, error) style.
|
||||
func Uncurry3[T1, T2, T3, R any](f func(T1) func(T2) func(T3) (R, error)) func(T1, T2, T3) (R, error) {
|
||||
return func(t1 T1, t2 T2, t3 T3) (R, error) {
|
||||
return f(t1)(t2)(t3)
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry4 converts a curried function returning (R, error) back to Go's (R, error) style.
|
||||
func Uncurry4[T1, T2, T3, T4, R any](f func(T1) func(T2) func(T3) func(T4) (R, error)) func(T1, T2, T3, T4) (R, error) {
|
||||
return func(t1 T1, t2 T2, t3 T3, t4 T4) (R, error) {
|
||||
return f(t1)(t2)(t3)(t4)
|
||||
}
|
||||
}
|
||||
66
v2/idiomatic/result/doc.go
Normal file
66
v2/idiomatic/result/doc.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 result provides an idiomatic Go approach to error handling using the (value, error) tuple pattern.
|
||||
//
|
||||
// This package represents the Result/Either monad idiomatically in Go, leveraging the standard
|
||||
// (T, error) return pattern that Go developers are familiar with. By convention:
|
||||
// - (value, nil) represents a success case (Right)
|
||||
// - (zero, error) represents a failure case (Left)
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// The Result pattern is a functional approach to error handling that makes error flows explicit
|
||||
// and composable. Instead of checking errors manually at each step, you can chain operations
|
||||
// that automatically short-circuit on the first error.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// // Creating Result values
|
||||
// success := result.Right[error](42) // (42, nil)
|
||||
// failure := result.Left[int](errors.New("oops")) // (0, error)
|
||||
//
|
||||
// // Pattern matching with Fold
|
||||
// output := result.Fold(
|
||||
// func(err error) string { return "Error: " + err.Error() },
|
||||
// func(n int) string { return fmt.Sprintf("Success: %d", n) },
|
||||
// )(success)
|
||||
//
|
||||
// // Chaining operations (short-circuits on Left/error)
|
||||
// output := result.Chain(func(n int) (int, error) {
|
||||
// return result.Right[error](n * 2)
|
||||
// })(success)
|
||||
//
|
||||
// # Monadic Operations
|
||||
//
|
||||
// Result implements the Monad interface, providing:
|
||||
// - Map: Transform the Right value
|
||||
// - Chain (FlatMap): Chain computations that may fail
|
||||
// - Ap: Apply a function wrapped in Result
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// Result provides utilities for working with Go's error type:
|
||||
// - FromError: Create Result from error-returning functions
|
||||
// - FromPredicate: Create Result based on a predicate
|
||||
// - ToError: Extract the error from a Result
|
||||
//
|
||||
// # Subpackages
|
||||
//
|
||||
// - result/exec: Execute system commands returning Result
|
||||
// - result/http: HTTP request builders returning Result
|
||||
package result
|
||||
|
||||
//go:generate go run .. either --count 15 --filename gen.go
|
||||
341
v2/idiomatic/result/either.go
Normal file
341
v2/idiomatic/result/either.go
Normal file
@@ -0,0 +1,341 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/option"
|
||||
)
|
||||
|
||||
// Of constructs a Right value containing the given value.
|
||||
// This is the monadic return/pure operation for Either.
|
||||
// Equivalent to [Right].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.Of[error](42) // Right(42)
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](a A) (A, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Ap is the curried version of [MonadAp].
|
||||
// Returns a function that applies a wrapped function to the given wrapped value.
|
||||
func Ap[B, A any](fa A, faerr error) Operator[func(A) B, B] {
|
||||
return func(fab func(A) B, faberr error) (B, error) {
|
||||
if faberr != nil {
|
||||
return Left[B](faberr)
|
||||
}
|
||||
if faerr != nil {
|
||||
return Left[B](faerr)
|
||||
}
|
||||
return Of(fab(fa))
|
||||
}
|
||||
}
|
||||
|
||||
// BiMap is the curried version of [MonadBiMap].
|
||||
// Maps a pair of functions over the two type arguments of the bifunctor.
|
||||
func BiMap[A, B any](f Endomorphism[error], g func(a A) B) Operator[A, B] {
|
||||
return func(a A, err error) (B, error) {
|
||||
if err != nil {
|
||||
return Left[B](f(err))
|
||||
}
|
||||
return Of(g(a))
|
||||
}
|
||||
}
|
||||
|
||||
// MapTo is the curried version of [MonadMapTo].
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return func(_ A, err error) (B, error) {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
|
||||
// Map is the curried version of [MonadMap].
|
||||
// Transforms the Right value using the provided function.
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return func(a A, err error) (B, error) {
|
||||
if err != nil {
|
||||
return Left[B](err)
|
||||
}
|
||||
return Of(f(a))
|
||||
}
|
||||
}
|
||||
|
||||
// MapLeft is the curried version of [MonadMapLeft].
|
||||
// Applies a mapping function to the Left (error) channel.
|
||||
func MapLeft[A any](f Endomorphism[error]) Operator[A, A] {
|
||||
return func(a A, err error) (A, error) {
|
||||
if err != nil {
|
||||
return Left[A](f(err))
|
||||
}
|
||||
return Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainOptionK is the curried version of [MonadChainOptionK].
|
||||
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
return func(f func(A) (B, bool)) Operator[A, B] {
|
||||
return func(a A, err error) (B, error) {
|
||||
if err != nil {
|
||||
return Left[B](err)
|
||||
}
|
||||
if b, ok := f(a); ok {
|
||||
return Of(b)
|
||||
}
|
||||
return Left[B](onNone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ChainTo is the curried version of [MonadChainTo].
|
||||
func ChainTo[A, B any](b B, berr error) Operator[A, B] {
|
||||
if berr != nil {
|
||||
return func(_ A, _ error) (B, error) {
|
||||
return Left[B](berr)
|
||||
}
|
||||
}
|
||||
return func(a A, aerr error) (B, error) {
|
||||
if aerr != nil {
|
||||
return Left[B](aerr)
|
||||
}
|
||||
return Of(b)
|
||||
}
|
||||
}
|
||||
|
||||
// Chain is the curried version of [MonadChain].
|
||||
// Sequences two computations where the second depends on the first.
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return func(a A, aerr error) (B, error) {
|
||||
if aerr != nil {
|
||||
return Left[B](aerr)
|
||||
}
|
||||
return f(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainFirst is the curried version of [MonadChainFirst].
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return func(a A, aerr error) (A, error) {
|
||||
if aerr != nil {
|
||||
return Left[A](aerr)
|
||||
}
|
||||
_, berr := f(a)
|
||||
return a, berr
|
||||
}
|
||||
}
|
||||
|
||||
// FromOption converts an Option to an Either, using the provided function to generate a Left value for None.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// opt := option.Some(42)
|
||||
// result := either.FromOption[int](func() error { return errors.New("none") })(opt) // Right(42)
|
||||
func FromOption[A any](onNone func() error) func(A, bool) (A, error) {
|
||||
return func(a A, aok bool) (A, error) {
|
||||
if !aok {
|
||||
return Left[A](onNone())
|
||||
}
|
||||
return Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ToOption converts an Either to an Option, discarding the Left value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.ToOption(either.Right[error](42)) // Some(42)
|
||||
// result := either.ToOption(either.Left[int](errors.New("err"))) // None
|
||||
//
|
||||
//go:inline
|
||||
func ToOption[A any](a A, aerr error) (A, bool) {
|
||||
return a, aerr == nil
|
||||
}
|
||||
|
||||
// FromError creates an Either from a function that may return an error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validate := func(x int) error {
|
||||
// if x < 0 { return errors.New("negative") }
|
||||
// return nil
|
||||
// }
|
||||
// toEither := either.FromError(validate)
|
||||
// result := toEither(42) // Right(42)
|
||||
func FromError[A any](f func(a A) error) Kleisli[A, A] {
|
||||
return func(a A) (A, error) {
|
||||
return a, f(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ToError converts an Either[error, A] to an error, returning nil for Right values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// err := either.ToError(either.Left[int](errors.New("fail"))) // error
|
||||
// err := either.ToError(either.Right[error](42)) // nil
|
||||
func ToError[A any](_ A, err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fold is the curried version of [MonadFold].
|
||||
// Extracts the value from an Either by providing handlers for both cases.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.Fold(
|
||||
// func(err error) string { return "Error: " + err.Error() },
|
||||
// func(n int) string { return fmt.Sprintf("Value: %d", n) },
|
||||
// )(either.Right[error](42)) // "Value: 42"
|
||||
func Fold[A, B any](onLeft func(error) B, onRight func(A) B) func(A, error) B {
|
||||
return func(a A, aerr error) B {
|
||||
if aerr != nil {
|
||||
return onLeft(aerr)
|
||||
}
|
||||
return onRight(a)
|
||||
}
|
||||
}
|
||||
|
||||
// FromPredicate creates an Either based on a predicate.
|
||||
// If the predicate returns true, creates a Right; otherwise creates a Left using onFalse.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := either.FromPredicate(
|
||||
// func(x int) bool { return x > 0 },
|
||||
// func(x int) error { return errors.New("not positive") },
|
||||
// )
|
||||
// result := isPositive(42) // Right(42)
|
||||
// result := isPositive(-1) // Left(error)
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
|
||||
return func(a A) (A, error) {
|
||||
if pred(a) {
|
||||
return Right(a)
|
||||
}
|
||||
return Left[A](onFalse(a))
|
||||
}
|
||||
}
|
||||
|
||||
// FromNillable creates an Either from a pointer, using the provided error for nil pointers.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var ptr *int = nil
|
||||
// result := either.FromNillable[int](errors.New("nil"))(ptr) // Left(error)
|
||||
// val := 42
|
||||
// result := either.FromNillable[int](errors.New("nil"))(&val) // Right(&42)
|
||||
func FromNillable[A any](e error) Kleisli[*A, *A] {
|
||||
return func(a *A) (*A, error) {
|
||||
if F.IsNil(a) {
|
||||
return Left[*A](e)
|
||||
}
|
||||
return Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrElse extracts the Right value or computes a default from the Left value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := either.GetOrElse(func(err error) int { return 0 })(either.Right[error](42)) // 42
|
||||
// result := either.GetOrElse(func(err error) int { return 0 })(either.Left[int](err)) // 0
|
||||
func GetOrElse[A any](onLeft func(error) A) func(A, error) A {
|
||||
return func(a A, err error) A {
|
||||
if err != nil {
|
||||
return onLeft(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce folds an Either into a single value using a reducer function.
|
||||
// Returns the initial value for Left, or applies the reducer to the Right value.
|
||||
func Reduce[A, B any](f func(B, A) B, initial B) func(A, error) B {
|
||||
return func(a A, err error) B {
|
||||
if err != nil {
|
||||
return initial
|
||||
}
|
||||
return f(initial, a)
|
||||
}
|
||||
}
|
||||
|
||||
// Alt provides an alternative Either if the first is Left.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// alternative := either.Alt[error](func() either.Either[error, int] {
|
||||
// return either.Right[error](99)
|
||||
// })
|
||||
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
|
||||
func Alt[A any](that func() (A, error)) Operator[A, A] {
|
||||
return func(a A, err error) (A, error) {
|
||||
if err != nil {
|
||||
return that()
|
||||
}
|
||||
return Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left by providing an alternative computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// recover := either.OrElse(func(err error) either.Either[error, int] {
|
||||
// return either.Right[error](0) // default value
|
||||
// })
|
||||
// result := recover(either.Left[int](errors.New("fail"))) // Right(0)
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return func(a A, err error) (A, error) {
|
||||
if err != nil {
|
||||
return onLeft(err)
|
||||
}
|
||||
return Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ToType attempts to convert an any value to a specific type, returning Either.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// convert := either.ToType[int](func(v any) error {
|
||||
// return fmt.Errorf("cannot convert %v to int", v)
|
||||
// })
|
||||
// result := convert(42) // Right(42)
|
||||
// result := convert("string") // Left(error)
|
||||
func ToType[A any](onError func(any) error) Kleisli[any, A] {
|
||||
return func(x any) (A, error) {
|
||||
if a, ok := x.(A); ok {
|
||||
return Of(a)
|
||||
}
|
||||
return Left[A](onError(x))
|
||||
}
|
||||
}
|
||||
|
||||
// Memoize returns the Either unchanged (Either values are already memoized).
|
||||
func Memoize[A any](a A, err error) (A, error) {
|
||||
return a, err
|
||||
}
|
||||
|
||||
// Flap is the curried version of [MonadFlap].
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return func(fab func(A) B, faberr error) (B, error) {
|
||||
if faberr != nil {
|
||||
return Left[B](faberr)
|
||||
}
|
||||
return Of(fab(a))
|
||||
}
|
||||
}
|
||||
566
v2/idiomatic/result/either_bench_test.go
Normal file
566
v2/idiomatic/result/either_bench_test.go
Normal file
@@ -0,0 +1,566 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
var (
|
||||
errBench = errors.New("benchmark error")
|
||||
benchResultInt int
|
||||
benchResultErr error
|
||||
benchBool bool
|
||||
benchInt int
|
||||
benchString string
|
||||
)
|
||||
|
||||
// Benchmark core constructors
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Left[int](errBench)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRight(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Right(42)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Of(42)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark predicates
|
||||
func BenchmarkIsLeft(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchBool = IsLeft(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsRight(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchBool = IsRight(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark fold operations
|
||||
func BenchmarkFold_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
folder := Fold(
|
||||
func(e error) int { return 0 },
|
||||
N.Mul(2),
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchInt = folder(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFold_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
folder := Fold(
|
||||
func(e error) int { return 0 },
|
||||
N.Mul(2),
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchInt = folder(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark functor operations
|
||||
func BenchmarkMap_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
mapper := Map(N.Mul(2))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = mapper(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
mapper := Map(N.Mul(2))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = mapper(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark monad operations
|
||||
func BenchmarkChain_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
chainer := Chain(func(a int) (int, error) { return Right(a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = chainer(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
chainer := Chain(func(a int) (int, error) { return Right(a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = chainer(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
chainer := ChainFirst(func(a int) (string, error) { return Right("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = chainer(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainFirst_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
chainer := ChainFirst(func(a int) (string, error) { return Right("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = chainer(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark alternative operations
|
||||
func BenchmarkAlt_RightRight(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
alternative := Alt(func() (int, error) { return Right(99) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = alternative(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAlt_LeftRight(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
alternative := Alt(func() (int, error) { return Right(99) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = alternative(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOrElse_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
recover := OrElse(func(e error) (int, error) { return Right(0) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = recover(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOrElse_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
recover := OrElse(func(e error) (int, error) { return Right(0) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = recover(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark GetOrElse
|
||||
func BenchmarkGetOrElse_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
getter := GetOrElse(func(e error) int { return 0 })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchInt = getter(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetOrElse_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
getter := GetOrElse(func(e error) int { return 0 })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchInt = getter(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark pipeline operations
|
||||
func BenchmarkPipeline_Map_Right(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Pipe2(
|
||||
21,
|
||||
Right[int],
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Map_Left(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Pipe2(
|
||||
0,
|
||||
func(int) (int, error) { return Left[int](errBench) },
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Chain_Right(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Pipe2(
|
||||
21,
|
||||
Right[int],
|
||||
Chain(func(x int) (int, error) { return Right(x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Chain_Left(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Pipe2(
|
||||
0,
|
||||
func(int) (int, error) { return Left[int](errBench) },
|
||||
Chain(func(x int) (int, error) { return Right(x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Complex_Right(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Pipe4(
|
||||
10,
|
||||
Right[int],
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) (int, error) { return Right(x + 1) }),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Complex_Left(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = Pipe4(
|
||||
0,
|
||||
func(int) (int, error) { return Left[int](errBench) },
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) (int, error) { return Right(x + 1) }),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark string formatting
|
||||
func BenchmarkToString_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchString = ToString(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToString_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchString = ToString(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark BiMap
|
||||
func BenchmarkBiMap_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
wrapErr := func(e error) error { return e }
|
||||
mapper := BiMap[int, int](wrapErr, N.Mul(2))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = mapper(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBiMap_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
wrapErr := func(e error) error { return e }
|
||||
mapper := BiMap[int, int](wrapErr, N.Mul(2))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = mapper(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark MapTo
|
||||
func BenchmarkMapTo_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
mapper := MapTo[int, int](99)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = mapper(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapTo_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
mapper := MapTo[int, int](99)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = mapper(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark MapLeft
|
||||
func BenchmarkMapLeft_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
mapper := MapLeft[int](func(e error) error { return e })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = mapper(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapLeft_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
mapper := MapLeft[int](func(e error) error { return e })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = mapper(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark ChainTo
|
||||
func BenchmarkChainTo_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
chainer := ChainTo[int, int](99, nil)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = chainer(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainTo_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
chainer := ChainTo[int, int](99, nil)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = chainer(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark Reduce
|
||||
func BenchmarkReduce_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
reducer := Reduce(func(acc, v int) int { return acc + v }, 10)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchInt = reducer(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReduce_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
reducer := Reduce(func(acc, v int) int { return acc + v }, 10)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchInt = reducer(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark FromPredicate
|
||||
func BenchmarkFromPredicate_Pass(b *testing.B) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
func(x int) error { return errBench },
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = pred(42)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromPredicate_Fail(b *testing.B) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
func(x int) error { return errBench },
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = pred(-1)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark Flap
|
||||
func BenchmarkFlap_Right(b *testing.B) {
|
||||
fn, ferr := Right(N.Mul(2))
|
||||
flapper := Flap[int, int](21)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = flapper(fn, ferr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlap_Left(b *testing.B) {
|
||||
fn, ferr := Left[func(int) int](errBench)
|
||||
flapper := Flap[int, int](21)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = flapper(fn, ferr)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark ToOption
|
||||
func BenchmarkToOption_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
var resVal int
|
||||
var resOk bool
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
resVal, resOk = ToOption(val, err)
|
||||
benchInt = resVal
|
||||
benchBool = resOk
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToOption_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
var resVal int
|
||||
var resOk bool
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
resVal, resOk = ToOption(val, err)
|
||||
benchInt = resVal
|
||||
benchBool = resOk
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark FromOption
|
||||
func BenchmarkFromOption_Some(b *testing.B) {
|
||||
converter := FromOption[int](func() error { return errBench })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = converter(42, true)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromOption_None(b *testing.B) {
|
||||
converter := FromOption[int](func() error { return errBench })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultInt, benchResultErr = converter(0, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark ToError
|
||||
func BenchmarkToError_Right(b *testing.B) {
|
||||
val, err := Right(42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultErr = ToError(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToError_Left(b *testing.B) {
|
||||
val, err := Left[int](errBench)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResultErr = ToError(val, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark TraverseArray
|
||||
func BenchmarkTraverseArray_Success(b *testing.B) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
traverse := TraverseArray(func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
})
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_, benchResultErr = traverse(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray_Error(b *testing.B) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
traverse := TraverseArray(func(x int) (int, error) {
|
||||
if x == 3 {
|
||||
return Left[int](errBench)
|
||||
}
|
||||
return Right(x * 2)
|
||||
})
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_, benchResultErr = traverse(input)
|
||||
}
|
||||
}
|
||||
112
v2/idiomatic/result/either_test.go
Normal file
112
v2/idiomatic/result/either_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/option"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsLeft(t *testing.T) {
|
||||
err := errors.New("Some error")
|
||||
withError, e1 := Left[string](err)
|
||||
|
||||
assert.True(t, IsLeft(withError, e1))
|
||||
assert.False(t, IsRight(withError, e1))
|
||||
}
|
||||
|
||||
func TestIsRight(t *testing.T) {
|
||||
noError, err := Right("Carsten")
|
||||
|
||||
assert.True(t, IsRight(noError, err))
|
||||
assert.False(t, IsLeft(noError, err))
|
||||
}
|
||||
|
||||
func TestMapEither(t *testing.T) {
|
||||
|
||||
AssertEq(Pipe2("abc", Of, Map(utils.StringLen)))(Right(3))(t)
|
||||
|
||||
e := errors.New("s")
|
||||
|
||||
AssertEq(Left[int](e))(Pipe2(e, Left[string], Map(utils.StringLen)))(t)
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
f := S.Size
|
||||
|
||||
maError := errors.New("maError")
|
||||
mabError := errors.New("mabError")
|
||||
AssertEq(Right(3))(Pipe2(f, Right, Ap[int](Right("abc"))))(t)
|
||||
AssertEq(Left[int](maError))(Pipe2(f, Right, Ap[int](Left[string](maError))))(t)
|
||||
AssertEq(Left[int](mabError))(Pipe2(mabError, Left[func(string) int], Ap[int](Left[string](maError))))(t)
|
||||
AssertEq(Left[int](mabError))(Pipe2(mabError, Left[func(string) int], Ap[int](Right("abc"))))(t)
|
||||
}
|
||||
|
||||
func TestAlt(t *testing.T) {
|
||||
|
||||
a := errors.New("a")
|
||||
b := errors.New("b")
|
||||
|
||||
AssertEq(Right(1))(Pipe2(1, Right, Alt(func() (int, error) { return Right(2) })))(t)
|
||||
AssertEq(Right(1))(Pipe2(1, Right, Alt(func() (int, error) { return Left[int](a) })))(t)
|
||||
AssertEq(Right(2))(Pipe2(b, Left[int], Alt(func() (int, error) { return Right(2) })))(t)
|
||||
AssertEq(Left[int](b))(Pipe2(a, Left[int], Alt(func() (int, error) { return Left[int](b) })))(t)
|
||||
}
|
||||
|
||||
func TestChainFirst(t *testing.T) {
|
||||
f := func(s string) (int, error) {
|
||||
return Of(S.Size((s)))
|
||||
}
|
||||
|
||||
maError := errors.New("maError")
|
||||
|
||||
AssertEq(Right("abc"))(Pipe2("abc", Right, ChainFirst(f)))(t)
|
||||
AssertEq(Left[string](maError))(Pipe2(maError, Left[string], ChainFirst(f)))(t)
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
a := errors.New("a")
|
||||
b := errors.New("b")
|
||||
f := ChainOptionK[int, int](F.Constant(a))(func(n int) (int, bool) {
|
||||
if n > 0 {
|
||||
return option.Some(n)
|
||||
}
|
||||
return option.None[int]()
|
||||
})
|
||||
AssertEq(Right(1))(f(Right(1)))(t)
|
||||
AssertEq(Left[int](a))(f(Right(-1)))(t)
|
||||
AssertEq(Left[int](b))(f(Left[int](b)))(t)
|
||||
}
|
||||
|
||||
func TestFromOption(t *testing.T) {
|
||||
none := errors.New("none")
|
||||
AssertEq(Left[int](none))(FromOption[int](F.Constant(none))(option.None[int]()))(t)
|
||||
AssertEq(Right(1))(FromOption[int](F.Constant(none))(option.Some(1)))(t)
|
||||
}
|
||||
|
||||
func TestStringer(t *testing.T) {
|
||||
e := ToString(Of("foo"))
|
||||
exp := "Right[string](foo)"
|
||||
|
||||
assert.Equal(t, exp, e)
|
||||
|
||||
}
|
||||
63
v2/idiomatic/result/eq.go
Normal file
63
v2/idiomatic/result/eq.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
)
|
||||
|
||||
// Eq constructs an equality predicate for Result values (A, error).
|
||||
// Two Result values are equal if they are both Left (error) with equal error values,
|
||||
// or both Right (success) with equal values according to the provided equality predicate.
|
||||
//
|
||||
// Parameters:
|
||||
// - eq: Equality predicate for the Right (success) type A
|
||||
//
|
||||
// Returns a curried comparison function that takes two Result values and returns true if equal.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// eq := result.Eq(eq.FromStrictEquals[int]())
|
||||
// result1 := eq(42, nil)(42, nil) // true
|
||||
// result2 := eq(42, nil)(43, nil) // false
|
||||
func Eq[A any](eq EQ.Eq[A]) func(A, error) func(A, error) bool {
|
||||
return func(a A, aerr error) func(A, error) bool {
|
||||
return func(b A, berr error) bool {
|
||||
if aerr != nil {
|
||||
if berr != nil {
|
||||
return aerr == berr
|
||||
}
|
||||
return false
|
||||
}
|
||||
if berr != nil {
|
||||
return false
|
||||
}
|
||||
return eq.Equals(a, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromStrictEquals constructs an equality predicate using Go's == operator.
|
||||
// The Right type must be comparable.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// eq := result.FromStrictEquals[int]()
|
||||
// result1 := eq(42, nil)(42, nil) // true
|
||||
// result2 := eq(42, nil)(43, nil) // false
|
||||
func FromStrictEquals[A comparable]() func(A, error) func(A, error) bool {
|
||||
return Eq(EQ.FromStrictEquals[A]())
|
||||
}
|
||||
54
v2/idiomatic/result/eq_test.go
Normal file
54
v2/idiomatic/result/eq_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEq(t *testing.T) {
|
||||
|
||||
r1v, r1e := Of(1)
|
||||
r2v, r2e := Of(2)
|
||||
|
||||
e1v, e1e := Left[int](errorString("a"))
|
||||
e2v, e2e := Left[int](errorString("a"))
|
||||
e3v, e3e := Left[int](errorString("b"))
|
||||
|
||||
eq := FromStrictEquals[int]()
|
||||
|
||||
// Right values
|
||||
assert.True(t, eq(r1v, r1e)(r1v, r1e))
|
||||
assert.False(t, eq(r1v, r1e)(r2v, r2e))
|
||||
|
||||
// Left values (errors)
|
||||
assert.True(t, eq(e1v, e1e)(e1v, e1e))
|
||||
assert.True(t, eq(e1v, e1e)(e2v, e2e))
|
||||
assert.False(t, eq(e1v, e1e)(e3v, e3e))
|
||||
|
||||
// Mixed Left and Right
|
||||
assert.False(t, eq(r1v, r1e)(e1v, e1e))
|
||||
assert.False(t, eq(e1v, e1e)(r1v, r1e))
|
||||
}
|
||||
|
||||
// errorString is a simple error type for testing
|
||||
type errorString string
|
||||
|
||||
func (e errorString) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
58
v2/idiomatic/result/examples_create_test.go
Normal file
58
v2/idiomatic/result/examples_create_test.go
Normal 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 result
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/errors"
|
||||
)
|
||||
|
||||
func Example_creation() {
|
||||
// Build an Either
|
||||
leftValue, leftErr := Left[string](fmt.Errorf("some error"))
|
||||
rightValue, rightErr := Right("value")
|
||||
|
||||
// Build from a value
|
||||
fromNillable := FromNillable[string](fmt.Errorf("value was nil"))
|
||||
leftFromNil, nilErr := fromNillable(nil)
|
||||
value := "value"
|
||||
rightFromPointer, ptrErr := fromNillable(&value)
|
||||
|
||||
// some predicate
|
||||
isEven := func(num int) bool {
|
||||
return num%2 == 0
|
||||
}
|
||||
fromEven := FromPredicate(isEven, errors.OnSome[int]("%d is an odd number"))
|
||||
leftFromPred, predErrOdd := fromEven(3)
|
||||
rightFromPred, predErrEven := fromEven(4)
|
||||
|
||||
fmt.Println(ToString(leftValue, leftErr))
|
||||
fmt.Println(ToString(rightValue, rightErr))
|
||||
fmt.Println(ToString(leftFromNil, nilErr))
|
||||
fmt.Println(IsRight(rightFromPointer, ptrErr))
|
||||
fmt.Println(ToString(leftFromPred, predErrOdd))
|
||||
fmt.Println(ToString(rightFromPred, predErrEven))
|
||||
|
||||
// Output:
|
||||
// Left(some error)
|
||||
// Right[string](value)
|
||||
// Left(value was nil)
|
||||
// true
|
||||
// Left(3 is an odd number)
|
||||
// Right[int](4)
|
||||
|
||||
}
|
||||
62
v2/idiomatic/result/examples_extract_test.go
Normal file
62
v2/idiomatic/result/examples_extract_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
func Example_extraction() {
|
||||
leftValue, leftErr := Left[int](fmt.Errorf("Division by Zero!"))
|
||||
rightValue, rightErr := Right(10)
|
||||
|
||||
// Convert Either[A] to A with a default value
|
||||
leftWithDefault := GetOrElse(F.Constant1[error](0))(leftValue, leftErr) // 0
|
||||
rightWithDefault := GetOrElse(F.Constant1[error](0))(rightValue, rightErr) // 10
|
||||
|
||||
// Apply a different function on Left(...)/Right(...)
|
||||
doubleOrZero := Fold(F.Constant1[error](0), N.Mul(2)) // func(int, error) int
|
||||
doubleFromLeft := doubleOrZero(leftValue, leftErr) // 0
|
||||
doubleFromRight := doubleOrZero(rightValue, rightErr) // 20
|
||||
|
||||
// You can also chain operations using Map
|
||||
tripled, tripledErr := Pipe2(
|
||||
rightValue,
|
||||
Right[int],
|
||||
Map(N.Mul(3)),
|
||||
)
|
||||
tripledResult := GetOrElse(F.Constant1[error](0))(tripled, tripledErr) // 30
|
||||
|
||||
fmt.Println(ToString(leftValue, leftErr))
|
||||
fmt.Println(ToString(rightValue, rightErr))
|
||||
fmt.Println(leftWithDefault)
|
||||
fmt.Println(rightWithDefault)
|
||||
fmt.Println(doubleFromLeft)
|
||||
fmt.Println(doubleFromRight)
|
||||
fmt.Println(tripledResult)
|
||||
|
||||
// Output:
|
||||
// Left(Division by Zero!)
|
||||
// Right[int](10)
|
||||
// 0
|
||||
// 10
|
||||
// 0
|
||||
// 20
|
||||
// 30
|
||||
}
|
||||
49
v2/idiomatic/result/exec/exec.go
Normal file
49
v2/idiomatic/result/exec/exec.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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 exec provides utilities for executing system commands with Either-based error handling.
|
||||
package exec
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/exec"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
GE "github.com/IBM/fp-go/v2/internal/exec"
|
||||
)
|
||||
|
||||
var (
|
||||
// Command executes a system command and returns the result as an Either.
|
||||
// Use this version if the command does not produce any side effects,
|
||||
// i.e., if the output is uniquely determined by the input.
|
||||
// For commands with side effects, typically you'd use the IOEither version instead.
|
||||
//
|
||||
// Parameters (curried):
|
||||
// - name: The command name/path
|
||||
// - args: Command arguments
|
||||
// - in: Input bytes to send to the command's stdin
|
||||
//
|
||||
// Returns Either[error, CommandOutput] containing the command's output or an error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := exec.Command("echo")( []string{"hello"})([]byte{})
|
||||
// // result is Right(CommandOutput{Stdout: "hello\n", ...})
|
||||
Command = result.Curry3(command)
|
||||
)
|
||||
|
||||
func command(name string, args []string, in []byte) (exec.CommandOutput, error) {
|
||||
return GE.Exec(context.Background(), name, args, in)
|
||||
}
|
||||
89
v2/idiomatic/result/function.go
Normal file
89
v2/idiomatic/result/function.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package result
|
||||
|
||||
// Pipe1 takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Pipe1[F1 ~func(T0) (T1, error), T0, T1 any](t0 T0, f1 F1) (T1, error) {
|
||||
return f1(t0)
|
||||
}
|
||||
|
||||
// Flow1 creates a function that takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Flow1[F1 ~func(T0, error) (T1, error), T0, T1 any](f1 F1) func(T0, error) (T1, error) {
|
||||
return f1
|
||||
}
|
||||
|
||||
// Pipe2 takes an initial value t0 and successively applies 2 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Pipe2[F1 ~func(T0) (T1, error), F2 ~func(T1, error) (T2, error), T0, T1, T2 any](t0 T0, f1 F1, f2 F2) (T2, error) {
|
||||
return f2(f1(t0))
|
||||
}
|
||||
|
||||
// Flow2 creates a function that takes an initial value t0 and successively applies 2 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Flow2[F1 ~func(T0, error) (T1, error), F2 ~func(T1, error) (T2, error), T0, T1, T2 any](f1 F1, f2 F2) func(T0, error) (T2, error) {
|
||||
return func(t0 T0, t0ok error) (T2, error) {
|
||||
return f2(f1(t0, t0ok))
|
||||
}
|
||||
}
|
||||
|
||||
// Pipe3 takes an initial value t0 and successively applies 3 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Pipe3[F1 ~func(T0) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), T0, T1, T2, T3 any](t0 T0, f1 F1, f2 F2, f3 F3) (T3, error) {
|
||||
return f3(f2(f1(t0)))
|
||||
}
|
||||
|
||||
// Flow3 creates a function that takes an initial value t0 and successively applies 3 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Flow3[F1 ~func(T0, error) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), T0, T1, T2, T3 any](f1 F1, f2 F2, f3 F3) func(T0, error) (T3, error) {
|
||||
return func(t0 T0, t0ok error) (T3, error) {
|
||||
return f3(f2(f1(t0, t0ok)))
|
||||
}
|
||||
}
|
||||
|
||||
// Pipe4 takes an initial value t0 and successively applies 4 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Pipe4[F1 ~func(T0) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), F4 ~func(T3, error) (T4, error), T0, T1, T2, T3, T4 any](t0 T0, f1 F1, f2 F2, f3 F3, f4 F4) (T4, error) {
|
||||
return f4(f3(f2(f1(t0))))
|
||||
}
|
||||
|
||||
// Flow4 creates a function that takes an initial value t0 and successively applies 4 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Flow4[F1 ~func(T0, error) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), F4 ~func(T3, error) (T4, error), T0, T1, T2, T3, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(T0, error) (T4, error) {
|
||||
return func(t0 T0, t0ok error) (T4, error) {
|
||||
return f4(f3(f2(f1(t0, t0ok))))
|
||||
}
|
||||
}
|
||||
|
||||
// Pipe5 takes an initial value t0 and successively applies 5 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Pipe5[F1 ~func(T0) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), F4 ~func(T3, error) (T4, error), F5 ~func(T4, error) (T5, error), T0, T1, T2, T3, T4, T5 any](t0 T0, f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) (T5, error) {
|
||||
return f5(f4(f3(f2(f1(t0)))))
|
||||
}
|
||||
|
||||
// Flow5 creates a function that takes an initial value t0 and successively applies 5 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
//
|
||||
//go:inline
|
||||
func Flow5[F1 ~func(T0, error) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), F4 ~func(T3, error) (T4, error), F5 ~func(T4, error) (T5, error), T0, T1, T2, T3, T4, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(T0, error) (T5, error) {
|
||||
return func(t0 T0, t0ok error) (T5, error) {
|
||||
return f5(f4(f3(f2(f1(t0, t0ok)))))
|
||||
}
|
||||
}
|
||||
454
v2/idiomatic/result/functions_test.go
Normal file
454
v2/idiomatic/result/functions_test.go
Normal file
@@ -0,0 +1,454 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestBiMap tests mapping over both error and value channels
|
||||
func TestBiMap(t *testing.T) {
|
||||
wrapError := func(e error) error {
|
||||
return fmt.Errorf("wrapped: %w", e)
|
||||
}
|
||||
double := N.Mul(2)
|
||||
|
||||
t.Run("BiMap on Right", func(t *testing.T) {
|
||||
val, err := BiMap[int, int](wrapError, double)(Right(21))
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("BiMap on Left", func(t *testing.T) {
|
||||
originalErr := errors.New("original")
|
||||
val, err := BiMap[int, int](wrapError, double)(Left[int](originalErr))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "wrapped")
|
||||
assert.Contains(t, err.Error(), "original")
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMapTo tests mapping to a constant value
|
||||
func TestMapTo(t *testing.T) {
|
||||
t.Run("MapTo on Right", func(t *testing.T) {
|
||||
val, err := MapTo[int, string]("constant")(Right(42))
|
||||
AssertEq(Right("constant"))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("MapTo on Left", func(t *testing.T) {
|
||||
originalErr := errors.New("error")
|
||||
val, err := MapTo[int, string]("constant")(Left[int](originalErr))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
// MapTo still applies the constant value even for Left
|
||||
assert.Equal(t, "constant", val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainTo tests chaining to a constant value
|
||||
func TestChainTo(t *testing.T) {
|
||||
t.Run("ChainTo Right to Right", func(t *testing.T) {
|
||||
val, err := ChainTo[int, string]("success", nil)(Right(42))
|
||||
AssertEq(Right("success"))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("ChainTo Right to Left", func(t *testing.T) {
|
||||
targetErr := errors.New("target error")
|
||||
val, err := ChainTo[int, string]("", targetErr)(Right(42))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, targetErr, err)
|
||||
assert.Equal(t, "", val)
|
||||
})
|
||||
|
||||
t.Run("ChainTo Left", func(t *testing.T) {
|
||||
sourceErr := errors.New("source error")
|
||||
val, err := ChainTo[int, string]("success", nil)(Left[int](sourceErr))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, sourceErr, err)
|
||||
assert.Equal(t, "", val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReduce tests the reduce/fold operation
|
||||
func TestReduce(t *testing.T) {
|
||||
sum := func(acc, val int) int {
|
||||
return acc + val
|
||||
}
|
||||
|
||||
t.Run("Reduce on Right", func(t *testing.T) {
|
||||
result := Reduce(sum, 10)(Right(32))
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("Reduce on Left", func(t *testing.T) {
|
||||
result := Reduce(sum, 10)(Left[int](errors.New("error")))
|
||||
assert.Equal(t, 10, result) // Returns initial value
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromPredicate tests creating Result from a predicate
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
)
|
||||
|
||||
t.Run("Predicate passes", func(t *testing.T) {
|
||||
val, err := isPositive(42)
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("Predicate fails", func(t *testing.T) {
|
||||
val, err := isPositive(-5)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not positive")
|
||||
// FromPredicate returns zero value on Left
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
|
||||
t.Run("Predicate with zero", func(t *testing.T) {
|
||||
val, err := isPositive(0)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromNillable tests creating Result from nullable pointers
|
||||
func TestFromNillable(t *testing.T) {
|
||||
nilErr := errors.New("value is nil")
|
||||
fromPtr := FromNillable[int](nilErr)
|
||||
|
||||
t.Run("Non-nil pointer", func(t *testing.T) {
|
||||
value := 42
|
||||
val, err := fromPtr(&value)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, val)
|
||||
assert.Equal(t, 42, *val)
|
||||
})
|
||||
|
||||
t.Run("Nil pointer", func(t *testing.T) {
|
||||
val, err := fromPtr(nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, nilErr, err)
|
||||
assert.Nil(t, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToType tests type conversion
|
||||
func TestToType(t *testing.T) {
|
||||
toInt := ToType[int](func(v any) error {
|
||||
return fmt.Errorf("cannot convert %T to int", v)
|
||||
})
|
||||
|
||||
t.Run("Correct type", func(t *testing.T) {
|
||||
val, err := toInt(42)
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("Wrong type", func(t *testing.T) {
|
||||
val, err := toInt("string")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot convert")
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
|
||||
t.Run("Nil value", func(t *testing.T) {
|
||||
val, err := toInt(nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMemoize tests that Memoize returns the value unchanged
|
||||
func TestMemoize(t *testing.T) {
|
||||
t.Run("Memoize Right", func(t *testing.T) {
|
||||
val, err := Memoize(Right(42))
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("Memoize Left", func(t *testing.T) {
|
||||
originalErr := errors.New("error")
|
||||
val, err := Memoize(Left[int](originalErr))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFlap tests reverse application
|
||||
func TestFlap(t *testing.T) {
|
||||
t.Run("Flap with Right function", func(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
val, err := Flap[int, int](21)(Right(double))
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("Flap with Left function", func(t *testing.T) {
|
||||
fnErr := errors.New("function error")
|
||||
val, err := Flap[int, int](21)(Left[func(int) int](fnErr))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, fnErr, err)
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToError tests extracting error from Result
|
||||
func TestToError(t *testing.T) {
|
||||
t.Run("ToError from Right", func(t *testing.T) {
|
||||
err := ToError(Right(42))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("ToError from Left", func(t *testing.T) {
|
||||
originalErr := errors.New("error")
|
||||
err := ToError(Left[int](originalErr))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLet tests the Let operation for do-notation
|
||||
func TestLet(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
}
|
||||
|
||||
t.Run("Let with Right", func(t *testing.T) {
|
||||
val, err := Pipe2(
|
||||
State{value: 10},
|
||||
Right,
|
||||
Let(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State { return State{value: s.value + v} }
|
||||
},
|
||||
func(s State) int { return 32 },
|
||||
),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val.value)
|
||||
})
|
||||
|
||||
t.Run("Let with Left", func(t *testing.T) {
|
||||
originalErr := errors.New("error")
|
||||
val, err := Pipe2(
|
||||
originalErr,
|
||||
Left[State],
|
||||
Let(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State { return State{value: v} }
|
||||
},
|
||||
func(s State) int { return 42 },
|
||||
),
|
||||
)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
assert.Equal(t, State{}, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLetTo tests the LetTo operation
|
||||
func TestLetTo(t *testing.T) {
|
||||
type State struct {
|
||||
name string
|
||||
}
|
||||
|
||||
t.Run("LetTo with Right", func(t *testing.T) {
|
||||
val, err := Pipe2(
|
||||
State{},
|
||||
Right,
|
||||
LetTo(
|
||||
func(n string) func(State) State {
|
||||
return func(s State) State { return State{name: n} }
|
||||
},
|
||||
"Alice",
|
||||
),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", val.name)
|
||||
})
|
||||
|
||||
t.Run("LetTo with Left", func(t *testing.T) {
|
||||
originalErr := errors.New("error")
|
||||
val, err := Pipe2(
|
||||
originalErr,
|
||||
Left[State],
|
||||
LetTo(
|
||||
func(n string) func(State) State {
|
||||
return func(s State) State { return State{name: n} }
|
||||
},
|
||||
"Bob",
|
||||
),
|
||||
)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, State{}, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBindTo tests the BindTo operation
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
}
|
||||
|
||||
t.Run("BindTo with Right", func(t *testing.T) {
|
||||
val, err := Pipe2(
|
||||
42,
|
||||
Right,
|
||||
BindTo(func(v int) State { return State{value: v} }),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val.value)
|
||||
})
|
||||
|
||||
t.Run("BindTo with Left", func(t *testing.T) {
|
||||
originalErr := errors.New("error")
|
||||
val, err := Pipe2(
|
||||
originalErr,
|
||||
Left[int],
|
||||
BindTo(func(v int) State { return State{value: v} }),
|
||||
)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, State{}, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMapLeft tests mapping over the error channel
|
||||
func TestMapLeft(t *testing.T) {
|
||||
wrapError := func(e error) error {
|
||||
return fmt.Errorf("wrapped: %w", e)
|
||||
}
|
||||
|
||||
t.Run("MapLeft on Right", func(t *testing.T) {
|
||||
val, err := MapLeft[int](wrapError)(Right(42))
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("MapLeft on Left", func(t *testing.T) {
|
||||
originalErr := errors.New("original")
|
||||
_, err := MapLeft[int](wrapError)(Left[int](originalErr))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "wrapped")
|
||||
assert.Contains(t, err.Error(), "original")
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrElse tests recovery from error
|
||||
func TestOrElse(t *testing.T) {
|
||||
recover := OrElse(func(e error) (int, error) {
|
||||
return Right(0) // default value
|
||||
})
|
||||
|
||||
t.Run("OrElse on Right", func(t *testing.T) {
|
||||
val, err := recover(Right(42))
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("OrElse on Left recovers", func(t *testing.T) {
|
||||
val, err := recover(Left[int](errors.New("error")))
|
||||
AssertEq(Right(0))(val, err)(t)
|
||||
})
|
||||
}
|
||||
|
||||
// TestDo tests the Do operation
|
||||
func TestDo(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
result, err := Do(State{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, State{}, result)
|
||||
}
|
||||
|
||||
// TestOf tests the Of/pure operation
|
||||
func TestOf(t *testing.T) {
|
||||
val, err := Of(42)
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
}
|
||||
|
||||
// TestToString tests string representation
|
||||
func TestToString(t *testing.T) {
|
||||
t.Run("ToString Right", func(t *testing.T) {
|
||||
str := ToString(Right(42))
|
||||
assert.Equal(t, "Right[int](42)", str)
|
||||
})
|
||||
|
||||
t.Run("ToString Left", func(t *testing.T) {
|
||||
str := ToString(Left[int](errors.New("error")))
|
||||
assert.Contains(t, str, "Left(")
|
||||
assert.Contains(t, str, "error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestToOption tests conversion to Option
|
||||
func TestToOption(t *testing.T) {
|
||||
t.Run("ToOption from Right", func(t *testing.T) {
|
||||
val, ok := ToOption(Right(42))
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
t.Run("ToOption from Left", func(t *testing.T) {
|
||||
val, ok := ToOption(Left[int](errors.New("error")))
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromError tests creating Result from error-returning function
|
||||
func TestFromError(t *testing.T) {
|
||||
validate := func(x int) error {
|
||||
if x < 0 {
|
||||
return errors.New("negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
toResult := FromError(validate)
|
||||
|
||||
t.Run("FromError with valid value", func(t *testing.T) {
|
||||
val, err := toResult(42)
|
||||
AssertEq(Right(42))(val, err)(t)
|
||||
})
|
||||
|
||||
t.Run("FromError with invalid value", func(t *testing.T) {
|
||||
val, err := toResult(-5)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "negative", err.Error())
|
||||
assert.Equal(t, -5, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetOrElse tests extracting value with default
|
||||
func TestGetOrElse(t *testing.T) {
|
||||
defaultValue := func(error) int { return 0 }
|
||||
|
||||
t.Run("GetOrElse on Right", func(t *testing.T) {
|
||||
val := GetOrElse(defaultValue)(Right(42))
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
t.Run("GetOrElse on Left", func(t *testing.T) {
|
||||
val := GetOrElse(defaultValue)(Left[int](errors.New("error")))
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
}
|
||||
41
v2/idiomatic/result/functor.go
Normal file
41
v2/idiomatic/result/functor.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
type (
|
||||
eitherFunctor[A, B any] struct{}
|
||||
|
||||
Functor[A, B any] interface {
|
||||
Map(f func(A) B) Operator[A, B]
|
||||
}
|
||||
)
|
||||
|
||||
func (o eitherFunctor[A, B]) Map(f func(A) B) Operator[A, B] {
|
||||
return Map(f)
|
||||
}
|
||||
|
||||
// MakeFunctor creates a Functor instance for Result operations.
|
||||
// A functor provides the Map operation that transforms values inside a context
|
||||
// while preserving the structure.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// f := result.MakeFunctor[int, string]()
|
||||
// val, err := f.Map(strconv.Itoa)(result.Right[error](42))
|
||||
// // val is "42", err is nil
|
||||
func MakeFunctor[A, B any]() Functor[A, B] {
|
||||
return eitherFunctor[A, B]{}
|
||||
}
|
||||
303
v2/idiomatic/result/gen.go
Normal file
303
v2/idiomatic/result/gen.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2025-03-09 23:52:50.4396159 +0100 CET m=+0.001548701
|
||||
//
|
||||
// This file contains generated functions for converting between Either and tuple-based functions.
|
||||
// It provides Eitherize/Uneitherize functions for functions with 0-15 parameters,
|
||||
// as well as SequenceT/SequenceTuple/TraverseTuple functions for working with tuples of Either values.
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2025-03-09 23:52:50.4396159 +0100 CET m=+0.001548701
|
||||
|
||||
package result
|
||||
|
||||
// TraverseTuple1 converts a [Tuple1] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple1]].
|
||||
func TraverseTuple1[F1 ~func(A1) (T1, error), E, A1, T1 any](f1 F1) func(A1) (T1, error) {
|
||||
return func(a1 A1) (t1 T1, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple2 converts a [Tuple2] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple2]].
|
||||
func TraverseTuple2[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), E, A1, T1, A2, T2 any](f1 F1, f2 F2) func(A1, A2) (T1, T2, error) {
|
||||
return func(a1 A1, a2 A2) (t1 T1, t2 T2, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple3 converts a [Tuple3] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple3]].
|
||||
func TraverseTuple3[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), E, A1, T1, A2, T2, A3, T3 any](f1 F1, f2 F2, f3 F3) func(A1, A2, A3) (T1, T2, T3, error) {
|
||||
return func(a1 A1, a2 A2, a3 A3) (t1 T1, t2 T2, t3 T3, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t3, err = f3(a3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple4 converts a [Tuple4] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple4]].
|
||||
func TraverseTuple4[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), E, A1, T1, A2, T2, A3, T3, A4, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(A1, A2, A3, A4) (T1, T2, T3, T4, error) {
|
||||
return func(a1 A1, a2 A2, a3 A3, a4 A4) (t1 T1, t2 T2, t3 T3, t4 T4, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t3, err = f3(a3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t4, err = f4(a4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple5 converts a [Tuple5] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple5]].
|
||||
func TraverseTuple5[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(A1, A2, A3, A4, A5) (T1, T2, T3, T4, T5, error) {
|
||||
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t3, err = f3(a3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t4, err = f4(a4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t5, err = f5(a5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple6 converts a [Tuple6] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple6]].
|
||||
func TraverseTuple6[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6) func(A1, A2, A3, A4, A5, A6) (T1, T2, T3, T4, T5, T6, error) {
|
||||
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t3, err = f3(a3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t4, err = f4(a4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t5, err = f5(a5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t6, err = f6(a6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple7 converts a [Tuple7] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple7]].
|
||||
func TraverseTuple7[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), F7 ~func(A7) (T7, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7) func(A1, A2, A3, A4, A5, A6, A7) (T1, T2, T3, T4, T5, T6, T7, error) {
|
||||
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t3, err = f3(a3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t4, err = f4(a4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t5, err = f5(a5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t6, err = f6(a6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t7, err = f7(a7)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple8 converts a [Tuple8] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple8]].
|
||||
func TraverseTuple8[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), F7 ~func(A7) (T7, error), F8 ~func(A8) (T8, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8) func(A1, A2, A3, A4, A5, A6, A7, A8) (T1, T2, T3, T4, T5, T6, T7, T8, error) {
|
||||
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t3, err = f3(a3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t4, err = f4(a4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t5, err = f5(a5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t6, err = f6(a6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t7, err = f7(a7)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t8, err = f8(a8)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple9 converts a [Tuple9] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple9]].
|
||||
func TraverseTuple9[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), F7 ~func(A7) (T7, error), F8 ~func(A8) (T8, error), F9 ~func(A9) (T9, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8, A9, T9 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9) func(A1, A2, A3, A4, A5, A6, A7, A8, A9) (T1, T2, T3, T4, T5, T6, T7, T8, T9, error) {
|
||||
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8, a9 A9) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, t9 T9, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t3, err = f3(a3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t4, err = f4(a4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t5, err = f5(a5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t6, err = f6(a6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t7, err = f7(a7)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t8, err = f8(a8)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t9, err = f9(a9)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseTuple10 converts a [Tuple10] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple10]].
|
||||
func TraverseTuple10[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), F7 ~func(A7) (T7, error), F8 ~func(A8) (T8, error), F9 ~func(A9) (T9, error), F10 ~func(A10) (T10, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8, A9, T9, A10, T10 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10) func(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, error) {
|
||||
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8, a9 A9, a10 A10) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, t9 T9, t10 T10, err error) {
|
||||
t1, err = f1(a1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t2, err = f2(a2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t3, err = f3(a3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t4, err = f4(a4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t5, err = f5(a5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t6, err = f6(a6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t7, err = f7(a7)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t8, err = f8(a8)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t9, err = f9(a9)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t10, err = f10(a10)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
202
v2/idiomatic/result/gen_coverage_test.go
Normal file
202
v2/idiomatic/result/gen_coverage_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test TraverseTuple1
|
||||
func TestTraverseTuple1(t *testing.T) {
|
||||
f := func(x int) (string, error) {
|
||||
if x > 0 {
|
||||
return Right("positive")
|
||||
}
|
||||
return Left[string](errors.New("negative"))
|
||||
}
|
||||
|
||||
result, err := TraverseTuple1[func(int) (string, error), error](f)(5)
|
||||
AssertEq(Right("positive"))(result, err)(t)
|
||||
|
||||
result, err = TraverseTuple1[func(int) (string, error), error](f)(-1)
|
||||
AssertEq(Left[string](errors.New("negative")))(result, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple2
|
||||
func TestTraverseTuple2(t *testing.T) {
|
||||
f1 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f2 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, err := TraverseTuple2[func(int) (int, error), func(int) (int, error), error](f1, f2)(1, 2)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple3
|
||||
func TestTraverseTuple3(t *testing.T) {
|
||||
f1 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f2 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f3 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, r3, err := TraverseTuple3[func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f1, f2, f3)(1, 2, 3)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
AssertEq(Right(6))(r3, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple4
|
||||
func TestTraverseTuple4(t *testing.T) {
|
||||
f1 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f2 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f3 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f4 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, r3, r4, err := TraverseTuple4[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f1, f2, f3, f4)(1, 2, 3, 4)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
AssertEq(Right(6))(r3, err)(t)
|
||||
AssertEq(Right(8))(r4, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple5
|
||||
func TestTraverseTuple5(t *testing.T) {
|
||||
f1 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f2 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f3 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f4 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
f5 := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, r3, r4, r5, err := TraverseTuple5[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f1, f2, f3, f4, f5)(1, 2, 3, 4, 5)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
AssertEq(Right(6))(r3, err)(t)
|
||||
AssertEq(Right(8))(r4, err)(t)
|
||||
AssertEq(Right(10))(r5, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple6
|
||||
func TestTraverseTuple6(t *testing.T) {
|
||||
f := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, r3, r4, r5, r6, err := TraverseTuple6[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f)(1, 2, 3, 4, 5, 6)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
AssertEq(Right(6))(r3, err)(t)
|
||||
AssertEq(Right(8))(r4, err)(t)
|
||||
AssertEq(Right(10))(r5, err)(t)
|
||||
AssertEq(Right(12))(r6, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple7
|
||||
func TestTraverseTuple7(t *testing.T) {
|
||||
f := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, r3, r4, r5, r6, r7, err := TraverseTuple7[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f, f)(1, 2, 3, 4, 5, 6, 7)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
AssertEq(Right(6))(r3, err)(t)
|
||||
AssertEq(Right(8))(r4, err)(t)
|
||||
AssertEq(Right(10))(r5, err)(t)
|
||||
AssertEq(Right(12))(r6, err)(t)
|
||||
AssertEq(Right(14))(r7, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple8
|
||||
func TestTraverseTuple8(t *testing.T) {
|
||||
f := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, r3, r4, r5, r6, r7, r8, err := TraverseTuple8[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f, f, f)(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
AssertEq(Right(6))(r3, err)(t)
|
||||
AssertEq(Right(8))(r4, err)(t)
|
||||
AssertEq(Right(10))(r5, err)(t)
|
||||
AssertEq(Right(12))(r6, err)(t)
|
||||
AssertEq(Right(14))(r7, err)(t)
|
||||
AssertEq(Right(16))(r8, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple9
|
||||
func TestTraverseTuple9(t *testing.T) {
|
||||
f := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, r3, r4, r5, r6, r7, r8, r9, err := TraverseTuple9[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f, f, f, f)(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
AssertEq(Right(6))(r3, err)(t)
|
||||
AssertEq(Right(8))(r4, err)(t)
|
||||
AssertEq(Right(10))(r5, err)(t)
|
||||
AssertEq(Right(12))(r6, err)(t)
|
||||
AssertEq(Right(14))(r7, err)(t)
|
||||
AssertEq(Right(16))(r8, err)(t)
|
||||
AssertEq(Right(18))(r9, err)(t)
|
||||
}
|
||||
|
||||
// Test TraverseTuple10
|
||||
func TestTraverseTuple10(t *testing.T) {
|
||||
f := func(x int) (int, error) {
|
||||
return Right(x * 2)
|
||||
}
|
||||
|
||||
r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, err := TraverseTuple10[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f, f, f, f, f)(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
AssertEq(Right(2))(r1, err)(t)
|
||||
AssertEq(Right(4))(r2, err)(t)
|
||||
AssertEq(Right(6))(r3, err)(t)
|
||||
AssertEq(Right(8))(r4, err)(t)
|
||||
AssertEq(Right(10))(r5, err)(t)
|
||||
AssertEq(Right(12))(r6, err)(t)
|
||||
AssertEq(Right(14))(r7, err)(t)
|
||||
AssertEq(Right(16))(r8, err)(t)
|
||||
AssertEq(Right(18))(r9, err)(t)
|
||||
AssertEq(Right(20))(r10, err)(t)
|
||||
}
|
||||
70
v2/idiomatic/result/http/request.go
Normal file
70
v2/idiomatic/result/http/request.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 http provides utilities for creating HTTP requests with Either-based error handling.
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
// PostRequest creates a POST HTTP request with a body.
|
||||
// Usage: PostRequest(url)(body) returns Either[error, *http.Request]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// request := http.PostRequest("https://api.example.com/data")([]byte(`{"key":"value"}`))
|
||||
PostRequest = bodyRequest("POST")
|
||||
|
||||
// PutRequest creates a PUT HTTP request with a body.
|
||||
// Usage: PutRequest(url)(body) returns Either[error, *http.Request]
|
||||
PutRequest = bodyRequest("PUT")
|
||||
|
||||
// GetRequest creates a GET HTTP request without a body.
|
||||
// Usage: GetRequest(url) returns Either[error, *http.Request]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// request := http.GetRequest("https://api.example.com/data")
|
||||
GetRequest = noBodyRequest("GET")
|
||||
|
||||
// DeleteRequest creates a DELETE HTTP request without a body.
|
||||
// Usage: DeleteRequest(url) returns Either[error, *http.Request]
|
||||
DeleteRequest = noBodyRequest("DELETE")
|
||||
|
||||
// OptionsRequest creates an OPTIONS HTTP request without a body.
|
||||
// Usage: OptionsRequest(url) returns Either[error, *http.Request]
|
||||
OptionsRequest = noBodyRequest("OPTIONS")
|
||||
|
||||
// HeadRequest creates a HEAD HTTP request without a body.
|
||||
// Usage: HeadRequest(url) returns Either[error, *http.Request]
|
||||
HeadRequest = noBodyRequest("HEAD")
|
||||
)
|
||||
|
||||
func bodyRequest(method string) func(string) func([]byte) (*http.Request, error) {
|
||||
return func(url string) func([]byte) (*http.Request, error) {
|
||||
return func(body []byte) (*http.Request, error) {
|
||||
return http.NewRequest(method, url, bytes.NewReader(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func noBodyRequest(method string) func(string) (*http.Request, error) {
|
||||
return func(url string) (*http.Request, error) {
|
||||
return http.NewRequest(method, url, nil)
|
||||
}
|
||||
}
|
||||
56
v2/idiomatic/result/logger.go
Normal file
56
v2/idiomatic/result/logger.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
|
||||
func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[A, A] {
|
||||
return func(a A, err error) (A, error) {
|
||||
if err != nil {
|
||||
left("%s: %v", prefix, err)
|
||||
} else {
|
||||
right("%s: %v", prefix, a)
|
||||
}
|
||||
return a, err
|
||||
}
|
||||
}
|
||||
|
||||
// Logger creates a logging function for Either values that logs both Left and Right cases.
|
||||
// The function logs the value and then returns the original Either unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - loggers: Optional log.Logger instances. If none provided, uses default logger.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := either.Logger[error, int]()
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](42),
|
||||
// logger("Processing"),
|
||||
// either.Map(N.Mul(2)),
|
||||
// )
|
||||
// // Logs: "Processing: 42"
|
||||
// // result is Right(84)
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) Operator[A, A] {
|
||||
left, right := L.LoggingCallbacks(loggers...)
|
||||
return func(prefix string) Operator[A, A] {
|
||||
return _log[A](left, right, prefix)
|
||||
}
|
||||
}
|
||||
38
v2/idiomatic/result/logger_test.go
Normal file
38
v2/idiomatic/result/logger_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
|
||||
l := Logger[string]()
|
||||
|
||||
rVal, rErr := Right("test")
|
||||
|
||||
resVal, resErr := Pipe2(
|
||||
"test",
|
||||
Right[string],
|
||||
l("out"),
|
||||
)
|
||||
|
||||
assert.Equal(t, rVal, resVal)
|
||||
assert.Equal(t, rErr, resErr)
|
||||
}
|
||||
59
v2/idiomatic/result/monad.go
Normal file
59
v2/idiomatic/result/monad.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
type (
|
||||
eitherMonad[A, B any] struct{}
|
||||
|
||||
Monad[A, B any] interface {
|
||||
Applicative[A, B]
|
||||
Chainable[A, B]
|
||||
}
|
||||
)
|
||||
|
||||
func (o eitherMonad[A, B]) Of(a A) (A, error) {
|
||||
return Of(a)
|
||||
}
|
||||
|
||||
func (o eitherMonad[A, B]) Map(f func(A) B) Operator[A, B] {
|
||||
return Map(f)
|
||||
}
|
||||
|
||||
func (o eitherMonad[A, B]) Chain(f func(A) (B, error)) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
func (o eitherMonad[A, B]) Ap(a A, err error) Operator[func(A) B, B] {
|
||||
return Ap[B](a, err)
|
||||
}
|
||||
|
||||
// MakeMonad creates a Monad instance for Result operations.
|
||||
// A monad combines the capabilities of Functor (Map), Applicative (Ap), and Chain (flatMap/bind).
|
||||
// This allows for sequential composition of computations that may fail.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// m := result.MakeMonad[int, string]()
|
||||
// val, err := m.Chain(func(x int) (string, error) {
|
||||
// if x > 0 {
|
||||
// return result.Right[error](strconv.Itoa(x))
|
||||
// }
|
||||
// return result.Left[string](errors.New("negative"))
|
||||
// })(result.Right[error](42))
|
||||
// // val is "42", err is nil
|
||||
func MakeMonad[A, B any]() Monad[A, B] {
|
||||
return eitherMonad[A, B]{}
|
||||
}
|
||||
59
v2/idiomatic/result/monoid._go
Normal file
59
v2/idiomatic/result/monoid._go
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
L "github.com/IBM/fp-go/v2/lazy"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// AlternativeMonoid creates a monoid for Either using applicative semantics.
|
||||
// The empty value is Right with the monoid's empty value.
|
||||
// Combines values using applicative operations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/monoid"
|
||||
// intAdd := monoid.MakeMonoid(0, func(a, b int) int { return a + b })
|
||||
// m := either.AlternativeMonoid[error](intAdd)
|
||||
// result := m.Concat(either.Right[error](1), either.Right[error](2))
|
||||
// // result is Right(3)
|
||||
func AlternativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
|
||||
return M.AlternativeMonoid(
|
||||
Of[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadAp[A, E, A],
|
||||
MonadAlt[A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a monoid for Either using the Alt operation.
|
||||
// The empty value is provided as a lazy computation.
|
||||
// When combining, returns the first Right value, or the second if the first is Left.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// zero := func() either.Either[error, int] { return either.Left[int](errors.New("empty")) }
|
||||
// m := either.AltMonoid[error, int](zero)
|
||||
// result := m.Concat(either.Left[int](errors.New("err1")), either.Right[error](42))
|
||||
// // result is Right(42)
|
||||
func AltMonoid[A any](zero L.Lazy[Either[A]]) Monoid[A] {
|
||||
return M.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[A],
|
||||
)
|
||||
}
|
||||
39
v2/idiomatic/result/pointed.go
Normal file
39
v2/idiomatic/result/pointed.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
type (
|
||||
eitherPointed[A any] struct{}
|
||||
|
||||
Pointed[A any] interface {
|
||||
Of(a A) (A, error)
|
||||
}
|
||||
)
|
||||
|
||||
func (o eitherPointed[A]) Of(a A) (A, error) {
|
||||
return Of(a)
|
||||
}
|
||||
|
||||
// Pointed implements the pointed functor operations for Either.
|
||||
// A pointed functor provides the Of operation to lift a value into the Either context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// p := either.Pointed[error, int]()
|
||||
// result := p.Of(42) // Right(42)
|
||||
func MakePointed[A any]() Pointed[A] {
|
||||
return eitherPointed[A]{}
|
||||
}
|
||||
114
v2/idiomatic/result/record.go
Normal file
114
v2/idiomatic/result/record.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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 result
|
||||
|
||||
// TraverseRecordG transforms a map by applying a function that returns an Either to each value.
|
||||
// If any value produces a Left, the entire result is that Left (short-circuits).
|
||||
// Otherwise, returns Right containing the map of all Right values.
|
||||
// The G suffix indicates support for generic map types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parse := func(s string) either.Either[error, int] {
|
||||
// v, err := strconv.Atoi(s)
|
||||
// return either.FromError(v, err)
|
||||
// }
|
||||
// result := either.TraverseRecordG[map[string]string, map[string]int](parse)(map[string]string{"a": "1", "b": "2"})
|
||||
// // result is Right(map[string]int{"a": 1, "b": 2})
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
|
||||
return func(ga GA) (GB, error) {
|
||||
bs := make(GB)
|
||||
for k, a := range ga {
|
||||
b, err := f(a)
|
||||
if err != nil {
|
||||
return Left[GB](err)
|
||||
}
|
||||
bs[k] = b
|
||||
}
|
||||
return Of(bs)
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseRecord transforms a map by applying a function that returns an Either to each value.
|
||||
// If any value produces a Left, the entire result is that Left (short-circuits).
|
||||
// Otherwise, returns Right containing the map of all Right values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parse := func(s string) either.Either[error, int] {
|
||||
// v, err := strconv.Atoi(s)
|
||||
// return either.FromError(v, err)
|
||||
// }
|
||||
// result := either.TraverseRecord[string](parse)(map[string]string{"a": "1", "b": "2"})
|
||||
// // result is Right(map[string]int{"a": 1, "b": 2})
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return TraverseRecordG[map[K]A, map[K]B](f)
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndexG transforms a map by applying an indexed function that returns an Either.
|
||||
// The function receives both the key and the value.
|
||||
// If any value produces a Left, the entire result is that Left (short-circuits).
|
||||
// The G suffix indicates support for generic map types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validate := func(k string, v string) either.Either[error, string] {
|
||||
// if len(v) > 0 {
|
||||
// return either.Right[error](k + ":" + v)
|
||||
// }
|
||||
// return either.Left[string](fmt.Errorf("empty value for key %s", k))
|
||||
// }
|
||||
// result := either.TraverseRecordWithIndexG[map[string]string, map[string]string](validate)(map[string]string{"a": "1"})
|
||||
// // result is Right(map[string]string{"a": "a:1"})
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecordWithIndexG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f func(K, A) (B, error)) Kleisli[GA, GB] {
|
||||
return func(ga GA) (GB, error) {
|
||||
bs := make(GB)
|
||||
for k, a := range ga {
|
||||
b, err := f(k, a)
|
||||
if err != nil {
|
||||
return Left[GB](err)
|
||||
}
|
||||
bs[k] = b
|
||||
}
|
||||
return Of(bs)
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndex transforms a map by applying an indexed function that returns an Either.
|
||||
// The function receives both the key and the value.
|
||||
// If any value produces a Left, the entire result is that Left (short-circuits).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validate := func(k string, v string) either.Either[error, string] {
|
||||
// if len(v) > 0 {
|
||||
// return either.Right[error](k + ":" + v)
|
||||
// }
|
||||
// return either.Left[string](fmt.Errorf("empty value for key %s", k))
|
||||
// }
|
||||
// result := either.TraverseRecordWithIndex[string](validate)(map[string]string{"a": "1"})
|
||||
// // result is Right(map[string]string{"a": "a:1"})
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) (B, error)) Kleisli[map[K]A, map[K]B] {
|
||||
return TraverseRecordWithIndexG[map[K]A, map[K]B](f)
|
||||
}
|
||||
253
v2/idiomatic/result/record_test.go
Normal file
253
v2/idiomatic/result/record_test.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestTraverseRecordG_Success tests successful traversal of a map
|
||||
func TestTraverseRecordG_Success(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
input := map[string]string{"a": "1", "b": "2", "c": "3"}
|
||||
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, result["a"])
|
||||
assert.Equal(t, 2, result["b"])
|
||||
assert.Equal(t, 3, result["c"])
|
||||
}
|
||||
|
||||
// TestTraverseRecordG_Error tests that traversal short-circuits on error
|
||||
func TestTraverseRecordG_Error(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
input := map[string]string{"a": "1", "b": "bad", "c": "3"}
|
||||
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseRecordG_EmptyMap tests traversal of an empty map
|
||||
func TestTraverseRecordG_EmptyMap(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
input := map[string]string{}
|
||||
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
assert.NotNil(t, result) // Should be an empty map, not nil
|
||||
}
|
||||
|
||||
// TestTraverseRecordG_CustomMapType tests with custom map types
|
||||
func TestTraverseRecordG_CustomMapType(t *testing.T) {
|
||||
type StringMap map[string]string
|
||||
type IntMap map[string]int
|
||||
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
input := StringMap{"x": "10", "y": "20"}
|
||||
result, err := TraverseRecordG[StringMap, IntMap](parse)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, IntMap{"x": 10, "y": 20}, result)
|
||||
}
|
||||
|
||||
// TestTraverseRecord_Success tests successful traversal
|
||||
func TestTraverseRecord_Success(t *testing.T) {
|
||||
validate := func(s string) (int, error) {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n < 0 {
|
||||
return 0, errors.New("negative number")
|
||||
}
|
||||
return n * 2, nil
|
||||
}
|
||||
|
||||
input := map[string]string{"a": "1", "b": "2"}
|
||||
result, err := TraverseRecord[string, string, int](validate)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, result["a"])
|
||||
assert.Equal(t, 4, result["b"])
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ValidationError tests validation failure
|
||||
func TestTraverseRecord_ValidationError(t *testing.T) {
|
||||
validate := func(s string) (int, error) {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n < 0 {
|
||||
return 0, errors.New("negative number")
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
input := map[string]string{"a": "1", "b": "-5"}
|
||||
result, err := TraverseRecord[string, string, int](validate)(input)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "negative")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseRecordWithIndexG_Success tests successful indexed traversal
|
||||
func TestTraverseRecordWithIndexG_Success(t *testing.T) {
|
||||
annotate := func(k string, v string) (string, error) {
|
||||
if len(v) == 0 {
|
||||
return "", fmt.Errorf("empty value for key %s", k)
|
||||
}
|
||||
return fmt.Sprintf("%s=%s", k, v), nil
|
||||
}
|
||||
|
||||
input := map[string]string{"a": "1", "b": "2"}
|
||||
result, err := TraverseRecordWithIndexG[map[string]string, map[string]string](annotate)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "a=1", result["a"])
|
||||
assert.Equal(t, "b=2", result["b"])
|
||||
}
|
||||
|
||||
// TestTraverseRecordWithIndexG_Error tests error handling with key
|
||||
func TestTraverseRecordWithIndexG_Error(t *testing.T) {
|
||||
annotate := func(k string, v string) (string, error) {
|
||||
if len(v) == 0 {
|
||||
return "", fmt.Errorf("empty value for key %s", k)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
input := map[string]string{"a": "1", "b": ""}
|
||||
result, err := TraverseRecordWithIndexG[map[string]string, map[string]string](annotate)(input)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "key b")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseRecordWithIndexG_EmptyMap tests empty map
|
||||
func TestTraverseRecordWithIndexG_EmptyMap(t *testing.T) {
|
||||
annotate := func(k string, v string) (string, error) {
|
||||
return k + ":" + v, nil
|
||||
}
|
||||
|
||||
input := map[string]string{}
|
||||
result, err := TraverseRecordWithIndexG[map[string]string, map[string]string](annotate)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseRecordWithIndex_Success tests successful indexed traversal
|
||||
func TestTraverseRecordWithIndex_Success(t *testing.T) {
|
||||
check := func(k string, v int) (string, error) {
|
||||
if v < 0 {
|
||||
return "", fmt.Errorf("negative value for key %s", k)
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", k, v*2), nil
|
||||
}
|
||||
|
||||
input := map[string]int{"a": 1, "b": 2}
|
||||
result, err := TraverseRecordWithIndex[string, int, string](check)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "a:2", result["a"])
|
||||
assert.Equal(t, "b:4", result["b"])
|
||||
}
|
||||
|
||||
// TestTraverseRecordWithIndex_Error tests error with key info
|
||||
func TestTraverseRecordWithIndex_Error(t *testing.T) {
|
||||
check := func(k string, v int) (int, error) {
|
||||
if v < 0 {
|
||||
return 0, fmt.Errorf("negative value for key %s", k)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
input := map[string]int{"ok": 1, "bad": -5}
|
||||
result, err := TraverseRecordWithIndex[string, int, int](check)(input)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "key bad")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
// TestTraverseRecordWithIndex_TypeTransformation tests transforming types with key
|
||||
func TestTraverseRecordWithIndex_TypeTransformation(t *testing.T) {
|
||||
prefixKey := func(k string, v string) (string, error) {
|
||||
return k + "_" + v, nil
|
||||
}
|
||||
|
||||
input := map[string]string{"prefix": "value", "another": "test"}
|
||||
result, err := TraverseRecordWithIndex[string, string, string](prefixKey)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "prefix_value", result["prefix"])
|
||||
assert.Equal(t, "another_test", result["another"])
|
||||
}
|
||||
|
||||
// TestTraverseRecord_IntKeys tests with integer keys
|
||||
func TestTraverseRecord_IntKeys(t *testing.T) {
|
||||
double := func(n int) (int, error) {
|
||||
return n * 2, nil
|
||||
}
|
||||
|
||||
input := map[int]int{1: 10, 2: 20, 3: 30}
|
||||
result, err := TraverseRecord[int, int, int](double)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 20, result[1])
|
||||
assert.Equal(t, 40, result[2])
|
||||
assert.Equal(t, 60, result[3])
|
||||
}
|
||||
|
||||
// TestTraverseRecordG_PreservesKeys tests that keys are preserved
|
||||
func TestTraverseRecordG_PreservesKeys(t *testing.T) {
|
||||
identity := func(s string) (string, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
input := map[string]string{"key1": "val1", "key2": "val2"}
|
||||
result, err := TraverseRecordG[map[string]string, map[string]string](identity)(input)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result, "key1")
|
||||
assert.Contains(t, result, "key2")
|
||||
assert.Equal(t, "val1", result["key1"])
|
||||
assert.Equal(t, "val2", result["key2"])
|
||||
}
|
||||
60
v2/idiomatic/result/resource.go
Normal file
60
v2/idiomatic/result/resource.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 result
|
||||
|
||||
// WithResource constructs a function that creates a resource, operates on it, and then releases it.
|
||||
// This ensures proper resource cleanup even if operations fail.
|
||||
// The resource is released immediately after the operation completes.
|
||||
//
|
||||
// Parameters:
|
||||
// - onCreate: Function to create/acquire the resource
|
||||
// - onRelease: Function to release/cleanup the resource
|
||||
//
|
||||
// Returns a function that takes an operation to perform on the resource.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// withFile := either.WithResource(
|
||||
// func() either.Either[error, *os.File] {
|
||||
// return either.TryCatchError(os.Open("file.txt"))
|
||||
// },
|
||||
// func(f *os.File) either.Either[error, any] {
|
||||
// return either.TryCatchError(f.Close())
|
||||
// },
|
||||
// )
|
||||
// result := withFile(func(f *os.File) either.Either[error, string] {
|
||||
// // Use file here
|
||||
// return either.Right[error]("data")
|
||||
// })
|
||||
func WithResource[R, A, ANY any](onCreate func() (R, error), onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
|
||||
|
||||
return func(f func(R) (A, error)) (A, error) {
|
||||
r, rerr := onCreate()
|
||||
if rerr != nil {
|
||||
return Left[A](rerr)
|
||||
}
|
||||
a, aerr := f(r)
|
||||
_, nerr := onRelease(r)
|
||||
if aerr != nil {
|
||||
return Left[A](aerr)
|
||||
}
|
||||
if nerr != nil {
|
||||
return Left[A](nerr)
|
||||
|
||||
}
|
||||
return Of(a)
|
||||
}
|
||||
}
|
||||
45
v2/idiomatic/result/resource_test.go
Normal file
45
v2/idiomatic/result/resource_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWithResource(t *testing.T) {
|
||||
|
||||
onCreate := func() (*os.File, error) {
|
||||
return os.CreateTemp("", "*")
|
||||
}
|
||||
onDelete := func(f *os.File) (any, error) {
|
||||
return Chain(func(name string) (any, error) {
|
||||
return any(name), os.Remove(name)
|
||||
})(f.Name(), f.Close())
|
||||
}
|
||||
|
||||
onHandler := func(f *os.File) (string, error) {
|
||||
return Of(f.Name())
|
||||
}
|
||||
|
||||
tempFile := WithResource[*os.File, string](onCreate, onDelete)
|
||||
|
||||
res, err := tempFile(onHandler)
|
||||
|
||||
assert.True(t, IsRight(res, err))
|
||||
}
|
||||
38
v2/idiomatic/result/semigroup._go
Normal file
38
v2/idiomatic/result/semigroup._go
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// AltSemigroup creates a semigroup for Either that uses the Alt operation for combining values.
|
||||
// When combining two Either values, it returns the first Right value, or the second value if the first is Left.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// sg := either.AltSemigroup[error, int]()
|
||||
// result := sg.Concat(either.Left[int](errors.New("error")), either.Right[error](42))
|
||||
// // result is Right(42)
|
||||
// result2 := sg.Concat(either.Right[error](1), either.Right[error](2))
|
||||
// // result2 is Right(1) - first Right wins
|
||||
//
|
||||
//go:inline
|
||||
func AltSemigroup[A any]() S.Semigroup[Either[A]] {
|
||||
return S.AltSemigroup(
|
||||
MonadAlt[A],
|
||||
)
|
||||
}
|
||||
103
v2/idiomatic/result/testing/laws._go
Normal file
103
v2/idiomatic/result/testing/laws._go
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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 testing provides utilities for testing Either monad laws.
|
||||
// This is useful for verifying that custom Either implementations satisfy the monad laws.
|
||||
package testing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
L "github.com/IBM/fp-go/v2/internal/monad/testing"
|
||||
)
|
||||
|
||||
// AssertLaws asserts that the Either monad satisfies the monad laws.
|
||||
// This includes testing:
|
||||
// - Identity laws (left and right identity)
|
||||
// - Associativity law
|
||||
// - Functor laws
|
||||
// - Applicative laws
|
||||
//
|
||||
// Parameters:
|
||||
// - t: Testing context
|
||||
// - eqe, eqa, eqb, eqc: Equality predicates for the types
|
||||
// - ab: Function from A to B for testing
|
||||
// - bc: Function from B to C for testing
|
||||
//
|
||||
// Returns a function that takes a value of type A and returns true if all laws hold.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestEitherLaws(t *testing.T) {
|
||||
// eqInt := eq.FromStrictEquals[int]()
|
||||
// eqString := eq.FromStrictEquals[string]()
|
||||
// eqError := eq.FromStrictEquals[error]()
|
||||
//
|
||||
// ab := func(x int) string { return strconv.Itoa(x) }
|
||||
// bc := func(s string) bool { return len(s) > 0 }
|
||||
//
|
||||
// testing.AssertLaws(t, eqError, eqInt, eqString, eq.FromStrictEquals[bool](), ab, bc)(42)
|
||||
// }
|
||||
func AssertLaws[A, B, C any](t *testing.T,
|
||||
eqe EQ.Eq[E],
|
||||
eqa EQ.Eq[A],
|
||||
eqb EQ.Eq[B],
|
||||
eqc EQ.Eq[C],
|
||||
|
||||
ab func(A) B,
|
||||
bc func(B) C,
|
||||
) func(a A) bool {
|
||||
|
||||
return L.AssertLaws(t,
|
||||
ET.Eq(eqe, eqa),
|
||||
ET.Eq(eqe, eqb),
|
||||
ET.Eq(eqe, eqc),
|
||||
|
||||
ET.Of[A],
|
||||
ET.Of[B],
|
||||
ET.Of[C],
|
||||
|
||||
ET.Of[func(A) A],
|
||||
ET.Of[func(A) B],
|
||||
ET.Of[func(B) C],
|
||||
ET.Of[func(func(A) B) B],
|
||||
|
||||
ET.MonadMap[A, A],
|
||||
ET.MonadMap[A, B],
|
||||
ET.MonadMap[A, C],
|
||||
ET.MonadMap[B, C],
|
||||
|
||||
ET.MonadMap[func(B) C, func(func(A) B) func(A) C],
|
||||
|
||||
ET.MonadChain[A, A],
|
||||
ET.MonadChain[A, B],
|
||||
ET.MonadChain[A, C],
|
||||
ET.MonadChain[B, C],
|
||||
|
||||
ET.MonadAp[A, E, A],
|
||||
ET.MonadAp[B, E, A],
|
||||
ET.MonadAp[C, E, B],
|
||||
ET.MonadAp[C, E, A],
|
||||
|
||||
ET.MonadAp[B, E, func(A) B],
|
||||
ET.MonadAp[func(A) C, E, func(A) B],
|
||||
|
||||
ab,
|
||||
bc,
|
||||
)
|
||||
|
||||
}
|
||||
48
v2/idiomatic/result/testing/laws_test._go
Normal file
48
v2/idiomatic/result/testing/laws_test._go
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonadLaws(t *testing.T) {
|
||||
// some comparison
|
||||
eqe := EQ.FromStrictEquals[string]()
|
||||
eqa := EQ.FromStrictEquals[bool]()
|
||||
eqb := EQ.FromStrictEquals[int]()
|
||||
eqc := EQ.FromStrictEquals[string]()
|
||||
|
||||
ab := func(a bool) int {
|
||||
if a {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
bc := func(b int) string {
|
||||
return fmt.Sprintf("value %d", b)
|
||||
}
|
||||
|
||||
laws := AssertLaws(t, eqe, eqa, eqb, eqc, ab, bc)
|
||||
|
||||
assert.True(t, laws(true))
|
||||
assert.True(t, laws(false))
|
||||
}
|
||||
72
v2/idiomatic/result/traverse.go
Normal file
72
v2/idiomatic/result/traverse.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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 result
|
||||
|
||||
// Traverse converts an Either of some higher kinded type into the higher kinded type of an Either.
|
||||
// This is a generic traversal operation that works with any applicative functor.
|
||||
//
|
||||
// Parameters:
|
||||
// - mof: Lifts an Either into the target higher-kinded type
|
||||
// - mmap: Maps over the target higher-kinded type
|
||||
//
|
||||
// Example (conceptual - requires understanding of higher-kinded types):
|
||||
//
|
||||
// // Traverse an Either[error, Option[int]] to Option[Either[error, int]]
|
||||
// result := either.Traverse[int, error, int, option.Option[int], option.Option[either.Either[error, int]]](
|
||||
// option.Of[either.Either[error, int]],
|
||||
// option.Map[int, either.Either[error, int]],
|
||||
// )(f)(eitherOfOption)
|
||||
func Traverse[A, B, HKTB, HKTRB any](
|
||||
mof func(B, error) HKTRB,
|
||||
mmap func(Kleisli[B, B]) func(HKTB) HKTRB,
|
||||
) func(func(A) HKTB) func(A, error) HKTRB {
|
||||
return func(f func(A) HKTB) func(A, error) HKTRB {
|
||||
right := mmap(Right[B])
|
||||
return func(a A, err error) HKTRB {
|
||||
if err != nil {
|
||||
return mof(Left[B](err))
|
||||
}
|
||||
return right(f(a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence converts an Either of some higher kinded type into the higher kinded type of an Either.
|
||||
// This is the identity version of Traverse - it doesn't transform the values, just swaps the type constructors.
|
||||
//
|
||||
// Parameters:
|
||||
// - mof: Lifts an Either into the target higher-kinded type
|
||||
// - mmap: Maps over the target higher-kinded type
|
||||
//
|
||||
// Example (conceptual - requires understanding of higher-kinded types):
|
||||
//
|
||||
// // Sequence an Either[error, Option[int]] to Option[Either[error, int]]
|
||||
// result := either.Sequence[error, int, option.Option[int], option.Option[either.Either[error, int]]](
|
||||
// option.Of[either.Either[error, int]],
|
||||
// option.Map[int, either.Either[error, int]],
|
||||
// )(eitherOfOption)
|
||||
func Sequence[A, HKTA, HKTRA any](
|
||||
mof func(A, error) HKTRA,
|
||||
mmap func(Kleisli[A, A]) func(HKTA) HKTRA,
|
||||
) func(hkta HKTA, err error) HKTRA {
|
||||
right := mmap(Right[A])
|
||||
return func(hkta HKTA, err error) HKTRA {
|
||||
if err != nil {
|
||||
return mof(Left[A](err))
|
||||
}
|
||||
return right(hkta)
|
||||
}
|
||||
}
|
||||
33
v2/idiomatic/result/types.go
Normal file
33
v2/idiomatic/result/types.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Either and Option together.
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
|
||||
Kleisli[A, B any] = func(A) (B, error)
|
||||
Operator[A, B any] = func(A, error) (B, error)
|
||||
)
|
||||
103
v2/idiomatic/result/validation.go
Normal file
103
v2/idiomatic/result/validation.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
import (
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// ApV applies a function wrapped in a Result to a value wrapped in a Result,
|
||||
// accumulating errors using a semigroup instead of short-circuiting.
|
||||
//
|
||||
// This function is designed for validation scenarios where you want to collect
|
||||
// all validation errors rather than stopping at the first error. It differs
|
||||
// from the standard [Ap] function in that it combines errors from both the
|
||||
// function and the value using the provided semigroup operation.
|
||||
//
|
||||
// The function works as follows:
|
||||
// - If both the value and the function have errors, it combines them using the semigroup
|
||||
// - If only one has an error, it returns that error
|
||||
// - If neither has an error, it applies the function to the value
|
||||
//
|
||||
// Type Parameters:
|
||||
// - B: The result type after applying the function
|
||||
// - A: The input type to the function
|
||||
//
|
||||
// Parameters:
|
||||
// - sg: A semigroup that defines how to combine two error values. The semigroup's
|
||||
// Concat operation determines how errors are accumulated (e.g., concatenating
|
||||
// error messages, merging error lists, etc.)
|
||||
//
|
||||
// Returns:
|
||||
// - A curried function that takes a value (A, error), then takes a function
|
||||
// (func(A) B, error), and returns the result (B, error) with accumulated errors
|
||||
//
|
||||
// Behavior:
|
||||
// - Right + Right: Applies the function to the value and returns Right(result)
|
||||
// - Right + Left: Returns the Left error from the function
|
||||
// - Left + Right: Returns the Left error from the value
|
||||
// - Left + Left: Returns Left(sg.Concat(function_error, value_error))
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "errors"
|
||||
// "fmt"
|
||||
// "strings"
|
||||
// S "github.com/IBM/fp-go/v2/semigroup"
|
||||
// "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
// )
|
||||
//
|
||||
// // Create a semigroup that combines errors by concatenating their messages
|
||||
// errorSemigroup := S.MakeSemigroup(func(e1, e2 error) error {
|
||||
// return fmt.Errorf("%v; %v", e1, e2)
|
||||
// })
|
||||
//
|
||||
// // ApV with both function and value having errors
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// apv := result.ApV[int, int](errorSemigroup)
|
||||
//
|
||||
// value := result.Left[int](errors.New("invalid value"))
|
||||
// fn := result.Left[func(int) int](errors.New("invalid function"))
|
||||
//
|
||||
// result := apv(value)(fn)
|
||||
// // Left(error: "invalid function; invalid value")
|
||||
//
|
||||
// // ApV with successful application
|
||||
// goodValue, _ := result.Right(5)
|
||||
// goodFn, _ := result.Right(double)
|
||||
// result2 := apv(goodValue)(goodFn)
|
||||
// // Right(10)
|
||||
func ApV[B, A any](sg S.Semigroup[error]) func(A, error) Operator[func(A) B, B] {
|
||||
return func(a A, aerr error) Operator[func(A) B, B] {
|
||||
return func(fab func(A) B, faberr error) (B, error) {
|
||||
// Both have errors: combine them using the semigroup
|
||||
if aerr != nil {
|
||||
if faberr != nil {
|
||||
return Left[B](sg.Concat(faberr, aerr))
|
||||
}
|
||||
// Only value has error
|
||||
return Left[B](aerr)
|
||||
}
|
||||
// Only function has error
|
||||
if faberr != nil {
|
||||
return Left[B](faberr)
|
||||
}
|
||||
// Both are successful: apply function to value
|
||||
return Of(fab(a))
|
||||
}
|
||||
}
|
||||
}
|
||||
373
v2/idiomatic/result/validation_test.go
Normal file
373
v2/idiomatic/result/validation_test.go
Normal file
@@ -0,0 +1,373 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper function to create a semigroup that concatenates error messages
|
||||
func makeErrorConcatSemigroup() S.Semigroup[error] {
|
||||
return S.MakeSemigroup(func(e1, e2 error) error {
|
||||
return fmt.Errorf("%v; %v", e1, e2)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to create a semigroup that collects error messages in a slice
|
||||
func makeErrorListSemigroup() S.Semigroup[error] {
|
||||
return S.MakeSemigroup(func(e1, e2 error) error {
|
||||
msg1 := e1.Error()
|
||||
msg2 := e2.Error()
|
||||
|
||||
// Parse existing lists
|
||||
var msgs []string
|
||||
if strings.HasPrefix(msg1, "[") && strings.HasSuffix(msg1, "]") {
|
||||
trimmed := strings.Trim(msg1, "[]")
|
||||
if trimmed != "" {
|
||||
msgs = strings.Split(trimmed, ", ")
|
||||
}
|
||||
} else {
|
||||
msgs = []string{msg1}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(msg2, "[") && strings.HasSuffix(msg2, "]") {
|
||||
trimmed := strings.Trim(msg2, "[]")
|
||||
if trimmed != "" {
|
||||
msgs = append(msgs, strings.Split(trimmed, ", ")...)
|
||||
}
|
||||
} else {
|
||||
msgs = append(msgs, msg2)
|
||||
}
|
||||
|
||||
return fmt.Errorf("[%s]", strings.Join(msgs, ", "))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApV_BothRight tests ApV when both the value and function are Right
|
||||
func TestApV_BothRight(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
value, verr := Right(5)
|
||||
fn, ferr := Right(double)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
// TestApV_ValueLeft_FunctionRight tests ApV when value is Left and function is Right
|
||||
func TestApV_ValueLeft_FunctionRight(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
valueError := errors.New("invalid value")
|
||||
value, verr := Left[int](valueError)
|
||||
fn, ferr := Right(double)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, valueError, err)
|
||||
assert.Equal(t, 0, result) // zero value for int
|
||||
}
|
||||
|
||||
// TestApV_ValueRight_FunctionLeft tests ApV when value is Right and function is Left
|
||||
func TestApV_ValueRight_FunctionLeft(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
fnError := errors.New("invalid function")
|
||||
value, verr := Right(5)
|
||||
fn, ferr := Left[func(int) int](fnError)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, fnError, err)
|
||||
assert.Equal(t, 0, result) // zero value for int
|
||||
}
|
||||
|
||||
// TestApV_BothLeft tests ApV when both value and function are Left
|
||||
func TestApV_BothLeft(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
valueError := errors.New("invalid value")
|
||||
fnError := errors.New("invalid function")
|
||||
|
||||
value, verr := Left[int](valueError)
|
||||
fn, ferr := Left[func(int) int](fnError)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, result) // zero value for int
|
||||
|
||||
// Verify the error message contains both errors
|
||||
expectedMsg := "invalid function; invalid value"
|
||||
assert.Equal(t, expectedMsg, err.Error())
|
||||
}
|
||||
|
||||
// TestApV_BothLeft_WithListSemigroup tests error accumulation with a list semigroup
|
||||
func TestApV_BothLeft_WithListSemigroup(t *testing.T) {
|
||||
sg := makeErrorListSemigroup()
|
||||
apv := ApV[string, string](sg)
|
||||
|
||||
valueError := errors.New("error1")
|
||||
fnError := errors.New("error2")
|
||||
|
||||
value, verr := Left[string](valueError)
|
||||
fn, ferr := Left[func(string) string](fnError)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", result) // zero value for string
|
||||
|
||||
// Verify both errors are in the list
|
||||
expectedMsg := "[error2, error1]"
|
||||
assert.Equal(t, expectedMsg, err.Error())
|
||||
}
|
||||
|
||||
// TestApV_StringTransformation tests ApV with string transformation
|
||||
func TestApV_StringTransformation(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[string, string](sg)
|
||||
|
||||
toUpper := func(s string) string { return strings.ToUpper(s) }
|
||||
|
||||
value, verr := Right("hello")
|
||||
fn, ferr := Right(toUpper)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "HELLO", result)
|
||||
}
|
||||
|
||||
// TestApV_DifferentTypes tests ApV with different input and output types
|
||||
func TestApV_DifferentTypes(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[string, int](sg)
|
||||
|
||||
intToString := func(x int) string { return fmt.Sprintf("Number: %d", x) }
|
||||
|
||||
value, verr := Right(42)
|
||||
fn, ferr := Right(intToString)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Number: 42", result)
|
||||
}
|
||||
|
||||
// TestApV_ComplexType tests ApV with complex types (structs)
|
||||
func TestApV_ComplexType(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[string, Person](sg)
|
||||
|
||||
getName := func(p Person) string { return p.Name }
|
||||
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
value, verr := Right(person)
|
||||
fn, ferr := Right(getName)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result)
|
||||
}
|
||||
|
||||
// TestApV_MultipleValidations demonstrates chaining multiple validations
|
||||
func TestApV_MultipleValidations(t *testing.T) {
|
||||
_ = makeErrorListSemigroup() // Semigroup available for future use
|
||||
|
||||
// Validation functions
|
||||
validatePositive := func(x int) (int, error) {
|
||||
if x > 0 {
|
||||
return Right(x)
|
||||
}
|
||||
return Left[int](errors.New("must be positive"))
|
||||
}
|
||||
|
||||
validateEven := func(x int) (int, error) {
|
||||
if x%2 == 0 {
|
||||
return Right(x)
|
||||
}
|
||||
return Left[int](errors.New("must be even"))
|
||||
}
|
||||
|
||||
// Test valid value (positive and even)
|
||||
t.Run("valid value", func(t *testing.T) {
|
||||
value, err := Right(4)
|
||||
validatedPositive, err1 := validatePositive(value)
|
||||
validatedEven, err2 := validateEven(validatedPositive)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, 4, validatedEven)
|
||||
})
|
||||
|
||||
// Test invalid value (negative)
|
||||
t.Run("negative value", func(t *testing.T) {
|
||||
value, _ := Right(-3)
|
||||
_, err := validatePositive(value)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "must be positive", err.Error())
|
||||
})
|
||||
|
||||
// Test invalid value (odd)
|
||||
t.Run("odd value", func(t *testing.T) {
|
||||
value, _ := Right(3)
|
||||
validatedPositive, err1 := validatePositive(value)
|
||||
_, err2 := validateEven(validatedPositive)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "must be even", err2.Error())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApV_ZeroValues tests ApV with zero values
|
||||
func TestApV_ZeroValues(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
identity := func(x int) int { return x }
|
||||
|
||||
value, verr := Right(0)
|
||||
fn, ferr := Right(identity)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestApV_NilError tests that nil errors are handled correctly
|
||||
func TestApV_NilError(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[string, string](sg)
|
||||
|
||||
identity := func(s string) string { return s }
|
||||
|
||||
// Right is equivalent to (value, nil)
|
||||
value, verr := Right("test")
|
||||
fn, ferr := Right(identity)
|
||||
|
||||
assert.Nil(t, verr)
|
||||
assert.Nil(t, ferr)
|
||||
|
||||
result, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", result)
|
||||
}
|
||||
|
||||
// TestApV_SemigroupAssociativity tests that error combination is associative
|
||||
func TestApV_SemigroupAssociativity(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
|
||||
e1 := errors.New("error1")
|
||||
e2 := errors.New("error2")
|
||||
e3 := errors.New("error3")
|
||||
|
||||
// (e1 + e2) + e3
|
||||
left := sg.Concat(sg.Concat(e1, e2), e3)
|
||||
// e1 + (e2 + e3)
|
||||
right := sg.Concat(e1, sg.Concat(e2, e3))
|
||||
|
||||
assert.Equal(t, left.Error(), right.Error())
|
||||
}
|
||||
|
||||
// TestApV_CustomSemigroup tests ApV with a custom semigroup
|
||||
func TestApV_CustomSemigroup(t *testing.T) {
|
||||
// Custom semigroup that counts errors
|
||||
type ErrorCount struct {
|
||||
count int
|
||||
msg string
|
||||
}
|
||||
|
||||
countSemigroup := S.MakeSemigroup(func(e1, e2 error) error {
|
||||
// Simple counter in error message
|
||||
return fmt.Errorf("combined: %v | %v", e1, e2)
|
||||
})
|
||||
|
||||
apv := ApV[int, int](countSemigroup)
|
||||
|
||||
e1 := errors.New("first")
|
||||
e2 := errors.New("second")
|
||||
|
||||
value, verr := Left[int](e1)
|
||||
fn, ferr := Left[func(int) int](e2)
|
||||
|
||||
_, err := apv(value, verr)(fn, ferr)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "first")
|
||||
assert.Contains(t, err.Error(), "second")
|
||||
}
|
||||
|
||||
// BenchmarkApV_BothRight benchmarks the happy path
|
||||
func BenchmarkApV_BothRight(b *testing.B) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
value, verr := Right(5)
|
||||
fn, ferr := Right(double)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
apv(value, verr)(fn, ferr)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkApV_BothLeft benchmarks the error accumulation path
|
||||
func BenchmarkApV_BothLeft(b *testing.B) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
valueError := errors.New("value error")
|
||||
fnError := errors.New("function error")
|
||||
|
||||
value, verr := Left[int](valueError)
|
||||
fn, ferr := Left[func(int) int](fnError)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
apv(value, verr)(fn, ferr)
|
||||
}
|
||||
}
|
||||
96
v2/idiomatic/result/variadic._go
Normal file
96
v2/idiomatic/result/variadic._go
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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 result
|
||||
|
||||
// Variadic0 converts a function taking a slice and returning (R, error) into a variadic function returning Either.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// sum := func(nums []int) (int, error) {
|
||||
// total := 0
|
||||
// for _, n := range nums { total += n }
|
||||
// return total, nil
|
||||
// }
|
||||
// variadicSum := either.Variadic0(sum)
|
||||
// result := variadicSum(1, 2, 3) // Right(6)
|
||||
func Variadic0[V, R any](f func([]V) (R, error)) func(...V) Either[error, R] {
|
||||
return func(v ...V) Either[error, R] {
|
||||
return TryCatchError(f(v))
|
||||
}
|
||||
}
|
||||
|
||||
// Variadic1 converts a function with 1 fixed parameter and a slice into a variadic function returning Either.
|
||||
func Variadic1[T1, V, R any](f func(T1, []V) (R, error)) func(T1, ...V) Either[error, R] {
|
||||
return func(t1 T1, v ...V) Either[error, R] {
|
||||
return TryCatchError(f(t1, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Variadic2 converts a function with 2 fixed parameters and a slice into a variadic function returning Either.
|
||||
func Variadic2[T1, T2, V, R any](f func(T1, T2, []V) (R, error)) func(T1, T2, ...V) Either[error, R] {
|
||||
return func(t1 T1, t2 T2, v ...V) Either[error, R] {
|
||||
return TryCatchError(f(t1, t2, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Variadic3 converts a function with 3 fixed parameters and a slice into a variadic function returning Either.
|
||||
func Variadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, []V) (R, error)) func(T1, T2, T3, ...V) Either[error, R] {
|
||||
return func(t1 T1, t2 T2, t3 T3, v ...V) Either[error, R] {
|
||||
return TryCatchError(f(t1, t2, t3, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Variadic4 converts a function with 4 fixed parameters and a slice into a variadic function returning Either.
|
||||
func Variadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, []V) (R, error)) func(T1, T2, T3, T4, ...V) Either[error, R] {
|
||||
return func(t1 T1, t2 T2, t3 T3, t4 T4, v ...V) Either[error, R] {
|
||||
return TryCatchError(f(t1, t2, t3, t4, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Unvariadic0 converts a variadic function returning (R, error) into a function taking a slice and returning Either.
|
||||
func Unvariadic0[V, R any](f func(...V) (R, error)) func([]V) Either[error, R] {
|
||||
return func(v []V) Either[error, R] {
|
||||
return TryCatchError(f(v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Unvariadic1 converts a variadic function with 1 fixed parameter into a function taking a slice and returning Either.
|
||||
func Unvariadic1[T1, V, R any](f func(T1, ...V) (R, error)) func(T1, []V) Either[error, R] {
|
||||
return func(t1 T1, v []V) Either[error, R] {
|
||||
return TryCatchError(f(t1, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Unvariadic2 converts a variadic function with 2 fixed parameters into a function taking a slice and returning Either.
|
||||
func Unvariadic2[T1, T2, V, R any](f func(T1, T2, ...V) (R, error)) func(T1, T2, []V) Either[error, R] {
|
||||
return func(t1 T1, t2 T2, v []V) Either[error, R] {
|
||||
return TryCatchError(f(t1, t2, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Unvariadic3 converts a variadic function with 3 fixed parameters into a function taking a slice and returning Either.
|
||||
func Unvariadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, ...V) (R, error)) func(T1, T2, T3, []V) Either[error, R] {
|
||||
return func(t1 T1, t2 T2, t3 T3, v []V) Either[error, R] {
|
||||
return TryCatchError(f(t1, t2, t3, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Unvariadic4 converts a variadic function with 4 fixed parameters into a function taking a slice and returning Either.
|
||||
func Unvariadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, ...V) (R, error)) func(T1, T2, T3, T4, []V) Either[error, R] {
|
||||
return func(t1 T1, t2 T2, t3 T3, t4 T4, v []V) Either[error, R] {
|
||||
return TryCatchError(f(t1, t2, t3, t4, v...))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user