mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-07 23:03:15 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8ab6b0ce5 | ||
|
|
4e9998b645 | ||
|
|
2ea9e292e1 | ||
|
|
12a20e30d1 | ||
|
|
4909ad5473 | ||
|
|
d116317cde |
67
v2/coverage.txt
Normal file
67
v2/coverage.txt
Normal file
@@ -0,0 +1,67 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/readerresult/array.go:33.74,35.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/array.go:49.98,51.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/array.go:68.76,70.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:42.22,44.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:93.49,95.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:103.49,105.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:113.49,115.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:122.22,124.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:172.49,174.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:211.21,213.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:252.21,254.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:288.21,290.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:321.21,323.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:35.64,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:46.81,48.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:58.98,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:69.115,71.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:81.83,83.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:92.100,94.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:103.117,105.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/from.go:33.70,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/from.go:45.80,47.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/from.go:57.92,59.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/from.go:69.104,71.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/monoid.go:37.62,45.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/monoid.go:64.70,69.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/monoid.go:91.62,98.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:41.59,43.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:49.59,51.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:61.63,63.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:73.66,75.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:85.49,87.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:98.46,100.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:106.62,108.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:120.83,122.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:133.54,135.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:147.92,149.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:160.63,162.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:173.43,175.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:189.101,191.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:197.71,199.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:215.89,217.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:234.131,236.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:249.100,251.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:265.70,267.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:282.81,289.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:303.38,305.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:317.56,319.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:330.103,337.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:348.74,354.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:367.97,369.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:381.84,383.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:395.108,397.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:409.79,411.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:426.88,428.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:440.61,442.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:453.85,455.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:460.55,462.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:473.94,475.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:486.65,488.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:494.103,502.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:508.71,515.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/sequence.go:35.78,40.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/sequence.go:54.35,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/sequence.go:75.38,82.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/sequence.go:95.41,103.2 1 1
|
||||
94
v2/idiomatic/readerresult/array.go
Normal file
94
v2/idiomatic/readerresult/array.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.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
)
|
||||
|
||||
// TraverseArray applies a ReaderResult-returning function to each element of an array,
|
||||
// collecting the results. If any element fails, the entire operation fails with the first error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
|
||||
// ids := []int{1, 2, 3}
|
||||
// result := readerresult.TraverseArray[DB](parseUser)(ids)
|
||||
// // result(db) returns ([]User, nil) with all users or (nil, error) on first error
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[R, []B],
|
||||
Map[R, []B, func(B) []B],
|
||||
Ap[[]B, R, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseArray[R, A, B any](as []A, f Kleisli[R, A, B]) ReaderResult[R, []B] {
|
||||
return array.MonadTraverse[[]A](
|
||||
Of[R, []B],
|
||||
Map[R, []B, func(B) []B],
|
||||
Ap[[]B, R, B],
|
||||
as,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex is like TraverseArray but the function also receives the element's index.
|
||||
// This is useful when the transformation depends on the position in the array.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// processItem := func(idx int, item string) readerresult.ReaderResult[Config, int] {
|
||||
// return readerresult.Of[Config](idx + len(item))
|
||||
// }
|
||||
// items := []string{"a", "bb", "ccc"}
|
||||
// result := readerresult.TraverseArrayWithIndex[Config](processItem)(items)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArrayWithIndex[R, A, B any](f func(int, A) ReaderResult[R, B]) Kleisli[R, []A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[R, []B],
|
||||
Map[R, []B, func(B) []B],
|
||||
Ap[[]B, R, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArray converts an array of ReaderResult values into a single ReaderResult of an array.
|
||||
// If any element fails, the entire operation fails with the first error encountered.
|
||||
// All computations share the same environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readers := []readerresult.ReaderResult[Config, int]{
|
||||
// readerresult.Of[Config](1),
|
||||
// readerresult.Of[Config](2),
|
||||
// readerresult.Of[Config](3),
|
||||
// }
|
||||
// result := readerresult.SequenceArray(readers)
|
||||
// // result(cfg) returns ([]int{1, 2, 3}, nil)
|
||||
//
|
||||
//go:inline
|
||||
func SequenceArray[R, A any](ma []ReaderResult[R, A]) ReaderResult[R, []A] {
|
||||
return MonadTraverseArray(ma, F.Identity[ReaderResult[R, A]])
|
||||
}
|
||||
42
v2/idiomatic/readerresult/array_test.go
Normal file
42
v2/idiomatic/readerresult/array_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
|
||||
n := 10
|
||||
|
||||
readers := A.MakeBy(n, Of[context.Context, int])
|
||||
exp := A.MakeBy(n, F.Identity[int])
|
||||
|
||||
g := F.Pipe1(
|
||||
readers,
|
||||
SequenceArray[context.Context, int],
|
||||
)
|
||||
|
||||
v, err := g(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, exp, v)
|
||||
}
|
||||
283
v2/idiomatic/readerresult/benchmark_test.go
Normal file
283
v2/idiomatic/readerresult/benchmark_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
type BenchContext struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
// Benchmark basic operations
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Of[BenchContext](i)
|
||||
_, _ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := testError
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Left[BenchContext, int](err)
|
||||
_, _ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
double := func(x int) int { return x * 2 }
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := F.Pipe1(rr, Map[BenchContext](double))
|
||||
_, _ = mapped(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
double := func(x int) int { return x * 2 }
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
chained := F.Pipe1(rr, Chain(addOne))
|
||||
_, _ = chained(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainDeep(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAp(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
fab := Of[BenchContext](func(x int) int { return x * 2 })
|
||||
fa := Of[BenchContext](21)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := MonadAp(fab, fa)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT2(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT2(rr1, rr2)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT4(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
rr3 := Of[BenchContext](30)
|
||||
rr4 := Of[BenchContext](40)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT4(rr1, rr2, rr3, rr4)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDoNotation(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
|
||||
type State struct {
|
||||
A int
|
||||
B int
|
||||
C int
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
Bind(
|
||||
func(a int) func(State) State {
|
||||
return func(s State) State { s.A = a; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](10)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(b int) func(State) State {
|
||||
return func(s State) State { s.B = b; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A * 2)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(c int) func(State) State {
|
||||
return func(s State) State { s.C = c; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A + s.B)
|
||||
},
|
||||
),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErrorPropagation(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := testError
|
||||
rr := Left[BenchContext, int](err)
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
kleisli := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x * 2)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
traversed := TraverseArray[BenchContext](kleisli)
|
||||
result := traversed(arr)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []ReaderResult[BenchContext, int]{
|
||||
Of[BenchContext](1),
|
||||
Of[BenchContext](2),
|
||||
Of[BenchContext](3),
|
||||
Of[BenchContext](4),
|
||||
Of[BenchContext](5),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceArray(arr)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Real-world scenario benchmarks
|
||||
|
||||
func BenchmarkRealWorldPipeline(b *testing.B) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
Offset int
|
||||
}
|
||||
|
||||
ctx := Config{Multiplier: 5, Offset: 10}
|
||||
|
||||
type State struct {
|
||||
Input int
|
||||
Result int
|
||||
}
|
||||
|
||||
getMultiplier := func(cfg Config) int { return cfg.Multiplier }
|
||||
getOffset := func(cfg Config) int { return cfg.Offset }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
step1 := Bind(
|
||||
func(m int) func(State) State {
|
||||
return func(s State) State { s.Result = s.Input * m; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getMultiplier)
|
||||
},
|
||||
)
|
||||
step2 := Bind(
|
||||
func(off int) func(State) State {
|
||||
return func(s State) State { s.Result += off; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getOffset)
|
||||
},
|
||||
)
|
||||
result := F.Pipe3(
|
||||
Do[Config](State{Input: 10}),
|
||||
step1,
|
||||
step2,
|
||||
Map[Config](func(s State) int { return s.Result }),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
664
v2/idiomatic/readerresult/bind.go
Normal file
664
v2/idiomatic/readerresult/bind.go
Normal file
@@ -0,0 +1,664 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
AP "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
FE "github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
FR "github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
FC "github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
// result := readereither.Do[Env, error](State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[R, S any](
|
||||
empty S,
|
||||
) ReaderResult[R, S] {
|
||||
return Of[R](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the shared environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderResult[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// readereither.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderResult[Env, error, Config] {
|
||||
// // This can access s.User from the previous step
|
||||
// return readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfigForUser(s.User.ID)
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[R, S1, S2] {
|
||||
return FC.Let(
|
||||
Map[R, S1, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
return FC.LetTo(
|
||||
Map[R, S1, S2],
|
||||
setter,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[R, T, S1] {
|
||||
return C.BindTo(
|
||||
Map[R, T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// readereither.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderResult[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return AP.ApS(
|
||||
Ap[S2, R, T],
|
||||
Map[R, S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
// result := F.Pipe2(
|
||||
// readereither.Of[Env, error](State{}),
|
||||
// readereither.ApSL(configLens, getConfig),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderResult[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a ReaderEither computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.BindL(userLens, func(user User) readereither.ReaderResult[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[R, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a ReaderEither).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
|
||||
// readereither.LetL(configLens, func(cfg Config) Config {
|
||||
// cfg.Port = 8080
|
||||
// return cfg
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[R, S, S] {
|
||||
return Let[R](lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// newConfig := Config{Host: "localhost", Port: 8080}
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[any, error](State{}),
|
||||
// readereither.LetToL(configLens, newConfig),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[R, S, S] {
|
||||
return LetTo[R](lens.Set, b)
|
||||
}
|
||||
|
||||
// BindReaderK lifts a Reader Kleisli arrow into a ReaderResult context and binds it to the state.
|
||||
// This allows you to integrate pure Reader computations (that don't have error handling)
|
||||
// into a ReaderResult computation chain.
|
||||
//
|
||||
// The function f takes the current state S1 and returns a Reader[R, T] computation.
|
||||
// The result T is then attached to the state using the setter to produce state S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// ConfigPath string
|
||||
// }
|
||||
// type State struct {
|
||||
// Config string
|
||||
// }
|
||||
//
|
||||
// // A pure Reader computation that reads from environment
|
||||
// getConfigPath := func(s State) reader.Reader[Env, string] {
|
||||
// return func(env Env) string {
|
||||
// return env.ConfigPath
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.BindReaderK(
|
||||
// func(path string) func(State) State {
|
||||
// return func(s State) State { s.Config = path; return s }
|
||||
// },
|
||||
// getConfigPath,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f reader.Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return FR.BindReaderK(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
FromReader[R, T],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindEitherK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f RES.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return FE.BindEitherK(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
FromEither[R, T],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// BindResultK lifts a Result Kleisli arrow into a ReaderResult context and binds it to the state.
|
||||
// This allows you to integrate Result computations (that may fail with an error but don't need
|
||||
// environment access) into a ReaderResult computation chain.
|
||||
//
|
||||
// The function f takes the current state S1 and returns a Result[T] computation.
|
||||
// If the Result is successful, the value T is attached to the state using the setter to produce state S2.
|
||||
// If the Result is an error, the entire computation short-circuits with that error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// ParsedValue int
|
||||
// }
|
||||
//
|
||||
// // A Result computation that may fail
|
||||
// parseValue := func(s State) result.Result[int] {
|
||||
// if s.Value < 0 {
|
||||
// return result.Error[int](errors.New("negative value"))
|
||||
// }
|
||||
// return result.Of(s.Value * 2)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[any](State{Value: 5}),
|
||||
// readerresult.BindResultK(
|
||||
// func(parsed int) func(State) State {
|
||||
// return func(s State) State { s.ParsedValue = parsed; return s }
|
||||
// },
|
||||
// parseValue,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindResultK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f result.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
setter,
|
||||
func(s1 S1) ReaderResult[R, T] {
|
||||
return FromResult[R](f(s1))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// BindToReader initializes a new state S1 from a Reader[R, T] computation.
|
||||
// This is used to start a ReaderResult computation chain from a pure Reader value.
|
||||
//
|
||||
// The setter function takes the result T from the Reader and initializes the state S1.
|
||||
// This is useful when you want to begin a do-notation chain with a Reader computation
|
||||
// that doesn't involve error handling.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// ConfigPath string
|
||||
// }
|
||||
// type State struct {
|
||||
// Config string
|
||||
// }
|
||||
//
|
||||
// // A Reader that just reads from the environment
|
||||
// getConfigPath := func(env Env) string {
|
||||
// return env.ConfigPath
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// reader.Of[Env](getConfigPath),
|
||||
// readerresult.BindToReader(func(path string) State {
|
||||
// return State{Config: path}
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindToReader[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Reader[R, T]) ReaderResult[R, S1] {
|
||||
return function.Flow2(
|
||||
FromReader[R],
|
||||
BindTo[R](setter),
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToEither[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Result[T]) ReaderResult[R, S1] {
|
||||
return function.Flow2(
|
||||
FromEither[R],
|
||||
BindTo[R](setter),
|
||||
)
|
||||
}
|
||||
|
||||
// BindToResult initializes a new state S1 from a Result[T] value.
|
||||
// This is used to start a ReaderResult computation chain from a Result that may contain an error.
|
||||
//
|
||||
// The setter function takes the successful result T and initializes the state S1.
|
||||
// If the Result is an error, the entire computation will carry that error forward.
|
||||
// This is useful when you want to begin a do-notation chain with a Result computation
|
||||
// that doesn't need environment access.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// // A Result that might contain an error
|
||||
// parseResult := result.TryCatch(func() int {
|
||||
// // some parsing logic that might fail
|
||||
// return 42
|
||||
// })
|
||||
//
|
||||
// computation := F.Pipe1(
|
||||
// parseResult,
|
||||
// readerresult.BindToResult[any](func(value int) State {
|
||||
// return State{Value: value}
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindToResult[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(T, error) ReaderResult[R, S1] {
|
||||
bt := BindTo[R](setter)
|
||||
return func(t T, err error) ReaderResult[R, S1] {
|
||||
return bt(FromResult[R](t, err))
|
||||
}
|
||||
}
|
||||
|
||||
// ApReaderS attaches a value from a pure Reader computation to a context [S1] to produce a context [S2]
|
||||
// using Applicative semantics (independent, non-sequential composition).
|
||||
//
|
||||
// Unlike BindReaderK which uses monadic bind (sequential), ApReaderS uses applicative apply,
|
||||
// meaning the Reader computation fa is independent of the current state and can conceptually
|
||||
// execute in parallel.
|
||||
//
|
||||
// This is useful when you want to combine a Reader computation with your ReaderResult state
|
||||
// without creating a dependency between them.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// DefaultPort int
|
||||
// DefaultHost string
|
||||
// }
|
||||
// type State struct {
|
||||
// Port int
|
||||
// Host string
|
||||
// }
|
||||
//
|
||||
// getDefaultPort := func(env Env) int { return env.DefaultPort }
|
||||
// getDefaultHost := func(env Env) string { return env.DefaultHost }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.ApReaderS(
|
||||
// func(port int) func(State) State {
|
||||
// return func(s State) State { s.Port = port; return s }
|
||||
// },
|
||||
// getDefaultPort,
|
||||
// ),
|
||||
// readerresult.ApReaderS(
|
||||
// func(host string) func(State) State {
|
||||
// return func(s State) State { s.Host = host; return s }
|
||||
// },
|
||||
// getDefaultHost,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(
|
||||
setter,
|
||||
FromReader[R](fa),
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApResultS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
) func(T, error) Operator[R, S1, S2] {
|
||||
return func(t T, err error) Operator[R, S1, S2] {
|
||||
return ApS(
|
||||
setter,
|
||||
FromResult[R](t, err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ApResultS attaches a value from a Result to a context [S1] to produce a context [S2]
|
||||
// using Applicative semantics (independent, non-sequential composition).
|
||||
//
|
||||
// Unlike BindResultK which uses monadic bind (sequential), ApResultS uses applicative apply,
|
||||
// meaning the Result computation fa is independent of the current state and can conceptually
|
||||
// execute in parallel.
|
||||
//
|
||||
// If the Result fa contains an error, the entire computation short-circuits with that error.
|
||||
// This is useful when you want to combine a Result value with your ReaderResult state
|
||||
// without creating a dependency between them.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value1 int
|
||||
// Value2 int
|
||||
// }
|
||||
//
|
||||
// // Independent Result computations
|
||||
// parseValue1 := result.TryCatch(func() int { return 42 })
|
||||
// parseValue2 := result.TryCatch(func() int { return 100 })
|
||||
//
|
||||
// computation := F.Pipe2(
|
||||
// readerresult.Do[any](State{}),
|
||||
// readerresult.ApResultS(
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value1 = v; return s }
|
||||
// },
|
||||
// parseValue1,
|
||||
// ),
|
||||
// readerresult.ApResultS(
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value2 = v; return s }
|
||||
// },
|
||||
// parseValue2,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Result[T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(
|
||||
setter,
|
||||
FromEither[R](fa),
|
||||
)
|
||||
}
|
||||
338
v2/idiomatic/readerresult/bind_test.go
Normal file
338
v2/idiomatic/readerresult/bind_test.go
Normal file
@@ -0,0 +1,338 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getLastName(s utils.Initial) ReaderResult[context.Context, string] {
|
||||
return Of[context.Context]("Doe")
|
||||
}
|
||||
|
||||
func getGivenName(s utils.WithLastName) ReaderResult[context.Context, string] {
|
||||
return Of[context.Context]("John")
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
Bind(utils.SetLastName, getLastName),
|
||||
Bind(utils.SetGivenName, getGivenName),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
v, err := res(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "John Doe", v)
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
ApS(utils.SetLastName, Of[context.Context]("Doe")),
|
||||
ApS(utils.SetGivenName, Of[context.Context]("John")),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
v, err := res(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "John Doe", v)
|
||||
}
|
||||
|
||||
func TestBindReaderK(t *testing.T) {
|
||||
type Env struct {
|
||||
ConfigPath string
|
||||
}
|
||||
type State struct {
|
||||
Config string
|
||||
}
|
||||
|
||||
// A pure Reader computation
|
||||
getConfigPath := func(s State) func(Env) string {
|
||||
return func(env Env) string {
|
||||
return env.ConfigPath
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[Env](State{}),
|
||||
BindReaderK(
|
||||
func(path string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Config = path
|
||||
return s
|
||||
}
|
||||
},
|
||||
getConfigPath,
|
||||
),
|
||||
Map[Env](func(s State) string { return s.Config }),
|
||||
)
|
||||
|
||||
env := Env{ConfigPath: "/etc/config"}
|
||||
v, err := res(env)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/etc/config", v)
|
||||
}
|
||||
|
||||
func TestBindResultK(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
ParsedValue int
|
||||
}
|
||||
|
||||
// A Result computation that may fail
|
||||
parseValue := func(s State) (int, error) {
|
||||
if s.Value < 0 {
|
||||
return 0, assert.AnError
|
||||
}
|
||||
return s.Value * 2, nil
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[context.Context](State{Value: 5}),
|
||||
BindResultK[context.Context](
|
||||
func(parsed int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.ParsedValue = parsed
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[context.Context](func(s State) int { return s.ParsedValue }),
|
||||
)
|
||||
|
||||
v, err := res(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, v)
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[context.Context](State{Value: -5}),
|
||||
BindResultK[context.Context](
|
||||
func(parsed int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.ParsedValue = parsed
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[context.Context](func(s State) int { return s.ParsedValue }),
|
||||
)
|
||||
|
||||
_, err := res(context.Background())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindToReader(t *testing.T) {
|
||||
type Env struct {
|
||||
ConfigPath string
|
||||
}
|
||||
type State struct {
|
||||
Config string
|
||||
}
|
||||
|
||||
// A Reader that just reads from the environment
|
||||
getConfigPath := func(env Env) string {
|
||||
return env.ConfigPath
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
getConfigPath,
|
||||
BindToReader[Env](func(path string) State {
|
||||
return State{Config: path}
|
||||
}),
|
||||
Map[Env](func(s State) string { return s.Config }),
|
||||
)
|
||||
|
||||
env := Env{ConfigPath: "/etc/config"}
|
||||
v, err := res(env)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/etc/config", v)
|
||||
}
|
||||
|
||||
func TestBindToResult(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
BindToResult[context.Context](func(value int) State {
|
||||
return State{Value: value}
|
||||
})(42, nil),
|
||||
Map[context.Context](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
v, err := computation(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
BindToResult[context.Context](func(value int) State {
|
||||
return State{Value: value}
|
||||
})(0, assert.AnError),
|
||||
Map[context.Context](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
_, err := computation(context.Background())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApReaderS(t *testing.T) {
|
||||
type Env struct {
|
||||
DefaultPort int
|
||||
DefaultHost string
|
||||
}
|
||||
type State struct {
|
||||
Port int
|
||||
Host string
|
||||
}
|
||||
|
||||
getDefaultPort := func(env Env) int { return env.DefaultPort }
|
||||
getDefaultHost := func(env Env) string { return env.DefaultHost }
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[Env](State{}),
|
||||
ApReaderS(
|
||||
func(port int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Port = port
|
||||
return s
|
||||
}
|
||||
},
|
||||
getDefaultPort,
|
||||
),
|
||||
ApReaderS(
|
||||
func(host string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Host = host
|
||||
return s
|
||||
}
|
||||
},
|
||||
getDefaultHost,
|
||||
),
|
||||
Map[Env](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
env := Env{DefaultPort: 8080, DefaultHost: "localhost"}
|
||||
state, err := res(env)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 8080, state.Port)
|
||||
assert.Equal(t, "localhost", state.Host)
|
||||
}
|
||||
|
||||
func TestApResultS(t *testing.T) {
|
||||
type State struct {
|
||||
Value1 int
|
||||
Value2 int
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(42, nil),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(100, nil),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
state, err := computation(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, state.Value1)
|
||||
assert.Equal(t, 100, state.Value2)
|
||||
})
|
||||
|
||||
t.Run("error in first value", func(t *testing.T) {
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(0, assert.AnError),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(100, nil),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
_, err := computation(context.Background())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("error in second value", func(t *testing.T) {
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(42, nil),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(0, assert.AnError),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
_, err := computation(context.Background())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
125
v2/idiomatic/readerresult/curry.go
Normal file
125
v2/idiomatic/readerresult/curry.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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 readerresult
|
||||
|
||||
// These functions curry/uncurry Go functions with context as the first parameter into/from ReaderResult form.
|
||||
// This follows the Go convention of putting context as the first parameter as advised in https://pkg.go.dev/context.
|
||||
//
|
||||
// Unlike the From* functions which return partially applied functions, Curry* functions return fully curried
|
||||
// functions where each parameter is applied one at a time.
|
||||
|
||||
// Curry0 converts a context-only function into a ReaderResult (same as From0 but emphasizes immediate application).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getConfig := func(ctx context.Context) (Config, error) { ... }
|
||||
// rr := readerresult.Curry0(getConfig)
|
||||
// // rr is a ReaderResult[context.Context, Config]
|
||||
func Curry0[R, A any](f func(R) (A, error)) ReaderResult[R, A] {
|
||||
return f
|
||||
}
|
||||
|
||||
// Curry1 converts a function with one parameter into a curried function returning a ReaderResult.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := func(ctx context.Context, id int) (User, error) { ... }
|
||||
// curried := readerresult.Curry1(getUser)
|
||||
// // curried(42) returns ReaderResult[context.Context, User]
|
||||
func Curry1[R, T1, A any](f func(R, T1) (A, error)) func(T1) ReaderResult[R, A] {
|
||||
return func(t T1) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Curry2 converts a function with two parameters into a fully curried function.
|
||||
// Each parameter is applied one at a time.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// queryDB := func(ctx context.Context, table string, id int) (Record, error) { ... }
|
||||
// curried := readerresult.Curry2(queryDB)
|
||||
// // curried("users")(42) returns ReaderResult[context.Context, Record]
|
||||
func Curry2[R, T1, T2, A any](f func(R, T1, T2) (A, error)) func(T1) func(T2) ReaderResult[R, A] {
|
||||
return func(t1 T1) func(T2) ReaderResult[R, A] {
|
||||
return func(t2 T2) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t1, t2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Curry3 converts a function with three parameters into a fully curried function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// updateRecord := func(ctx context.Context, table string, id int, data string) (Result, error) { ... }
|
||||
// curried := readerresult.Curry3(updateRecord)
|
||||
// // curried("users")(42)("data") returns ReaderResult[context.Context, Result]
|
||||
func Curry3[R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderResult[R, A] {
|
||||
return func(t1 T1) func(T2) func(T3) ReaderResult[R, A] {
|
||||
return func(t2 T2) func(T3) ReaderResult[R, A] {
|
||||
return func(t3 T3) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t1, t2, t3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry1 converts a ReaderResult-returning function back into an idiomatic Go function.
|
||||
// This is useful for adapting functional code to work with traditional Go APIs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(id int) readerresult.ReaderResult[context.Context, User] { ... }
|
||||
// gofunc := readerresult.Uncurry1(rrf)
|
||||
// // gofunc(ctx, 42) returns (User, error)
|
||||
func Uncurry1[R, T1, A any](f func(T1) ReaderResult[R, A]) func(R, T1) (A, error) {
|
||||
return func(r R, t T1) (A, error) {
|
||||
return f(t)(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry2 converts a curried two-parameter ReaderResult function into an idiomatic Go function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(table string) func(int) readerresult.ReaderResult[context.Context, Record] { ... }
|
||||
// gofunc := readerresult.Uncurry2(rrf)
|
||||
// // gofunc(ctx, "users", 42) returns (Record, error)
|
||||
func Uncurry2[R, T1, T2, A any](f func(T1) func(T2) ReaderResult[R, A]) func(R, T1, T2) (A, error) {
|
||||
return func(r R, t1 T1, t2 T2) (A, error) {
|
||||
return f(t1)(t2)(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry3 converts a curried three-parameter ReaderResult function into an idiomatic Go function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(table string) func(int) func(string) readerresult.ReaderResult[context.Context, Result] { ... }
|
||||
// gofunc := readerresult.Uncurry3(rrf)
|
||||
// // gofunc(ctx, "users", 42, "data") returns (Result, error)
|
||||
func Uncurry3[R, T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderResult[R, A]) func(R, T1, T2, T3) (A, error) {
|
||||
return func(r R, t1 T1, t2 T2, t3 T3) (A, error) {
|
||||
return f(t1)(t2)(t3)(r)
|
||||
}
|
||||
}
|
||||
184
v2/idiomatic/readerresult/curry_test.go
Normal file
184
v2/idiomatic/readerresult/curry_test.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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCurry0(t *testing.T) {
|
||||
getConfig := func(ctx context.Context) (string, error) {
|
||||
return "config", nil
|
||||
}
|
||||
|
||||
rr := Curry0(getConfig)
|
||||
v, err := rr(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "config", v)
|
||||
}
|
||||
|
||||
func TestCurry1(t *testing.T) {
|
||||
getUser := func(ctx context.Context, id int) (string, error) {
|
||||
if id == 42 {
|
||||
return "Alice", nil
|
||||
}
|
||||
return "", errors.New("user not found")
|
||||
}
|
||||
|
||||
curried := Curry1(getUser)
|
||||
|
||||
rr1 := curried(42)
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", v)
|
||||
|
||||
rr2 := curried(99)
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCurry2(t *testing.T) {
|
||||
queryDB := func(ctx context.Context, table string, id int) (string, error) {
|
||||
if table == "users" && id == 42 {
|
||||
return "record", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
curried := Curry2(queryDB)
|
||||
|
||||
rr1 := curried("users")(42)
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "record", v)
|
||||
|
||||
rr2 := curried("posts")(1)
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCurry3(t *testing.T) {
|
||||
updateRecord := func(ctx context.Context, table string, id int, data string) (string, error) {
|
||||
if table == "users" && id == 42 && data == "updated" {
|
||||
return "success", nil
|
||||
}
|
||||
return "", errors.New("update failed")
|
||||
}
|
||||
|
||||
curried := Curry3(updateRecord)
|
||||
|
||||
rr1 := curried("users")(42)("updated")
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", v)
|
||||
|
||||
rr2 := curried("posts")(1)("data")
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUncurry1(t *testing.T) {
|
||||
rrf := func(id int) ReaderResult[context.Context, string] {
|
||||
if id == 42 {
|
||||
return Of[context.Context]("Alice")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("user not found"))
|
||||
}
|
||||
|
||||
gofunc := Uncurry1(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), 42)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "Alice", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), 99)
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
func TestUncurry2(t *testing.T) {
|
||||
rrf := func(table string) func(int) ReaderResult[context.Context, string] {
|
||||
return func(id int) ReaderResult[context.Context, string] {
|
||||
if table == "users" && id == 42 {
|
||||
return Of[context.Context]("record")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("not found"))
|
||||
}
|
||||
}
|
||||
|
||||
gofunc := Uncurry2(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), "users", 42)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "record", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), "posts", 1)
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
func TestUncurry3(t *testing.T) {
|
||||
rrf := func(table string) func(int) func(string) ReaderResult[context.Context, string] {
|
||||
return func(id int) func(string) ReaderResult[context.Context, string] {
|
||||
return func(data string) ReaderResult[context.Context, string] {
|
||||
if table == "users" && id == 42 && data == "updated" {
|
||||
return Of[context.Context]("success")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("update failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gofunc := Uncurry3(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), "users", 42, "updated")
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "success", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), "posts", 1, "data")
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
// Test round-trip conversions
|
||||
func TestCurryUncurryRoundTrip(t *testing.T) {
|
||||
// Original Go function
|
||||
original := func(ctx context.Context, id int) (string, error) {
|
||||
if id == 42 {
|
||||
return "Alice", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
// Curry then uncurry
|
||||
curried := Curry1(original)
|
||||
uncurried := Uncurry1(curried)
|
||||
|
||||
// Should behave the same as original
|
||||
res1, err1 := original(context.Background(), 42)
|
||||
res2, err2 := uncurried(context.Background(), 42)
|
||||
assert.Equal(t, res1, res2)
|
||||
assert.Equal(t, err1, err2)
|
||||
|
||||
res3, err3 := original(context.Background(), 99)
|
||||
res4, err4 := uncurried(context.Background(), 99)
|
||||
assert.Equal(t, res3, res4)
|
||||
assert.Equal(t, err3, err4)
|
||||
}
|
||||
178
v2/idiomatic/readerresult/doc.go
Normal file
178
v2/idiomatic/readerresult/doc.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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 readerresult provides a ReaderResult monad that combines the Reader and Result monads.
|
||||
//
|
||||
// A ReaderResult[R, A] represents a computation that:
|
||||
// - Depends on an environment of type R (Reader aspect)
|
||||
// - May fail with an error (Result aspect, which is Either[error, A])
|
||||
//
|
||||
// This is equivalent to Reader[R, Result[A]] or Reader[R, Either[error, A]].
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// ReaderResult is particularly useful for:
|
||||
//
|
||||
// 1. Dependency injection with error handling - pass configuration/services through
|
||||
// computations that may fail
|
||||
// 2. Functional error handling - compose operations that depend on context and may error
|
||||
// 3. Testing - easily mock dependencies by changing the environment value
|
||||
//
|
||||
// # Basic Example
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // Function that needs config and may fail
|
||||
// func getUser(id int) readerresult.ReaderResult[Config, User] {
|
||||
// return readerresult.Asks(func(cfg Config) result.Result[User] {
|
||||
// // Use cfg.DatabaseURL to fetch user
|
||||
// return result.Of(user)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Execute by providing the config
|
||||
// cfg := Config{DatabaseURL: "postgres://..."}
|
||||
// user, err := getUser(42)(cfg) // Returns (User, error)
|
||||
//
|
||||
// # Composition
|
||||
//
|
||||
// ReaderResult provides several ways to compose computations:
|
||||
//
|
||||
// 1. Map - transform successful values
|
||||
// 2. Chain (FlatMap) - sequence dependent operations
|
||||
// 3. Ap - combine independent computations
|
||||
// 4. Do-notation - imperative-style composition with Bind
|
||||
//
|
||||
// # Do-Notation Example
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[Config](State{}),
|
||||
// readerresult.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readerresult.ReaderResult[Config, User] {
|
||||
// return getUser(42)
|
||||
// },
|
||||
// ),
|
||||
// readerresult.Bind(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// func(s State) readerresult.ReaderResult[Config, []Post] {
|
||||
// return getPosts(s.User.ID)
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// # Object-Oriented Patterns with Curry Functions
|
||||
//
|
||||
// The Curry functions enable an interesting pattern where you can treat the Reader context (R)
|
||||
// as an object instance, effectively creating method-like functions that compose functionally.
|
||||
//
|
||||
// When you curry a function like func(R, T1, T2) (A, error), the context R becomes the last
|
||||
// argument to be applied, even though it appears first in the original function signature.
|
||||
// This is intentional and follows Go's context-first convention while enabling functional
|
||||
// composition patterns.
|
||||
//
|
||||
// Why R is the last curried argument:
|
||||
//
|
||||
// - In Go, context conventionally comes first: func(ctx Context, params...) (Result, error)
|
||||
// - In curried form: Curry2(f)(param1)(param2) returns ReaderResult[R, A]
|
||||
// - The ReaderResult is then applied to R: Curry2(f)(param1)(param2)(ctx)
|
||||
// - This allows partial application of business parameters before providing the context/object
|
||||
//
|
||||
// Object-Oriented Example:
|
||||
//
|
||||
// // A service struct that acts as the Reader context
|
||||
// type UserService struct {
|
||||
// db *sql.DB
|
||||
// cache Cache
|
||||
// }
|
||||
//
|
||||
// // A method-like function following Go conventions (context first)
|
||||
// func (s *UserService) GetUserByID(ctx context.Context, id int) (User, error) {
|
||||
// // Use s.db and s.cache...
|
||||
// }
|
||||
//
|
||||
// func (s *UserService) UpdateUser(ctx context.Context, id int, name string) (User, error) {
|
||||
// // Use s.db and s.cache...
|
||||
// }
|
||||
//
|
||||
// // Curry these into composable operations
|
||||
// getUser := readerresult.Curry1((*UserService).GetUserByID)
|
||||
// updateUser := readerresult.Curry2((*UserService).UpdateUser)
|
||||
//
|
||||
// // Now compose operations that will be bound to a UserService instance
|
||||
// type Context struct {
|
||||
// Svc *UserService
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// getUser(42), // ReaderResult[Context, User]
|
||||
// readerresult.Chain(func(user User) readerresult.ReaderResult[Context, User] {
|
||||
// newName := user.Name + " (updated)"
|
||||
// return updateUser(user.ID)(newName)
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// // Execute by providing the service instance as context
|
||||
// svc := &UserService{db: db, cache: cache}
|
||||
// ctx := Context{Svc: svc}
|
||||
// updatedUser, err := pipeline(ctx)
|
||||
//
|
||||
// The key insight is that currying creates a chain where:
|
||||
// 1. Business parameters are applied first: getUser(42)
|
||||
// 2. This returns a ReaderResult that waits for the context
|
||||
// 3. Multiple operations can be composed before providing the context
|
||||
// 4. Finally, the context/object is provided to execute everything: pipeline(ctx)
|
||||
//
|
||||
// This pattern is particularly useful for:
|
||||
// - Creating reusable operation pipelines independent of service instances
|
||||
// - Testing with mock service instances
|
||||
// - Dependency injection in a functional style
|
||||
// - Composing operations that share the same service context
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// ReaderResult provides several functions for error handling:
|
||||
//
|
||||
// - Left/Right - create failed/successful values
|
||||
// - GetOrElse - provide a default value for errors
|
||||
// - OrElse - recover from errors with an alternative computation
|
||||
// - Fold - handle both success and failure cases
|
||||
// - ChainEitherK - lift result.Result computations into ReaderResult
|
||||
//
|
||||
// # Relationship to Other Monads
|
||||
//
|
||||
// ReaderResult is related to several other monads in this library:
|
||||
//
|
||||
// - Reader[R, A] - ReaderResult without error handling
|
||||
// - Result[A] (Either[error, A]) - error handling without environment
|
||||
// - ReaderEither[R, E, A] - like ReaderResult but with custom error type E
|
||||
// - IOResult[A] - like ReaderResult but with no environment (IO with errors)
|
||||
//
|
||||
// # Performance Note
|
||||
//
|
||||
// ReaderResult is a zero-cost abstraction - it compiles to a simple function type
|
||||
// with no runtime overhead beyond the underlying computation.
|
||||
package readerresult
|
||||
83
v2/idiomatic/readerresult/from.go
Normal file
83
v2/idiomatic/readerresult/from.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// These functions convert idiomatic Go functions (with context as first parameter and (value, error) return)
|
||||
// into ReaderResult computations. This follows the Go convention of putting context as the first parameter
|
||||
// as advised in https://pkg.go.dev/context.
|
||||
|
||||
// From0 converts a function that takes only a context and returns (A, error) into a ReaderResult.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getConfig := func(ctx context.Context) (Config, error) { ... }
|
||||
// rr := readerresult.From0(getConfig)()
|
||||
// // rr is a ReaderResult[context.Context, Config]
|
||||
func From0[R, A any](f func(R) (A, error)) func() ReaderResult[R, A] {
|
||||
return function.Constant(f)
|
||||
}
|
||||
|
||||
// From1 converts a function with one parameter into a ReaderResult-returning function.
|
||||
// The context parameter is moved to the end (ReaderResult style).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := func(ctx context.Context, id int) (User, error) { ... }
|
||||
// rr := readerresult.From1(getUser)
|
||||
// // rr(42) returns ReaderResult[context.Context, User]
|
||||
func From1[R, T1, A any](f func(R, T1) (A, error)) func(T1) ReaderResult[R, A] {
|
||||
return func(t1 T1) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From2 converts a function with two parameters into a ReaderResult-returning function.
|
||||
// The context parameter is moved to the end (ReaderResult style).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// queryDB := func(ctx context.Context, table string, id int) (Record, error) { ... }
|
||||
// rr := readerresult.From2(queryDB)
|
||||
// // rr("users", 42) returns ReaderResult[context.Context, Record]
|
||||
func From2[R, T1, T2, A any](f func(R, T1, T2) (A, error)) func(T1, T2) ReaderResult[R, A] {
|
||||
return func(t1 T1, t2 T2) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t1, t2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From3 converts a function with three parameters into a ReaderResult-returning function.
|
||||
// The context parameter is moved to the end (ReaderResult style).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// updateRecord := func(ctx context.Context, table string, id int, data string) (Result, error) { ... }
|
||||
// rr := readerresult.From3(updateRecord)
|
||||
// // rr("users", 42, "data") returns ReaderResult[context.Context, Result]
|
||||
func From3[R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, error)) func(T1, T2, T3) ReaderResult[R, A] {
|
||||
return func(t1 T1, t2 T2, t3 T3) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t1, t2, t3)
|
||||
}
|
||||
}
|
||||
}
|
||||
104
v2/idiomatic/readerresult/from_test.go
Normal file
104
v2/idiomatic/readerresult/from_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFrom0(t *testing.T) {
|
||||
getConfig := func(ctx context.Context) (string, error) {
|
||||
return "config", nil
|
||||
}
|
||||
|
||||
rr := From0(getConfig)()
|
||||
v, err := rr(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "config", v)
|
||||
|
||||
// Test with error
|
||||
getConfigErr := func(ctx context.Context) (string, error) {
|
||||
return "", errors.New("config error")
|
||||
}
|
||||
|
||||
rrErr := From0(getConfigErr)()
|
||||
_, err = rrErr(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFrom1(t *testing.T) {
|
||||
getUser := func(ctx context.Context, id int) (string, error) {
|
||||
if id == 42 {
|
||||
return "Alice", nil
|
||||
}
|
||||
return "", errors.New("user not found")
|
||||
}
|
||||
|
||||
rr := From1(getUser)
|
||||
|
||||
rr1 := rr(42)
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", v)
|
||||
|
||||
rr2 := rr(99)
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFrom2(t *testing.T) {
|
||||
queryDB := func(ctx context.Context, table string, id int) (string, error) {
|
||||
if table == "users" && id == 42 {
|
||||
return "record", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
rr := From2(queryDB)
|
||||
|
||||
rr1 := rr("users", 42)
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "record", v)
|
||||
|
||||
rr2 := rr("posts", 1)
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFrom3(t *testing.T) {
|
||||
updateRecord := func(ctx context.Context, table string, id int, data string) (string, error) {
|
||||
if table == "users" && id == 42 && data == "updated" {
|
||||
return "success", nil
|
||||
}
|
||||
return "", errors.New("update failed")
|
||||
}
|
||||
|
||||
rr := From3(updateRecord)
|
||||
|
||||
rr1 := rr("users", 42, "updated")
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", v)
|
||||
|
||||
rr2 := rr("posts", 1, "data")
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
104
v2/idiomatic/readerresult/monoid.go
Normal file
104
v2/idiomatic/readerresult/monoid.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// AlternativeMonoid creates a Monoid for ReaderResult that combines both Alternative and Applicative behavior.
|
||||
// It uses the provided monoid for the success values and falls back to alternative computations on failure.
|
||||
//
|
||||
// The empty element is Of(m.Empty()), and concat tries the first computation, falling back to the second
|
||||
// if it fails, then combines successful values using the underlying monoid.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intAdd := monoid.MakeMonoid(0, func(a, b int) int { return a + b })
|
||||
// rrMonoid := readerresult.AlternativeMonoid[Config](intAdd)
|
||||
//
|
||||
// rr1 := readerresult.Of[Config](5)
|
||||
// rr2 := readerresult.Of[Config](3)
|
||||
// combined := rrMonoid.Concat(rr1, rr2)
|
||||
// // combined(cfg) returns (8, nil)
|
||||
//
|
||||
//go:inline
|
||||
func AlternativeMonoid[R, A any](m M.Monoid[A]) Monoid[R, A] {
|
||||
return M.AlternativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[A, R, A],
|
||||
MonadAlt[R, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid for ReaderResult based on the Alternative pattern.
|
||||
// The empty element is the provided zero computation, and concat tries the first computation,
|
||||
// falling back to the second if it fails.
|
||||
//
|
||||
// This is useful for combining computations where you want to try alternatives until one succeeds.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// zero := func() readerresult.ReaderResult[Config, User] {
|
||||
// return readerresult.Left[Config, User](errors.New("no user"))
|
||||
// }
|
||||
// userMonoid := readerresult.AltMonoid[Config](zero)
|
||||
//
|
||||
// primary := getPrimaryUser()
|
||||
// backup := getBackupUser()
|
||||
// combined := userMonoid.Concat(primary, backup)
|
||||
// // Tries primary, falls back to backup if primary fails
|
||||
//
|
||||
//go:inline
|
||||
func AltMonoid[R, A any](zero Lazy[ReaderResult[R, A]]) Monoid[R, A] {
|
||||
return M.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[R, A],
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid creates a Monoid for ReaderResult based on Applicative functor composition.
|
||||
// The empty element is Of(m.Empty()), and concat combines two computations using the underlying monoid.
|
||||
// Both computations must succeed for the result to succeed.
|
||||
//
|
||||
// This is useful for accumulating results from multiple independent computations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intAdd := monoid.MakeMonoid(0, func(a, b int) int { return a + b })
|
||||
// rrMonoid := readerresult.ApplicativeMonoid[Config](intAdd)
|
||||
//
|
||||
// rr1 := readerresult.Of[Config](5)
|
||||
// rr2 := readerresult.Of[Config](3)
|
||||
// combined := rrMonoid.Concat(rr1, rr2)
|
||||
// // combined(cfg) returns (8, nil)
|
||||
//
|
||||
// // If either fails, the whole computation fails
|
||||
// rr3 := readerresult.Left[Config, int](errors.New("error"))
|
||||
// failed := rrMonoid.Concat(rr1, rr3)
|
||||
// // failed(cfg) returns (nil, error)
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[R, A any](m M.Monoid[A]) Monoid[R, A] {
|
||||
return M.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[A, R, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
323
v2/idiomatic/readerresult/monoid_test.go
Normal file
323
v2/idiomatic/readerresult/monoid_test.go
Normal file
@@ -0,0 +1,323 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
intAddMonoid = N.MonoidSum[int]()
|
||||
strMonoid = S.Monoid
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
rrMonoid := ApplicativeMonoid[MyContext](intAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
v, err := empty(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, v)
|
||||
})
|
||||
|
||||
t.Run("concat two success values", func(t *testing.T) {
|
||||
rr1 := Of[MyContext](5)
|
||||
rr2 := Of[MyContext](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 8, v)
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
v, err := combined1(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
v, err = combined2(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("concat with left failure", func(t *testing.T) {
|
||||
rrSuccess := Of[MyContext](5)
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
_, err := combined(defaultContext)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("concat with right failure", func(t *testing.T) {
|
||||
rrSuccess := Of[MyContext](5)
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
_, err := combined(defaultContext)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("concat multiple values", func(t *testing.T) {
|
||||
rr1 := Of[MyContext](1)
|
||||
rr2 := Of[MyContext](2)
|
||||
rr3 := Of[MyContext](3)
|
||||
rr4 := Of[MyContext](4)
|
||||
|
||||
// Chain concat calls: ((1 + 2) + 3) + 4
|
||||
combined := rrMonoid.Concat(
|
||||
rrMonoid.Concat(
|
||||
rrMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
),
|
||||
rr4,
|
||||
)
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, v)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
strRRMonoid := ApplicativeMonoid[MyContext](strMonoid)
|
||||
|
||||
rr1 := Of[MyContext]("Hello")
|
||||
rr2 := Of[MyContext](" ")
|
||||
rr3 := Of[MyContext]("World")
|
||||
|
||||
combined := strRRMonoid.Concat(
|
||||
strRRMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
)
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello World", v)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
zero := func() ReaderResult[MyContext, int] {
|
||||
return Left[MyContext, int](errors.New("empty"))
|
||||
}
|
||||
|
||||
rrMonoid := AltMonoid(zero)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
_, err := empty(defaultContext)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("concat two success values - uses first", func(t *testing.T) {
|
||||
rr1 := Of[MyContext](5)
|
||||
rr2 := Of[MyContext](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
// AltMonoid takes the first successful value
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, v)
|
||||
})
|
||||
|
||||
t.Run("concat failure then success", func(t *testing.T) {
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
rrSuccess := Of[MyContext](42)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
// Should fall back to second when first fails
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("concat success then failure", func(t *testing.T) {
|
||||
rrSuccess := Of[MyContext](42)
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
// Should use first successful value
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("concat two failures", func(t *testing.T) {
|
||||
err1 := errors.New("error 1")
|
||||
err2 := errors.New("error 2")
|
||||
|
||||
rr1 := Left[MyContext, int](err1)
|
||||
rr2 := Left[MyContext, int](err2)
|
||||
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
// Should use second error when both fail
|
||||
_, err := combined(defaultContext)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
v, err := combined1(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
v, err = combined2(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("fallback chain", func(t *testing.T) {
|
||||
// Simulate trying multiple sources until one succeeds
|
||||
primary := Left[MyContext, string](errors.New("primary failed"))
|
||||
secondary := Left[MyContext, string](errors.New("secondary failed"))
|
||||
tertiary := Of[MyContext]("tertiary success")
|
||||
|
||||
strZero := func() ReaderResult[MyContext, string] {
|
||||
return Left[MyContext, string](errors.New("all failed"))
|
||||
}
|
||||
strMonoid := AltMonoid(strZero)
|
||||
|
||||
// Chain concat: try primary, then secondary, then tertiary
|
||||
combined := strMonoid.Concat(
|
||||
strMonoid.Concat(primary, secondary),
|
||||
tertiary,
|
||||
)
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "tertiary success", v)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
rrMonoid := AlternativeMonoid[MyContext](intAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
v, err := empty(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, v)
|
||||
})
|
||||
|
||||
t.Run("concat two success values", func(t *testing.T) {
|
||||
rr1 := Of[MyContext](5)
|
||||
rr2 := Of[MyContext](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 8, v)
|
||||
})
|
||||
|
||||
t.Run("concat failure then success", func(t *testing.T) {
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
rrSuccess := Of[MyContext](42)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
// Alternative falls back to second when first fails
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("concat success then failure", func(t *testing.T) {
|
||||
rrSuccess := Of[MyContext](42)
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
// Should use first successful value
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
v, err := combined1(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
v, err = combined2(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("multiple values with some failures", func(t *testing.T) {
|
||||
rr1 := Left[MyContext, int](errors.New("fail 1"))
|
||||
rr2 := Of[MyContext](5)
|
||||
rr3 := Left[MyContext, int](errors.New("fail 2"))
|
||||
rr4 := Of[MyContext](10)
|
||||
|
||||
// Alternative should skip failures and accumulate successes
|
||||
combined := rrMonoid.Concat(
|
||||
rrMonoid.Concat(
|
||||
rrMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
),
|
||||
rr4,
|
||||
)
|
||||
// Should accumulate successful values: 5 + 10 = 15
|
||||
v, err := combined(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 15, v)
|
||||
})
|
||||
}
|
||||
|
||||
// Test monoid laws
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
rrMonoid := ApplicativeMonoid[MyContext](intAddMonoid)
|
||||
|
||||
// Left identity: empty <> x == x
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Of[MyContext](42)
|
||||
v1, err1 := rrMonoid.Concat(rrMonoid.Empty(), x)(defaultContext)
|
||||
v2, err2 := x(defaultContext)
|
||||
assert.Equal(t, err2, err1)
|
||||
assert.Equal(t, v2, v1)
|
||||
})
|
||||
|
||||
// Right identity: x <> empty == x
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Of[MyContext](42)
|
||||
v1, err1 := rrMonoid.Concat(x, rrMonoid.Empty())(defaultContext)
|
||||
v2, err2 := x(defaultContext)
|
||||
assert.Equal(t, err2, err1)
|
||||
assert.Equal(t, v2, v1)
|
||||
})
|
||||
|
||||
// Associativity: (x <> y) <> z == x <> (y <> z)
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
x := Of[MyContext](1)
|
||||
y := Of[MyContext](2)
|
||||
z := Of[MyContext](3)
|
||||
|
||||
vLeft, errLeft := rrMonoid.Concat(rrMonoid.Concat(x, y), z)(defaultContext)
|
||||
vRight, errRight := rrMonoid.Concat(x, rrMonoid.Concat(y, z))(defaultContext)
|
||||
|
||||
assert.Equal(t, errRight, errLeft)
|
||||
assert.Equal(t, vRight, vLeft)
|
||||
})
|
||||
}
|
||||
596
v2/idiomatic/readerresult/reader.go
Normal file
596
v2/idiomatic/readerresult/reader.go
Normal file
@@ -0,0 +1,596 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/option"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromEither lifts a Result[A] into a ReaderResult[R, A] that ignores the environment.
|
||||
// The resulting computation will always produce the same result regardless of the environment provided.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// res := result.Of(42)
|
||||
// rr := readerresult.FromEither[Config](res)
|
||||
// // rr(anyConfig) will always return (42, nil)
|
||||
func FromEither[R, A any](e Result[A]) ReaderResult[R, A] {
|
||||
return func(_ R) (A, error) {
|
||||
return RES.Unwrap(e)
|
||||
}
|
||||
}
|
||||
|
||||
// FromResult is an alias for FromEither.
|
||||
// It lifts a Result[A] into a ReaderResult[R, A] that ignores the environment.
|
||||
//
|
||||
//go:inline
|
||||
func FromResult[R, A any](a A, err error) ReaderResult[R, A] {
|
||||
return func(_ R) (A, error) {
|
||||
return a, err
|
||||
}
|
||||
}
|
||||
|
||||
// RightReader lifts a Reader[R, A] into a ReaderResult[R, A] that always succeeds.
|
||||
// The resulting computation reads a value from the environment and wraps it in a successful Result.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPort := func(cfg Config) int { return cfg.Port }
|
||||
// rr := readerresult.RightReader[Config](getPort)
|
||||
// // rr(cfg) returns (cfg.Port, nil)
|
||||
func RightReader[R, A any](rdr Reader[R, A]) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return result.Of(rdr(r))
|
||||
}
|
||||
}
|
||||
|
||||
// LeftReader lifts a Reader[R, error] into a ReaderResult[R, A] that always fails.
|
||||
// The resulting computation reads an error from the environment and wraps it in a failed Result.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getError := func(cfg Config) error { return cfg.InitError }
|
||||
// rr := readerresult.LeftReader[User](getError)
|
||||
// // rr(cfg) returns (nil, cfg.InitError)
|
||||
func LeftReader[A, R any](l Reader[R, error]) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return result.Left[A](l(r))
|
||||
}
|
||||
}
|
||||
|
||||
// Left creates a ReaderResult that always fails with the given error, ignoring the environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Left[Config, User](errors.New("not found"))
|
||||
// // rr(anyConfig) always returns (nil, error)
|
||||
func Left[R, A any](err error) ReaderResult[R, A] {
|
||||
return func(_ R) (A, error) {
|
||||
return result.Left[A](err)
|
||||
}
|
||||
}
|
||||
|
||||
// Right creates a ReaderResult that always succeeds with the given value, ignoring the environment.
|
||||
// This is the "pure" or "return" operation for the ReaderResult monad.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Right[Config](42)
|
||||
// // rr(anyConfig) always returns (42, nil)
|
||||
func Right[R, A any](a A) ReaderResult[R, A] {
|
||||
return func(_ R) (A, error) {
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// FromReader is an alias for RightReader.
|
||||
// It lifts a Reader[R, A] into a ReaderResult[R, A] that always succeeds.
|
||||
//
|
||||
//go:inline
|
||||
func FromReader[R, A any](r Reader[R, A]) ReaderResult[R, A] {
|
||||
return RightReader(r)
|
||||
}
|
||||
|
||||
// MonadMap transforms the success value of a ReaderResult using the given function.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of[Config](5)
|
||||
// doubled := readerresult.MonadMap(rr, func(x int) int { return x * 2 })
|
||||
// // doubled(cfg) returns (10, nil)
|
||||
func MonadMap[R, A, B any](fa ReaderResult[R, A], f func(A) B) ReaderResult[R, B] {
|
||||
mp := result.Map(f)
|
||||
return func(r R) (B, error) {
|
||||
return mp(fa(r))
|
||||
}
|
||||
}
|
||||
|
||||
// Map is the curried version of MonadMap.
|
||||
// It returns an Operator that can be used in function composition pipelines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := readerresult.Map[Config](func(x int) int { return x * 2 })
|
||||
// result := F.Pipe1(readerresult.Of[Config](5), double)
|
||||
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
|
||||
mp := result.Map(f)
|
||||
return func(rr ReaderResult[R, A]) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
return mp(rr(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChain sequences two ReaderResult computations, where the second depends on the result of the first.
|
||||
// This is also known as "flatMap" or "bind". If the first computation fails, the second is not executed.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
|
||||
// getPosts := func(user User) readerresult.ReaderResult[DB, []Post] { ... }
|
||||
// userPosts := readerresult.MonadChain(getUser(42), getPosts)
|
||||
func MonadChain[R, A, B any](ma ReaderResult[R, A], f Kleisli[R, A, B]) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
a, err := ma(r)
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return f(a)(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Chain is the curried version of MonadChain.
|
||||
// It returns an Operator that can be used in function composition pipelines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPosts := func(user User) readerresult.ReaderResult[DB, []Post] { ... }
|
||||
// result := F.Pipe1(getUser(42), readerresult.Chain[DB](getPosts))
|
||||
//
|
||||
//go:inline
|
||||
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return function.Bind2nd(MonadChain[R, A, B], f)
|
||||
}
|
||||
|
||||
// Of creates a ReaderResult that always succeeds with the given value.
|
||||
// This is an alias for Right and is the "pure" or "return" operation for the ReaderResult monad.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of[Config](42)
|
||||
// // rr(anyConfig) always returns (42, nil)
|
||||
//
|
||||
//go:inline
|
||||
func Of[R, A any](a A) ReaderResult[R, A] {
|
||||
return func(_ R) (A, error) {
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.
|
||||
// Both computations share the same environment. This is useful for combining independent
|
||||
// computations that don't depend on each other's results.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// add := func(x int) func(int) int { return func(y int) int { return x + y } }
|
||||
// fabr := readerresult.Of[Config](add(5))
|
||||
// fa := readerresult.Of[Config](3)
|
||||
// result := readerresult.MonadAp(fabr, fa) // Returns (8, nil)
|
||||
func MonadAp[B, R, A any](fab ReaderResult[R, func(A) B], fa ReaderResult[R, A]) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
ab, aberr := fab(r)
|
||||
if aberr != nil {
|
||||
return result.Left[B](aberr)
|
||||
}
|
||||
a, aerr := fa(r)
|
||||
if aerr != nil {
|
||||
return result.Left[B](aerr)
|
||||
}
|
||||
return result.Of(ab(a))
|
||||
}
|
||||
}
|
||||
|
||||
// Ap is the curried version of MonadAp.
|
||||
// It returns an Operator that can be used in function composition pipelines.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, R, A any](fa ReaderResult[R, A]) Operator[R, func(A) B, B] {
|
||||
return function.Bind2nd(MonadAp[B, R, A], fa)
|
||||
}
|
||||
|
||||
// FromPredicate creates a Kleisli arrow that tests a predicate and returns either the input value
|
||||
// or an error. If the predicate returns true, the value is returned as a success. If false,
|
||||
// the onFalse function is called to generate an error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := readerresult.FromPredicate[Config](
|
||||
// func(x int) bool { return x > 0 },
|
||||
// func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
// )
|
||||
// result := isPositive(5) // Returns ReaderResult that succeeds with 5
|
||||
// result := isPositive(-1) // Returns ReaderResult that fails with error
|
||||
func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) error) Kleisli[R, A, A] {
|
||||
return func(a A) ReaderResult[R, A] {
|
||||
ok := pred(a)
|
||||
if ok {
|
||||
return Of[R](a)
|
||||
}
|
||||
return Left[R, A](onFalse(a))
|
||||
}
|
||||
}
|
||||
|
||||
// Fold handles both success and failure cases by providing functions for each.
|
||||
// The result is always a Reader[R, B] (without the error channel).
|
||||
// This is useful for converting a ReaderResult into a plain Reader by handling the error case.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// handleError := func(err error) reader.Reader[Config, string] {
|
||||
// return func(cfg Config) string { return "Error: " + err.Error() }
|
||||
// }
|
||||
// handleSuccess := func(user User) reader.Reader[Config, string] {
|
||||
// return func(cfg Config) string { return user.Name }
|
||||
// }
|
||||
// result := readerresult.Fold[Config, User, string](handleError, handleSuccess)(getUserRR)
|
||||
func Fold[R, A, B any](onLeft reader.Kleisli[R, error, B], onRight reader.Kleisli[R, A, B]) func(ReaderResult[R, A]) Reader[R, B] {
|
||||
return func(rr ReaderResult[R, A]) Reader[R, B] {
|
||||
return func(r R) B {
|
||||
a, err := rr(r)
|
||||
if err != nil {
|
||||
return onLeft(err)(r)
|
||||
}
|
||||
return onRight(a)(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrElse extracts the success value or computes a default value from the error.
|
||||
// The result is a Reader[R, A] that always succeeds.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// defaultUser := func(err error) reader.Reader[Config, User] {
|
||||
// return func(cfg Config) User { return User{Name: "Guest"} }
|
||||
// }
|
||||
// result := readerresult.GetOrElse[Config](defaultUser)(getUserRR)
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[R, A any](onLeft reader.Kleisli[R, error, A]) func(ReaderResult[R, A]) Reader[R, A] {
|
||||
return Fold(onLeft, reader.Of[R, A])
|
||||
}
|
||||
|
||||
// OrElse provides an alternative ReaderResult computation if the first one fails.
|
||||
// This is useful for fallback logic or retry scenarios.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPrimaryUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
|
||||
// getBackupUser := func(err error) readerresult.ReaderResult[DB, User] {
|
||||
// return readerresult.Of[DB](User{Name: "Guest"})
|
||||
// }
|
||||
// result := F.Pipe1(getPrimaryUser(42), readerresult.OrElse[DB](getBackupUser))
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
|
||||
return func(rr ReaderResult[R, A]) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
a, err := rr(r)
|
||||
if err != nil {
|
||||
return onLeft(err)(r)
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OrLeft transforms the error value if the computation fails, leaving successful values unchanged.
|
||||
// This is useful for error mapping or enriching error information.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichError := func(err error) reader.Reader[Config, error] {
|
||||
// return func(cfg Config) error {
|
||||
// return fmt.Errorf("DB error on %s: %w", cfg.DBHost, err)
|
||||
// }
|
||||
// }
|
||||
// result := F.Pipe1(getUserRR, readerresult.OrLeft[Config](enrichError))
|
||||
func OrLeft[R, A any](onLeft reader.Kleisli[R, error, error]) Operator[R, A, A] {
|
||||
return func(rr ReaderResult[R, A]) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
a, err := rr(r)
|
||||
if err != nil {
|
||||
return result.Left[A](onLeft(err)(r))
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ask retrieves the current environment as a successful ReaderResult.
|
||||
// This is useful for accessing configuration or context within a ReaderResult computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Ask[Config](),
|
||||
// readerresult.Map[Config](func(cfg Config) int { return cfg.Port }),
|
||||
// )
|
||||
// // result(cfg) returns (cfg.Port, nil)
|
||||
//
|
||||
//go:inline
|
||||
func Ask[R any]() ReaderResult[R, R] {
|
||||
return fromreader.Ask(FromReader[R, R])()
|
||||
}
|
||||
|
||||
// Asks retrieves a value from the environment using the provided Reader function.
|
||||
// This lifts a Reader computation into a ReaderResult that always succeeds.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPort := func(cfg Config) int { return cfg.Port }
|
||||
// result := readerresult.Asks[Config](getPort)
|
||||
// // result(cfg) returns (cfg.Port, nil)
|
||||
//
|
||||
//go:inline
|
||||
func Asks[R, A any](r Reader[R, A]) ReaderResult[R, A] {
|
||||
return fromreader.Asks(FromReader[R, A])(r)
|
||||
}
|
||||
|
||||
// MonadChainEitherK chains a ReaderResult with a function that returns a plain Result.
|
||||
// This is useful for integrating functions that don't need environment access.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := readerresult.MonadChainEitherK(getUserDataRR, parseUser)
|
||||
func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f RES.Kleisli[A, B]) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
a, err := ma(r)
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return RES.Unwrap(f(a))
|
||||
}
|
||||
}
|
||||
|
||||
// ChainEitherK is the curried version of MonadChainEitherK.
|
||||
// It lifts a Result-returning function into a ReaderResult operator.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := F.Pipe1(getUserDataRR, readerresult.ChainEitherK[Config](parseUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[R, A, B any](f RES.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return function.Bind2nd(MonadChainEitherK[R, A, B], f)
|
||||
}
|
||||
|
||||
// MonadChainReaderK chains a ReaderResult with a function that returns a plain Result.
|
||||
// This is useful for integrating functions that don't need environment access.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := readerresult.MonadChainReaderK(getUserDataRR, parseUser)
|
||||
func MonadChainReaderK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B]) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
a, err := ma(r)
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return f(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainReaderK is the curried version of MonadChainEitherK.
|
||||
// It lifts a Result-returning function into a ReaderResult operator.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := F.Pipe1(getUserDataRR, readerresult.ChainReaderK[Config](parseUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return function.Bind2nd(MonadChainReaderK[R, A, B], f)
|
||||
}
|
||||
|
||||
// ChainOptionK chains with a function that returns an Option, converting None to an error.
|
||||
// This is useful for integrating functions that return optional values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// findUser := func(id int) option.Option[User] { ... }
|
||||
// notFound := func() error { return errors.New("user not found") }
|
||||
// chain := readerresult.ChainOptionK[Config, int, User](notFound)
|
||||
// result := F.Pipe1(readerresult.Of[Config](42), chain(findUser))
|
||||
func ChainOptionK[R, A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[R, A, B] {
|
||||
fo := result.FromOption[B](onNone)
|
||||
return func(k option.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return func(rr ReaderResult[R, A]) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
a, err := rr(r)
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return fo(k(a))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten removes one level of nesting from a nested ReaderResult.
|
||||
// This converts ReaderResult[R, ReaderResult[R, A]] into ReaderResult[R, A].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nested := readerresult.Of[Config](readerresult.Of[Config](42))
|
||||
// flat := readerresult.Flatten(nested)
|
||||
// // flat(cfg) returns (42, nil)
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[R, A any](mma ReaderResult[R, ReaderResult[R, A]]) ReaderResult[R, A] {
|
||||
return MonadChain(mma, function.Identity[ReaderResult[R, A]])
|
||||
}
|
||||
|
||||
// MonadBiMap maps functions over both the error and success channels simultaneously.
|
||||
// This transforms both the error type and the success type in a single operation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// result := readerresult.MonadBiMap(rr, enrichErr, double)
|
||||
//
|
||||
//go:inline
|
||||
func MonadBiMap[R, A, B any](fa ReaderResult[R, A], f Endomorphism[error], g func(A) B) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
a, err := fa(r)
|
||||
if err != nil {
|
||||
return result.Left[B](f(err))
|
||||
}
|
||||
return result.Of(g(a))
|
||||
}
|
||||
}
|
||||
|
||||
// BiMap is the curried version of MonadBiMap.
|
||||
// It maps a pair of functions over the error and success channels.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// result := F.Pipe1(rr, readerresult.BiMap[Config](enrichErr, double))
|
||||
func BiMap[R, A, B any](f Endomorphism[error], g func(A) B) Operator[R, A, B] {
|
||||
return func(fa ReaderResult[R, A]) ReaderResult[R, B] {
|
||||
return MonadBiMap(fa, f, g)
|
||||
}
|
||||
}
|
||||
|
||||
// Local changes the environment type during execution of a ReaderResult.
|
||||
// This is similar to Contravariant's contramap and allows adapting computations
|
||||
// to work with different environment types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Convert DB environment to Config environment
|
||||
// toConfig := func(db DB) Config { return db.Config }
|
||||
// rr := readerresult.Of[Config](42)
|
||||
// adapted := readerresult.Local[int](toConfig)(rr)
|
||||
// // adapted now accepts DB instead of Config
|
||||
func Local[A, R2, R1 any](f func(R2) R1) func(ReaderResult[R1, A]) ReaderResult[R2, A] {
|
||||
return func(rr ReaderResult[R1, A]) ReaderResult[R2, A] {
|
||||
return func(r R2) (A, error) {
|
||||
return rr(f(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read applies an environment value to a ReaderResult to execute it and obtain the Result.
|
||||
// This is the primary way to "run" a ReaderResult computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Asks(func(cfg Config) int { return cfg.Port })
|
||||
// run := readerresult.Read[int](myConfig)
|
||||
// port, err := run(rr) // Returns (int, error)
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, R any](r R) func(ReaderResult[R, A]) (A, error) {
|
||||
return func(rr ReaderResult[R, A]) (A, error) {
|
||||
return rr(r)
|
||||
}
|
||||
}
|
||||
|
||||
// MonadFlap applies a wrapped function to a concrete value (reverse of Ap).
|
||||
// This is useful when you have a function in a context and a plain value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fabr := readerresult.Of[Config](func(x int) int { return x * 2 })
|
||||
// result := readerresult.MonadFlap(fabr, 5) // Returns (10, nil)
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[R, A, B any](fab ReaderResult[R, func(A) B], a A) ReaderResult[R, B] {
|
||||
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
// Flap is the curried version of MonadFlap.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
return functor.Flap(Map[R, func(A) B, B], a)
|
||||
}
|
||||
|
||||
// MonadMapLeft transforms the error value without affecting successful results.
|
||||
// This is useful for error enrichment, wrapping, or transformation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("DB error: %w", e) }
|
||||
// result := readerresult.MonadMapLeft(getUserRR, enrichErr)
|
||||
func MonadMapLeft[R, A any](fa ReaderResult[R, A], f Endomorphism[error]) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
a, err := fa(r)
|
||||
if err != nil {
|
||||
return result.Left[A](f(err))
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// MapLeft is the curried version of MonadMapLeft.
|
||||
// It applies a mapping function to the error channel only.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("DB error: %w", e) }
|
||||
// result := F.Pipe1(getUserRR, readerresult.MapLeft[Config](enrichErr))
|
||||
//
|
||||
//go:inline
|
||||
func MapLeft[R, A any](f Endomorphism[error]) Operator[R, A, A] {
|
||||
return function.Bind2nd(MonadMapLeft[R, A], f)
|
||||
}
|
||||
|
||||
// MonadAlt tries the first computation, and if it fails, tries the second.
|
||||
// This implements the Alternative pattern for error recovery.
|
||||
func MonadAlt[R, A any](first ReaderResult[R, A], second Lazy[ReaderResult[R, A]]) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
a, err := first(r)
|
||||
if err != nil {
|
||||
return second()(r)
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// Alt tries the first computation, and if it fails, tries the second.
|
||||
// This implements the Alternative pattern for error recovery.
|
||||
//
|
||||
//go:inline
|
||||
func Alt[R, A any](second Lazy[ReaderResult[R, A]]) Operator[R, A, A] {
|
||||
return function.Bind2nd(MonadAlt[R, A], second)
|
||||
}
|
||||
397
v2/idiomatic/readerresult/reader_test.go
Normal file
397
v2/idiomatic/readerresult/reader_test.go
Normal file
@@ -0,0 +1,397 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MyContext string
|
||||
|
||||
const defaultContext MyContext = "default"
|
||||
|
||||
var (
|
||||
testError = errors.New("test error")
|
||||
)
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
rr := FromEither[MyContext](result.Of(42))
|
||||
v, err := rr(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
rrErr := FromEither[MyContext](result.Left[int](testError))
|
||||
_, err = rrErr(defaultContext)
|
||||
assert.Equal(t, testError, err)
|
||||
}
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
rr := FromResult[MyContext](42, nil)
|
||||
v, err := rr(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
}
|
||||
|
||||
func TestRightReader(t *testing.T) {
|
||||
r := func(ctx MyContext) int { return 42 }
|
||||
rr := RightReader(r)
|
||||
v, err := rr(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
}
|
||||
|
||||
func TestLeftReader(t *testing.T) {
|
||||
r := func(ctx MyContext) error { return testError }
|
||||
rr := LeftReader[int](r)
|
||||
_, err := rr(defaultContext)
|
||||
assert.Equal(t, testError, err)
|
||||
}
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
rr := Left[MyContext, int](testError)
|
||||
_, err := rr(defaultContext)
|
||||
assert.Equal(t, testError, err)
|
||||
}
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
rr := Right[MyContext](42)
|
||||
v, err := rr(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
v, err := rr(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
r := func(ctx MyContext) string { return string(ctx) }
|
||||
rr := FromReader(r)
|
||||
v, err := rr(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "default", v)
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](1),
|
||||
Map[MyContext](utils.Double),
|
||||
)
|
||||
v, err := g(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, v)
|
||||
|
||||
// Test with error
|
||||
gErr := F.Pipe1(
|
||||
Left[MyContext, int](testError),
|
||||
Map[MyContext](utils.Double),
|
||||
)
|
||||
_, err = gErr(defaultContext)
|
||||
assert.Equal(t, testError, err)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
rr := Of[MyContext](5)
|
||||
doubled := MonadMap(rr, func(x int) int { return x * 2 })
|
||||
v, err := doubled(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, v)
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
addOne := func(x int) ReaderResult[MyContext, int] {
|
||||
return Of[MyContext](x + 1)
|
||||
}
|
||||
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](5),
|
||||
Chain(addOne),
|
||||
)
|
||||
v, err := g(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, v)
|
||||
|
||||
// Test error propagation
|
||||
gErr := F.Pipe1(
|
||||
Left[MyContext, int](testError),
|
||||
Chain(addOne),
|
||||
)
|
||||
_, err = gErr(defaultContext)
|
||||
assert.Equal(t, testError, err)
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
addOne := func(x int) ReaderResult[MyContext, int] {
|
||||
return Of[MyContext](x + 1)
|
||||
}
|
||||
|
||||
rr := Of[MyContext](5)
|
||||
res := MonadChain(rr, addOne)
|
||||
v, err := res(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, v)
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](utils.Double),
|
||||
Ap[int](Of[MyContext](1)),
|
||||
)
|
||||
v, err := g(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, v)
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
add := func(x int) func(int) int {
|
||||
return func(y int) int { return x + y }
|
||||
}
|
||||
fabr := Of[MyContext](add(5))
|
||||
fa := Of[MyContext](3)
|
||||
res := MonadAp(fabr, fa)
|
||||
v, err := res(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 8, v)
|
||||
}
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate[MyContext](
|
||||
func(x int) bool { return x > 0 },
|
||||
func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
)
|
||||
|
||||
v, err := isPositive(5)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, v)
|
||||
|
||||
_, err = isPositive(-1)(defaultContext)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
handleError := func(err error) reader.Reader[MyContext, string] {
|
||||
return func(ctx MyContext) string { return "Error: " + err.Error() }
|
||||
}
|
||||
handleSuccess := func(x int) reader.Reader[MyContext, string] {
|
||||
return func(ctx MyContext) string { return fmt.Sprintf("Success: %d", x) }
|
||||
}
|
||||
|
||||
fold := Fold(handleError, handleSuccess)
|
||||
|
||||
res1 := fold(Of[MyContext](42))(defaultContext)
|
||||
assert.Equal(t, "Success: 42", res1)
|
||||
|
||||
res2 := fold(Left[MyContext, int](testError))(defaultContext)
|
||||
assert.Equal(t, "Error: "+testError.Error(), res2)
|
||||
}
|
||||
|
||||
func TestGetOrElse(t *testing.T) {
|
||||
defaultVal := func(err error) reader.Reader[MyContext, int] {
|
||||
return func(ctx MyContext) int { return 0 }
|
||||
}
|
||||
|
||||
getOrElse := GetOrElse(defaultVal)
|
||||
|
||||
res1 := getOrElse(Of[MyContext](42))(defaultContext)
|
||||
assert.Equal(t, 42, res1)
|
||||
|
||||
res2 := getOrElse(Left[MyContext, int](testError))(defaultContext)
|
||||
assert.Equal(t, 0, res2)
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
fallback := func(err error) ReaderResult[MyContext, int] {
|
||||
return Of[MyContext](99)
|
||||
}
|
||||
|
||||
orElse := OrElse(fallback)
|
||||
|
||||
v, err := F.Pipe1(Of[MyContext](42), orElse)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
v, err = F.Pipe1(Left[MyContext, int](testError), orElse)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 99, v)
|
||||
}
|
||||
|
||||
func TestOrLeft(t *testing.T) {
|
||||
enrichErr := func(err error) reader.Reader[MyContext, error] {
|
||||
return func(ctx MyContext) error {
|
||||
return fmt.Errorf("enriched: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
orLeft := OrLeft[MyContext, int](enrichErr)
|
||||
|
||||
v, err := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
_, err = F.Pipe1(Left[MyContext, int](testError), orLeft)(defaultContext)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
rr := Ask[MyContext]()
|
||||
v, err := rr(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, defaultContext, v)
|
||||
}
|
||||
|
||||
func TestAsks(t *testing.T) {
|
||||
getLen := func(ctx MyContext) int { return len(string(ctx)) }
|
||||
rr := Asks(getLen)
|
||||
v, err := rr(defaultContext) // "default" has 7 chars
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 7, v)
|
||||
}
|
||||
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
parseInt := func(s string) (int, error) {
|
||||
if s == "42" {
|
||||
return 42, nil
|
||||
}
|
||||
return 0, errors.New("parse error")
|
||||
}
|
||||
|
||||
chain := ChainReaderK[MyContext](parseInt)
|
||||
|
||||
v, err := F.Pipe1(Of[MyContext]("42"), chain)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
_, err = F.Pipe1(Of[MyContext]("invalid"), chain)(defaultContext)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestChainEitherK(t *testing.T) {
|
||||
parseInt := func(s string) RES.Result[int] {
|
||||
if s == "42" {
|
||||
return RES.Of(42)
|
||||
}
|
||||
return RES.Left[int](errors.New("parse error"))
|
||||
}
|
||||
|
||||
chain := ChainEitherK[MyContext](parseInt)
|
||||
|
||||
v, err := F.Pipe1(Of[MyContext]("42"), chain)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
_, err = F.Pipe1(Of[MyContext]("invalid"), chain)(defaultContext)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
findEven := func(x int) (int, bool) {
|
||||
if x%2 == 0 {
|
||||
return x, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
notFound := func() error { return errors.New("not even") }
|
||||
chain := ChainOptionK[MyContext, int, int](notFound)(findEven)
|
||||
|
||||
res := F.Pipe1(Of[MyContext](4), chain)
|
||||
v, err := res(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 4, v)
|
||||
|
||||
res2 := F.Pipe1(Of[MyContext](3), chain)
|
||||
_, err = res2(defaultContext)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](Of[MyContext]("a")),
|
||||
Flatten[MyContext, string],
|
||||
)
|
||||
v, err := g(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a", v)
|
||||
}
|
||||
|
||||
func TestBiMap(t *testing.T) {
|
||||
enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
res := F.Pipe1(Of[MyContext](5), BiMap[MyContext](enrichErr, double))
|
||||
v, err := res(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, v)
|
||||
|
||||
res2 := F.Pipe1(Left[MyContext, int](testError), BiMap[MyContext](enrichErr, double))
|
||||
_, err = res2(defaultContext)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
type OtherContext int
|
||||
toMyContext := func(oc OtherContext) MyContext {
|
||||
return MyContext(fmt.Sprintf("ctx-%d", oc))
|
||||
}
|
||||
|
||||
rr := Asks(func(ctx MyContext) string { return string(ctx) })
|
||||
adapted := Local[string](toMyContext)(rr)
|
||||
|
||||
v, err := adapted(OtherContext(42))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ctx-42", v)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
read := Read[int](defaultContext)
|
||||
v, err := read(rr)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
fabr := Of[MyContext](func(x int) int { return x * 2 })
|
||||
flapped := MonadFlap(fabr, 5)
|
||||
v, err := flapped(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, v)
|
||||
}
|
||||
|
||||
func TestMapLeft(t *testing.T) {
|
||||
enrichErr := func(e error) error { return fmt.Errorf("DB error: %w", e) }
|
||||
|
||||
res := F.Pipe1(Of[MyContext](42), MapLeft[MyContext, int](enrichErr))
|
||||
v, err := res(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
res2 := F.Pipe1(Left[MyContext, int](testError), MapLeft[MyContext, int](enrichErr))
|
||||
_, err = res2(defaultContext)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
112
v2/idiomatic/readerresult/sequence.go
Normal file
112
v2/idiomatic/readerresult/sequence.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 readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
// SequenceT functions convert multiple ReaderResult values into a single ReaderResult of a tuple.
|
||||
// These are useful for combining independent computations that share the same environment.
|
||||
// If any computation fails, the entire sequence fails with the first error.
|
||||
|
||||
// SequenceT1 wraps a single ReaderResult value in a Tuple1.
|
||||
// This is primarily for consistency with the other SequenceT functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of[Config](42)
|
||||
// result := readerresult.SequenceT1(rr)
|
||||
// // result(cfg) returns (Tuple1{42}, nil)
|
||||
//
|
||||
//go:inline
|
||||
func SequenceT1[R, A any](a ReaderResult[R, A]) ReaderResult[R, T.Tuple1[A]] {
|
||||
return apply.SequenceT1(
|
||||
Map[R, A, T.Tuple1[A]],
|
||||
|
||||
a)
|
||||
}
|
||||
|
||||
// SequenceT2 combines two independent ReaderResult computations into a tuple.
|
||||
// Both computations share the same environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPort := readerresult.Asks(func(cfg Config) int { return cfg.Port })
|
||||
// getHost := readerresult.Asks(func(cfg Config) string { return cfg.Host })
|
||||
// result := readerresult.SequenceT2(getPort, getHost)
|
||||
// // result(cfg) returns (Tuple2{cfg.Port, cfg.Host}, nil)
|
||||
//
|
||||
//go:inline
|
||||
func SequenceT2[R, A, B any](
|
||||
a ReaderResult[R, A],
|
||||
b ReaderResult[R, B],
|
||||
) ReaderResult[R, T.Tuple2[A, B]] {
|
||||
return apply.SequenceT2(
|
||||
Map[R, A, func(B) T.Tuple2[A, B]],
|
||||
Ap[T.Tuple2[A, B], R, B],
|
||||
a, b)
|
||||
}
|
||||
|
||||
// SequenceT3 combines three independent ReaderResult computations into a tuple.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := getUserRR(42)
|
||||
// getConfig := getConfigRR()
|
||||
// getStats := getStatsRR()
|
||||
// result := readerresult.SequenceT3(getUser, getConfig, getStats)
|
||||
// // result(env) returns (Tuple3{user, config, stats}, nil)
|
||||
//
|
||||
//go:inline
|
||||
func SequenceT3[R, A, B, C any](
|
||||
a ReaderResult[R, A],
|
||||
b ReaderResult[R, B],
|
||||
c ReaderResult[R, C],
|
||||
) ReaderResult[R, T.Tuple3[A, B, C]] {
|
||||
return apply.SequenceT3(
|
||||
Map[R, A, func(B) func(C) T.Tuple3[A, B, C]],
|
||||
Ap[func(C) T.Tuple3[A, B, C], R, B],
|
||||
Ap[T.Tuple3[A, B, C], R, C],
|
||||
|
||||
a, b, c,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceT4 combines four independent ReaderResult computations into a tuple.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := readerresult.SequenceT4(getUserRR, getConfigRR, getStatsRR, getMetadataRR)
|
||||
// // result(env) returns (Tuple4{user, config, stats, metadata}, nil)
|
||||
//
|
||||
//go:inline
|
||||
func SequenceT4[R, A, B, C, D any](
|
||||
a ReaderResult[R, A],
|
||||
b ReaderResult[R, B],
|
||||
c ReaderResult[R, C],
|
||||
d ReaderResult[R, D],
|
||||
) ReaderResult[R, T.Tuple4[A, B, C, D]] {
|
||||
return apply.SequenceT4(
|
||||
Map[R, A, func(B) func(C) func(D) T.Tuple4[A, B, C, D]],
|
||||
Ap[func(C) func(D) T.Tuple4[A, B, C, D], R, B],
|
||||
Ap[func(D) T.Tuple4[A, B, C, D], R, C],
|
||||
Ap[T.Tuple4[A, B, C, D], R, D],
|
||||
|
||||
a, b, c, d,
|
||||
)
|
||||
}
|
||||
105
v2/idiomatic/readerresult/sequence_test.go
Normal file
105
v2/idiomatic/readerresult/sequence_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
errFoo = fmt.Errorf("error")
|
||||
)
|
||||
|
||||
func TestSequenceT1(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := Left[MyContext, string](errFoo)
|
||||
|
||||
res1 := SequenceT1(t1)
|
||||
v1, err1 := res1(defaultContext)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, T.MakeTuple1("s1"), v1)
|
||||
|
||||
res2 := SequenceT1(e1)
|
||||
_, err2 := res2(defaultContext)
|
||||
assert.Equal(t, errFoo, err2)
|
||||
}
|
||||
|
||||
func TestSequenceT2(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := Left[MyContext, string](errFoo)
|
||||
t2 := Of[MyContext](2)
|
||||
e2 := Left[MyContext, int](errFoo)
|
||||
|
||||
res1 := SequenceT2(t1, t2)
|
||||
v1, err1 := res1(defaultContext)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, T.MakeTuple2("s1", 2), v1)
|
||||
|
||||
res2 := SequenceT2(e1, t2)
|
||||
_, err2 := res2(defaultContext)
|
||||
assert.Equal(t, errFoo, err2)
|
||||
|
||||
res3 := SequenceT2(t1, e2)
|
||||
_, err3 := res3(defaultContext)
|
||||
assert.Equal(t, errFoo, err3)
|
||||
}
|
||||
|
||||
func TestSequenceT3(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := Left[MyContext, string](errFoo)
|
||||
t2 := Of[MyContext](2)
|
||||
e2 := Left[MyContext, int](errFoo)
|
||||
t3 := Of[MyContext](true)
|
||||
e3 := Left[MyContext, bool](errFoo)
|
||||
|
||||
res1 := SequenceT3(t1, t2, t3)
|
||||
v1, err1 := res1(defaultContext)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, T.MakeTuple3("s1", 2, true), v1)
|
||||
|
||||
res2 := SequenceT3(e1, t2, t3)
|
||||
_, err2 := res2(defaultContext)
|
||||
assert.Equal(t, errFoo, err2)
|
||||
|
||||
res3 := SequenceT3(t1, e2, t3)
|
||||
_, err3 := res3(defaultContext)
|
||||
assert.Equal(t, errFoo, err3)
|
||||
|
||||
res4 := SequenceT3(t1, t2, e3)
|
||||
_, err4 := res4(defaultContext)
|
||||
assert.Equal(t, errFoo, err4)
|
||||
}
|
||||
|
||||
func TestSequenceT4(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
t2 := Of[MyContext](2)
|
||||
t3 := Of[MyContext](true)
|
||||
t4 := Of[MyContext](1.0)
|
||||
|
||||
res := SequenceT4(t1, t2, t3, t4)
|
||||
|
||||
v, err := res(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, T.MakeTuple4("s1", 2, true, 1.0), v)
|
||||
}
|
||||
41
v2/idiomatic/readerresult/types.go
Normal file
41
v2/idiomatic/readerresult/types.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
Option[A any] = option.Option[A]
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Result[A any] = result.Result[A]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
ReaderResult[R, A any] = func(R) (A, error)
|
||||
Monoid[R, A any] = monoid.Monoid[ReaderResult[R, A]]
|
||||
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderResult[R, B]]
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderResult[R, A], B]
|
||||
)
|
||||
@@ -34,13 +34,11 @@ func MonadAlt[LAZY ~func() HKTFA, E, A, HKTFA any](
|
||||
|
||||
func Alt[LAZY ~func() HKTFA, E, A, HKTFA any](
|
||||
fof func(ET.Either[E, A]) HKTFA,
|
||||
fchain func(HKTFA, func(ET.Either[E, A]) HKTFA) HKTFA,
|
||||
fchain func(func(ET.Either[E, A]) HKTFA) func(HKTFA) HKTFA,
|
||||
|
||||
second LAZY) func(HKTFA) HKTFA {
|
||||
|
||||
return func(fa HKTFA) HKTFA {
|
||||
return MonadAlt(fof, fchain, fa, second)
|
||||
}
|
||||
return fchain(ET.Fold(F.Ignore1of1[E](second), F.Flow2(ET.Of[E, A], fof)))
|
||||
}
|
||||
|
||||
// HKTFA = HKT<F, Either<E, A>>
|
||||
|
||||
@@ -22,10 +22,12 @@ import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func FromOption[A, HKTEA, E any](fromEither func(ET.Either[E, A]) HKTEA, onNone func() E) func(ma O.Option[A]) HKTEA {
|
||||
return F.Flow2(ET.FromOption[A](onNone), fromEither)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromPredicate[E, A, HKTEA any](fromEither func(ET.Either[E, A]) HKTEA, pred func(A) bool, onFalse func(A) E) func(A) HKTEA {
|
||||
return F.Flow2(ET.FromPredicate(pred, onFalse), fromEither)
|
||||
}
|
||||
@@ -45,6 +47,7 @@ func MonadFromOption[E, A, HKTEA any](
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromOptionK[A, E, B, HKTEB any](
|
||||
fromEither func(ET.Either[E, B]) HKTEB,
|
||||
onNone func() E) func(f func(A) O.Option[B]) func(A) HKTEB {
|
||||
@@ -52,6 +55,7 @@ func FromOptionK[A, E, B, HKTEB any](
|
||||
return F.Bind2nd(F.Flow2[func(A) O.Option[B], func(O.Option[B]) HKTEB, A, O.Option[B], HKTEB], FromOption(fromEither, onNone))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainEitherK[A, E, B, HKTEA, HKTEB any](
|
||||
mchain func(HKTEA, func(A) HKTEB) HKTEB,
|
||||
fromEither func(ET.Either[E, B]) HKTEB,
|
||||
@@ -60,6 +64,7 @@ func MonadChainEitherK[A, E, B, HKTEA, HKTEB any](
|
||||
return mchain(ma, F.Flow2(f, fromEither))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainEitherK[A, E, B, HKTEA, HKTEB any](
|
||||
mchain func(func(A) HKTEB) func(HKTEA) HKTEB,
|
||||
fromEither func(ET.Either[E, B]) HKTEB,
|
||||
@@ -67,6 +72,7 @@ func ChainEitherK[A, E, B, HKTEA, HKTEB any](
|
||||
return mchain(F.Flow2(f, fromEither))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainOptionK[A, E, B, HKTEA, HKTEB any](
|
||||
mchain func(HKTEA, func(A) HKTEB) HKTEB,
|
||||
fromEither func(ET.Either[E, B]) HKTEB,
|
||||
@@ -75,6 +81,7 @@ func ChainOptionK[A, E, B, HKTEA, HKTEB any](
|
||||
return F.Flow2(FromOptionK[A](fromEither, onNone), F.Bind1st(F.Bind2nd[HKTEA, func(A) HKTEB, HKTEB], mchain))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[A, E, B, HKTEA, HKTEB any](
|
||||
mchain func(HKTEA, func(A) HKTEA) HKTEA,
|
||||
mmap func(HKTEB, func(B) A) HKTEA,
|
||||
@@ -84,6 +91,7 @@ func MonadChainFirstEitherK[A, E, B, HKTEA, HKTEB any](
|
||||
return C.MonadChainFirst(mchain, mmap, ma, F.Flow2(f, fromEither))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstEitherK[A, E, B, HKTEA, HKTEB any](
|
||||
mchain func(func(A) HKTEA) func(HKTEA) HKTEA,
|
||||
mmap func(func(B) A) func(HKTEB) HKTEA,
|
||||
@@ -91,3 +99,23 @@ func ChainFirstEitherK[A, E, B, HKTEA, HKTEB any](
|
||||
f func(A) ET.Either[E, B]) func(HKTEA) HKTEA {
|
||||
return C.ChainFirst(mchain, mmap, F.Flow2(f, fromEither))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindEitherK[
|
||||
E, S1, S2, T,
|
||||
HKTET,
|
||||
HKTES1,
|
||||
HKTES2 any](
|
||||
mchain func(func(S1) HKTES2) func(HKTES1) HKTES2,
|
||||
mmap func(func(T) S2) func(HKTET) HKTES2,
|
||||
fromEither func(ET.Either[E, T]) HKTET,
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ET.Either[E, T],
|
||||
) func(HKTES1) HKTES2 {
|
||||
return C.Bind(
|
||||
mchain,
|
||||
mmap,
|
||||
setter,
|
||||
F.Flow2(f, fromEither),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package fromreader
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
G "github.com/IBM/fp-go/v2/reader/generic"
|
||||
)
|
||||
|
||||
@@ -69,3 +70,24 @@ func ChainFirstReaderK[GB ~func(R) B, R, A, B, HKTRA, HKTRB any](
|
||||
) func(HKTRA) HKTRA {
|
||||
return mchain(FromReaderK(fromReader, f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderK[
|
||||
GT ~func(R) T,
|
||||
R, S1, S2, T,
|
||||
HKTET,
|
||||
HKTES1,
|
||||
HKTES2 any](
|
||||
mchain func(func(S1) HKTES2) func(HKTES1) HKTES2,
|
||||
mmap func(func(T) S2) func(HKTET) HKTES2,
|
||||
fromReader func(GT) HKTET,
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) GT,
|
||||
) func(HKTES1) HKTES2 {
|
||||
return C.Bind(
|
||||
mchain,
|
||||
mmap,
|
||||
setter,
|
||||
FromReaderK(fromReader, f),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package readereither
|
||||
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
@@ -35,6 +36,8 @@ import (
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
// result := readereither.Do[Env, error](State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[R, E, S any](
|
||||
empty S,
|
||||
) ReaderEither[R, E, S] {
|
||||
@@ -83,6 +86,8 @@ func Do[R, E, S any](
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ReaderEither[R, E, T],
|
||||
@@ -90,7 +95,59 @@ func Bind[R, E, S1, S2, T any](
|
||||
return G.Bind[ReaderEither[R, E, S1], ReaderEither[R, E, S2]](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderK[R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) Reader[R, T],
|
||||
) func(ReaderEither[R, E, S1]) ReaderEither[R, E, S2] {
|
||||
return G.BindReaderK[ReaderEither[R, E, S1], ReaderEither[R, E, S2]](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindEitherK[R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) Either[E, T],
|
||||
) func(ReaderEither[R, E, S1]) ReaderEither[R, E, S2] {
|
||||
return G.BindEitherK[ReaderEither[R, E, S1], ReaderEither[R, E, S2]](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToReader[
|
||||
R, E, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Reader[R, T]) ReaderEither[R, E, S1] {
|
||||
return G.BindToReader[ReaderEither[R, E, S1], Reader[R, T]](setter)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToEither[
|
||||
R, E, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(ET.Either[E, T]) ReaderEither[R, E, S1] {
|
||||
return G.BindToEither[ReaderEither[R, E, S1]](setter)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderS[
|
||||
R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[R, T],
|
||||
) Operator[R, E, S1, S2] {
|
||||
return G.ApReaderS[ReaderEither[R, E, S1], ReaderEither[R, E, S2]](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApEitherS[
|
||||
R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ET.Either[E, T],
|
||||
) Operator[R, E, S1, S2] {
|
||||
return G.ApEitherS[ReaderEither[R, E, S1], ReaderEither[R, E, S2]](setter, fa)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
|
||||
@@ -17,8 +17,11 @@ package generic
|
||||
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
FE "github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
FR "github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
@@ -36,6 +39,8 @@ import (
|
||||
// UserService UserService
|
||||
// }
|
||||
// result := generic.Do[ReaderEither[Env, error, State], Env, error, State](State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[GS ~func(R) ET.Either[E, S], R, E, S any](
|
||||
empty S,
|
||||
) GS {
|
||||
@@ -84,7 +89,12 @@ func Do[GS ~func(R) ET.Either[E, S], R, E, S any](
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[GS1 ~func(R) ET.Either[E, S1], GS2 ~func(R) ET.Either[E, S2], GT ~func(R) ET.Either[E, T], R, E, S1, S2, T any](
|
||||
//
|
||||
//go:inline
|
||||
func Bind[
|
||||
GS1 ~func(R) ET.Either[E, S1],
|
||||
GS2 ~func(R) ET.Either[E, S2],
|
||||
GT ~func(R) ET.Either[E, T], R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) GT,
|
||||
) func(GS1) GS2 {
|
||||
@@ -96,6 +106,41 @@ func Bind[GS1 ~func(R) ET.Either[E, S1], GS2 ~func(R) ET.Either[E, S2], GT ~func
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderK[
|
||||
GS1 ~func(R) ET.Either[E, S1],
|
||||
GS2 ~func(R) ET.Either[E, S2],
|
||||
GRT ~func(R) T,
|
||||
R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) GRT,
|
||||
) func(GS1) GS2 {
|
||||
return FR.BindReaderK(
|
||||
Chain[GS1, GS2, E, R, S1, S2],
|
||||
Map[func(R) ET.Either[E, T], GS2, E, R, T, S2],
|
||||
FromReader[GRT, func(R) ET.Either[E, T]],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindEitherK[
|
||||
GS1 ~func(R) ET.Either[E, S1],
|
||||
GS2 ~func(R) ET.Either[E, S2],
|
||||
R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ET.Either[E, T],
|
||||
) func(GS1) GS2 {
|
||||
return FE.BindEitherK(
|
||||
Chain[GS1, GS2, E, R, S1, S2],
|
||||
Map[func(R) ET.Either[E, T], GS2, E, R, T, S2],
|
||||
FromEither[func(R) ET.Either[E, T]],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
func Let[GS1 ~func(R) ET.Either[E, S1], GS2 ~func(R) ET.Either[E, S2], R, E, S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
@@ -130,6 +175,31 @@ func BindTo[GS1 ~func(R) ET.Either[E, S1], GT ~func(R) ET.Either[E, T], R, E, S1
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToReader[
|
||||
GS1 ~func(R) ET.Either[E, S1],
|
||||
GT ~func(R) T,
|
||||
R, E, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(GT) GS1 {
|
||||
return function.Flow2(
|
||||
FromReader[GT, func(R) ET.Either[E, T]],
|
||||
BindTo[GS1, func(R) ET.Either[E, T]](setter),
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToEither[
|
||||
GS1 ~func(R) ET.Either[E, S1],
|
||||
R, E, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(ET.Either[E, T]) GS1 {
|
||||
return function.Flow2(
|
||||
FromEither[func(R) ET.Either[E, T]],
|
||||
BindTo[GS1, func(R) ET.Either[E, T]](setter),
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
@@ -182,3 +252,32 @@ func ApS[GS1 ~func(R) ET.Either[E, S1], GS2 ~func(R) ET.Either[E, S2], GT ~func(
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderS[
|
||||
GS1 ~func(R) ET.Either[E, S1],
|
||||
GS2 ~func(R) ET.Either[E, S2],
|
||||
GT ~func(R) T,
|
||||
R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa GT,
|
||||
) func(GS1) GS2 {
|
||||
return ApS[GS1, GS2](
|
||||
setter,
|
||||
FromReader[GT, func(R) ET.Either[E, T]](fa),
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApEitherS[
|
||||
GS1 ~func(R) ET.Either[E, S1],
|
||||
GS2 ~func(R) ET.Either[E, S2],
|
||||
R, E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ET.Either[E, T],
|
||||
) func(GS1) GS2 {
|
||||
return ApS[GS1, GS2](
|
||||
setter,
|
||||
FromEither[func(R) ET.Either[E, T]](fa),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,23 @@ func Chain[GEA ~func(E) ET.Either[L, A], GEB ~func(E) ET.Either[L, B], L, E, A,
|
||||
return F.Bind2nd(MonadChain[GEA, GEB, L, E, A, B], f)
|
||||
}
|
||||
|
||||
func MonadChainReaderK[
|
||||
GEA ~func(E) ET.Either[L, A],
|
||||
GEB ~func(E) ET.Either[L, B],
|
||||
GB ~func(E) B,
|
||||
L, E, A, B any](ma GEA, f func(A) GB) GEB {
|
||||
|
||||
return MonadChain(ma, F.Flow2(f, FromReader[GB, GEB, L, E, B]))
|
||||
}
|
||||
|
||||
func ChainReaderK[
|
||||
GEA ~func(E) ET.Either[L, A],
|
||||
GEB ~func(E) ET.Either[L, B],
|
||||
GB ~func(E) B,
|
||||
L, E, A, B any](f func(A) GB) func(GEA) GEB {
|
||||
return Chain[GEA, GEB, L, E, A, B](F.Flow2(f, FromReader[GB, GEB, L, E, B]))
|
||||
}
|
||||
|
||||
func Of[GEA ~func(E) ET.Either[L, A], L, E, A any](a A) GEA {
|
||||
return readert.MonadOf[GEA](ET.Of[L, A], a)
|
||||
}
|
||||
|
||||
@@ -767,7 +767,7 @@ func MonadAlt[R, E, A any](first ReaderIOEither[R, E, A], second L.Lazy[ReaderIO
|
||||
func Alt[R, E, A any](second L.Lazy[ReaderIOEither[R, E, A]]) Operator[R, E, A, A] {
|
||||
return eithert.Alt(
|
||||
readerio.Of[R, Either[E, A]],
|
||||
readerio.MonadChain[R, Either[E, A], Either[E, A]],
|
||||
readerio.Chain[R, Either[E, A], Either[E, A]],
|
||||
|
||||
second,
|
||||
)
|
||||
|
||||
70
v2/readerresult/array.go
Normal file
70
v2/readerresult/array.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 readerresult
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
)
|
||||
|
||||
// TraverseArray applies a ReaderResult-returning function to each element of an array,
|
||||
// collecting the results. If any element fails, the entire operation fails with the first error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
|
||||
// ids := []int{1, 2, 3}
|
||||
// result := readerresult.TraverseArray[DB](parseUser)(ids)
|
||||
// // result(db) returns result.Result[[]User] with all users or first error
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArray[L, A, B any](f Kleisli[L, A, B]) Kleisli[L, []A, []B] {
|
||||
return G.TraverseArray[ReaderResult[L, B], ReaderResult[L, []B], []A](f)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex is like TraverseArray but the function also receives the element's index.
|
||||
// This is useful when the transformation depends on the position in the array.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// processItem := func(idx int, item string) readerresult.ReaderResult[Config, int] {
|
||||
// return readerresult.Of[Config](idx + len(item))
|
||||
// }
|
||||
// items := []string{"a", "bb", "ccc"}
|
||||
// result := readerresult.TraverseArrayWithIndex[Config](processItem)(items)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArrayWithIndex[L, A, B any](f func(int, A) ReaderResult[L, B]) Kleisli[L, []A, []B] {
|
||||
return G.TraverseArrayWithIndex[ReaderResult[L, B], ReaderResult[L, []B], []A](f)
|
||||
}
|
||||
|
||||
// SequenceArray converts an array of ReaderResult values into a single ReaderResult of an array.
|
||||
// If any element fails, the entire operation fails with the first error encountered.
|
||||
// All computations share the same environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readers := []readerresult.ReaderResult[Config, int]{
|
||||
// readerresult.Of[Config](1),
|
||||
// readerresult.Of[Config](2),
|
||||
// readerresult.Of[Config](3),
|
||||
// }
|
||||
// result := readerresult.SequenceArray(readers)
|
||||
// // result(cfg) returns result.Of([]int{1, 2, 3})
|
||||
//
|
||||
//go:inline
|
||||
func SequenceArray[L, A any](ma []ReaderResult[L, A]) ReaderResult[L, []A] {
|
||||
return G.SequenceArray[ReaderResult[L, A], ReaderResult[L, []A]](ma)
|
||||
}
|
||||
41
v2/readerresult/array_test.go
Normal file
41
v2/readerresult/array_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
|
||||
n := 10
|
||||
|
||||
readers := A.MakeBy(n, Of[context.Context, int])
|
||||
exp := result.Of(A.MakeBy(n, F.Identity[int]))
|
||||
|
||||
g := F.Pipe1(
|
||||
readers,
|
||||
SequenceArray[context.Context, int],
|
||||
)
|
||||
|
||||
assert.Equal(t, exp, g(context.Background()))
|
||||
}
|
||||
286
v2/readerresult/benchmark_test.go
Normal file
286
v2/readerresult/benchmark_test.go
Normal file
@@ -0,0 +1,286 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
type BenchContext struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
var benchError = errors.New("benchmark error")
|
||||
|
||||
// Benchmark basic operations
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Of[BenchContext](i)
|
||||
_ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := benchError
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Left[BenchContext, int](err)
|
||||
_ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
double := func(x int) int { return x * 2 }
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := F.Pipe1(rr, Map[BenchContext](double))
|
||||
_ = mapped(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
double := func(x int) int { return x * 2 }
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
chained := F.Pipe1(rr, Chain(addOne))
|
||||
_ = chained(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainDeep(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAp(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
fab := Of[BenchContext](func(x int) int { return x * 2 })
|
||||
fa := Of[BenchContext](21)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := MonadAp(fab, fa)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT2(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT2(rr1, rr2)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT4(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
rr3 := Of[BenchContext](30)
|
||||
rr4 := Of[BenchContext](40)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT4(rr1, rr2, rr3, rr4)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDoNotation(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
|
||||
type State struct {
|
||||
A int
|
||||
B int
|
||||
C int
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
Bind(
|
||||
func(a int) func(State) State {
|
||||
return func(s State) State { s.A = a; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](10)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(b int) func(State) State {
|
||||
return func(s State) State { s.B = b; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A * 2)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(c int) func(State) State {
|
||||
return func(s State) State { s.C = c; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A + s.B)
|
||||
},
|
||||
),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErrorPropagation(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := benchError
|
||||
rr := Left[BenchContext, int](err)
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
kleisli := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x * 2)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
traversed := TraverseArray[BenchContext](kleisli)
|
||||
result := traversed(arr)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []ReaderResult[BenchContext, int]{
|
||||
Of[BenchContext](1),
|
||||
Of[BenchContext](2),
|
||||
Of[BenchContext](3),
|
||||
Of[BenchContext](4),
|
||||
Of[BenchContext](5),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceArray(arr)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Real-world scenario benchmarks
|
||||
|
||||
func BenchmarkRealWorldPipeline(b *testing.B) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
Offset int
|
||||
}
|
||||
|
||||
ctx := Config{Multiplier: 5, Offset: 10}
|
||||
|
||||
type State struct {
|
||||
Input int
|
||||
Result int
|
||||
}
|
||||
|
||||
getMultiplier := func(cfg Config) int { return cfg.Multiplier }
|
||||
getOffset := func(cfg Config) int { return cfg.Offset }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
step1 := Bind(
|
||||
func(m int) func(State) State {
|
||||
return func(s State) State { s.Result = s.Input * m; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getMultiplier)
|
||||
},
|
||||
)
|
||||
step2 := Bind(
|
||||
func(off int) func(State) State {
|
||||
return func(s State) State { s.Result += off; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getOffset)
|
||||
},
|
||||
)
|
||||
result := F.Pipe3(
|
||||
Do[Config](State{Input: 10}),
|
||||
step1,
|
||||
step2,
|
||||
Map[Config](func(s State) int { return s.Result }),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
599
v2/readerresult/bind.go
Normal file
599
v2/readerresult/bind.go
Normal file
@@ -0,0 +1,599 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
// result := readereither.Do[Env, error](State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[R, S any](
|
||||
empty S,
|
||||
) ReaderResult[R, S] {
|
||||
return G.Do[ReaderResult[R, S]](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the shared environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderResult[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// readereither.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderResult[Env, error, Config] {
|
||||
// // This can access s.User from the previous step
|
||||
// return readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfigForUser(s.User.ID)
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[R, S1, T],
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
return G.Bind[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
return G.Let[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
return G.LetTo[ReaderResult[R, S1], ReaderResult[R, S2]](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[R, T, S1] {
|
||||
return G.BindTo[ReaderResult[R, S1], ReaderResult[R, T]](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// readereither.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderResult[R, T],
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
return G.ApS[ReaderResult[R, S1], ReaderResult[R, S2]](setter, fa)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
// result := F.Pipe2(
|
||||
// readereither.Of[Env, error](State{}),
|
||||
// readereither.ApSL(configLens, getConfig),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderResult[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a ReaderEither computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.BindL(userLens, func(user User) readereither.ReaderResult[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[R, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a ReaderEither).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
|
||||
// readereither.LetL(configLens, func(cfg Config) Config {
|
||||
// cfg.Port = 8080
|
||||
// return cfg
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[R, S, S] {
|
||||
return Let[R](lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// newConfig := Config{Host: "localhost", Port: 8080}
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[any, error](State{}),
|
||||
// readereither.LetToL(configLens, newConfig),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[R, S, S] {
|
||||
return LetTo[R](lens.Set, b)
|
||||
}
|
||||
|
||||
// BindReaderK lifts a Reader Kleisli arrow into a ReaderResult context and binds it to the state.
|
||||
// This allows you to integrate pure Reader computations (that don't have error handling)
|
||||
// into a ReaderResult computation chain.
|
||||
//
|
||||
// The function f takes the current state S1 and returns a Reader[R, T] computation.
|
||||
// The result T is then attached to the state using the setter to produce state S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// ConfigPath string
|
||||
// }
|
||||
// type State struct {
|
||||
// Config string
|
||||
// }
|
||||
//
|
||||
// // A pure Reader computation that reads from environment
|
||||
// getConfigPath := func(s State) reader.Reader[Env, string] {
|
||||
// return func(env Env) string {
|
||||
// return env.ConfigPath
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.BindReaderK(
|
||||
// func(path string) func(State) State {
|
||||
// return func(s State) State { s.Config = path; return s }
|
||||
// },
|
||||
// getConfigPath,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f reader.Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return G.BindReaderK[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindEitherK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f result.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return G.BindEitherK[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
// BindResultK lifts a Result Kleisli arrow into a ReaderResult context and binds it to the state.
|
||||
// This allows you to integrate Result computations (that may fail with an error but don't need
|
||||
// environment access) into a ReaderResult computation chain.
|
||||
//
|
||||
// The function f takes the current state S1 and returns a Result[T] computation.
|
||||
// If the Result is successful, the value T is attached to the state using the setter to produce state S2.
|
||||
// If the Result is an error, the entire computation short-circuits with that error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// ParsedValue int
|
||||
// }
|
||||
//
|
||||
// // A Result computation that may fail
|
||||
// parseValue := func(s State) result.Result[int] {
|
||||
// if s.Value < 0 {
|
||||
// return result.Error[int](errors.New("negative value"))
|
||||
// }
|
||||
// return result.Of(s.Value * 2)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[any](State{Value: 5}),
|
||||
// readerresult.BindResultK(
|
||||
// func(parsed int) func(State) State {
|
||||
// return func(s State) State { s.ParsedValue = parsed; return s }
|
||||
// },
|
||||
// parseValue,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindResultK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f result.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return G.BindEitherK[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
// BindToReader initializes a new state S1 from a Reader[R, T] computation.
|
||||
// This is used to start a ReaderResult computation chain from a pure Reader value.
|
||||
//
|
||||
// The setter function takes the result T from the Reader and initializes the state S1.
|
||||
// This is useful when you want to begin a do-notation chain with a Reader computation
|
||||
// that doesn't involve error handling.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// ConfigPath string
|
||||
// }
|
||||
// type State struct {
|
||||
// Config string
|
||||
// }
|
||||
//
|
||||
// // A Reader that just reads from the environment
|
||||
// getConfigPath := func(env Env) string {
|
||||
// return env.ConfigPath
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// reader.Of[Env](getConfigPath),
|
||||
// readerresult.BindToReader(func(path string) State {
|
||||
// return State{Config: path}
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindToReader[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Reader[R, T]) ReaderResult[R, S1] {
|
||||
return G.BindToReader[ReaderResult[R, S1], Reader[R, T]](setter)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToEither[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Result[T]) ReaderResult[R, S1] {
|
||||
return G.BindToEither[ReaderResult[R, S1]](setter)
|
||||
}
|
||||
|
||||
// BindToResult initializes a new state S1 from a Result[T] value.
|
||||
// This is used to start a ReaderResult computation chain from a Result that may contain an error.
|
||||
//
|
||||
// The setter function takes the successful result T and initializes the state S1.
|
||||
// If the Result is an error, the entire computation will carry that error forward.
|
||||
// This is useful when you want to begin a do-notation chain with a Result computation
|
||||
// that doesn't need environment access.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// // A Result that might contain an error
|
||||
// parseResult := result.TryCatch(func() int {
|
||||
// // some parsing logic that might fail
|
||||
// return 42
|
||||
// })
|
||||
//
|
||||
// computation := F.Pipe1(
|
||||
// parseResult,
|
||||
// readerresult.BindToResult[any](func(value int) State {
|
||||
// return State{Value: value}
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindToResult[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Result[T]) ReaderResult[R, S1] {
|
||||
return G.BindToEither[ReaderResult[R, S1]](setter)
|
||||
}
|
||||
|
||||
// ApReaderS attaches a value from a pure Reader computation to a context [S1] to produce a context [S2]
|
||||
// using Applicative semantics (independent, non-sequential composition).
|
||||
//
|
||||
// Unlike BindReaderK which uses monadic bind (sequential), ApReaderS uses applicative apply,
|
||||
// meaning the Reader computation fa is independent of the current state and can conceptually
|
||||
// execute in parallel.
|
||||
//
|
||||
// This is useful when you want to combine a Reader computation with your ReaderResult state
|
||||
// without creating a dependency between them.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// DefaultPort int
|
||||
// DefaultHost string
|
||||
// }
|
||||
// type State struct {
|
||||
// Port int
|
||||
// Host string
|
||||
// }
|
||||
//
|
||||
// getDefaultPort := func(env Env) int { return env.DefaultPort }
|
||||
// getDefaultHost := func(env Env) string { return env.DefaultHost }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.ApReaderS(
|
||||
// func(port int) func(State) State {
|
||||
// return func(s State) State { s.Port = port; return s }
|
||||
// },
|
||||
// getDefaultPort,
|
||||
// ),
|
||||
// readerresult.ApReaderS(
|
||||
// func(host string) func(State) State {
|
||||
// return func(s State) State { s.Host = host; return s }
|
||||
// },
|
||||
// getDefaultHost,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return G.ApReaderS[ReaderResult[R, S1], ReaderResult[R, S2]](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApEitherS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Result[T],
|
||||
) Operator[R, S1, S2] {
|
||||
return G.ApEitherS[ReaderResult[R, S1], ReaderResult[R, S2]](setter, fa)
|
||||
}
|
||||
|
||||
// ApResultS attaches a value from a Result to a context [S1] to produce a context [S2]
|
||||
// using Applicative semantics (independent, non-sequential composition).
|
||||
//
|
||||
// Unlike BindResultK which uses monadic bind (sequential), ApResultS uses applicative apply,
|
||||
// meaning the Result computation fa is independent of the current state and can conceptually
|
||||
// execute in parallel.
|
||||
//
|
||||
// If the Result fa contains an error, the entire computation short-circuits with that error.
|
||||
// This is useful when you want to combine a Result value with your ReaderResult state
|
||||
// without creating a dependency between them.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value1 int
|
||||
// Value2 int
|
||||
// }
|
||||
//
|
||||
// // Independent Result computations
|
||||
// parseValue1 := result.TryCatch(func() int { return 42 })
|
||||
// parseValue2 := result.TryCatch(func() int { return 100 })
|
||||
//
|
||||
// computation := F.Pipe2(
|
||||
// readerresult.Do[any](State{}),
|
||||
// readerresult.ApResultS(
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value1 = v; return s }
|
||||
// },
|
||||
// parseValue1,
|
||||
// ),
|
||||
// readerresult.ApResultS(
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value2 = v; return s }
|
||||
// },
|
||||
// parseValue2,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApResultS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Result[T],
|
||||
) Operator[R, S1, S2] {
|
||||
return G.ApEitherS[ReaderResult[R, S1], ReaderResult[R, S2]](setter, fa)
|
||||
}
|
||||
346
v2/readerresult/bind_test.go
Normal file
346
v2/readerresult/bind_test.go
Normal file
@@ -0,0 +1,346 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getLastName(s utils.Initial) ReaderResult[context.Context, string] {
|
||||
return Of[context.Context]("Doe")
|
||||
}
|
||||
|
||||
func getGivenName(s utils.WithLastName) ReaderResult[context.Context, string] {
|
||||
return Of[context.Context]("John")
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
Bind(utils.SetLastName, getLastName),
|
||||
Bind(utils.SetGivenName, getGivenName),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(context.Background()), result.Of("John Doe"))
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
ApS(utils.SetLastName, Of[context.Context]("Doe")),
|
||||
ApS(utils.SetGivenName, Of[context.Context]("John")),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(context.Background()), result.Of("John Doe"))
|
||||
}
|
||||
|
||||
func TestBindReaderK(t *testing.T) {
|
||||
type Env struct {
|
||||
ConfigPath string
|
||||
}
|
||||
type State struct {
|
||||
Config string
|
||||
}
|
||||
|
||||
// A pure Reader computation
|
||||
getConfigPath := func(s State) func(Env) string {
|
||||
return func(env Env) string {
|
||||
return env.ConfigPath
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[Env](State{}),
|
||||
BindReaderK(
|
||||
func(path string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Config = path
|
||||
return s
|
||||
}
|
||||
},
|
||||
getConfigPath,
|
||||
),
|
||||
Map[Env](func(s State) string { return s.Config }),
|
||||
)
|
||||
|
||||
env := Env{ConfigPath: "/etc/config"}
|
||||
assert.Equal(t, result.Of("/etc/config"), res(env))
|
||||
}
|
||||
|
||||
func TestBindResultK(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
ParsedValue int
|
||||
}
|
||||
|
||||
// A Result computation that may fail
|
||||
parseValue := func(s State) result.Result[int] {
|
||||
if s.Value < 0 {
|
||||
return result.Left[int](assert.AnError)
|
||||
}
|
||||
return result.Of(s.Value * 2)
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[context.Context](State{Value: 5}),
|
||||
BindResultK[context.Context](
|
||||
func(parsed int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.ParsedValue = parsed
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[context.Context](func(s State) int { return s.ParsedValue }),
|
||||
)
|
||||
|
||||
assert.Equal(t, result.Of(10), res(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[context.Context](State{Value: -5}),
|
||||
BindResultK[context.Context](
|
||||
func(parsed int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.ParsedValue = parsed
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[context.Context](func(s State) int { return s.ParsedValue }),
|
||||
)
|
||||
|
||||
assert.True(t, result.IsLeft(res(context.Background())))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindToReader(t *testing.T) {
|
||||
type Env struct {
|
||||
ConfigPath string
|
||||
}
|
||||
type State struct {
|
||||
Config string
|
||||
}
|
||||
|
||||
// A Reader that just reads from the environment
|
||||
getConfigPath := func(env Env) string {
|
||||
return env.ConfigPath
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
getConfigPath,
|
||||
BindToReader[Env](func(path string) State {
|
||||
return State{Config: path}
|
||||
}),
|
||||
Map[Env](func(s State) string { return s.Config }),
|
||||
)
|
||||
|
||||
env := Env{ConfigPath: "/etc/config"}
|
||||
assert.Equal(t, result.Of("/etc/config"), res(env))
|
||||
}
|
||||
|
||||
func TestBindToResult(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
parseResult := result.Of(42)
|
||||
|
||||
computation := F.Pipe2(
|
||||
parseResult,
|
||||
BindToResult[context.Context](func(value int) State {
|
||||
return State{Value: value}
|
||||
}),
|
||||
Map[context.Context](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
assert.Equal(t, result.Of(42), computation(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
parseResult := result.Left[int](assert.AnError)
|
||||
|
||||
computation := F.Pipe2(
|
||||
parseResult,
|
||||
BindToResult[context.Context](func(value int) State {
|
||||
return State{Value: value}
|
||||
}),
|
||||
Map[context.Context](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
assert.True(t, result.IsLeft(computation(context.Background())))
|
||||
})
|
||||
}
|
||||
|
||||
func TestApReaderS(t *testing.T) {
|
||||
type Env struct {
|
||||
DefaultPort int
|
||||
DefaultHost string
|
||||
}
|
||||
type State struct {
|
||||
Port int
|
||||
Host string
|
||||
}
|
||||
|
||||
getDefaultPort := func(env Env) int { return env.DefaultPort }
|
||||
getDefaultHost := func(env Env) string { return env.DefaultHost }
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[Env](State{}),
|
||||
ApReaderS(
|
||||
func(port int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Port = port
|
||||
return s
|
||||
}
|
||||
},
|
||||
getDefaultPort,
|
||||
),
|
||||
ApReaderS(
|
||||
func(host string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Host = host
|
||||
return s
|
||||
}
|
||||
},
|
||||
getDefaultHost,
|
||||
),
|
||||
Map[Env](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
env := Env{DefaultPort: 8080, DefaultHost: "localhost"}
|
||||
r := res(env)
|
||||
assert.True(t, result.IsRight(r))
|
||||
state := result.GetOrElse(func(error) State { return State{} })(r)
|
||||
assert.Equal(t, 8080, state.Port)
|
||||
assert.Equal(t, "localhost", state.Host)
|
||||
}
|
||||
|
||||
func TestApResultS(t *testing.T) {
|
||||
type State struct {
|
||||
Value1 int
|
||||
Value2 int
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
parseValue1 := result.Of(42)
|
||||
parseValue2 := result.Of(100)
|
||||
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue1,
|
||||
),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue2,
|
||||
),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
r := computation(context.Background())
|
||||
assert.True(t, result.IsRight(r))
|
||||
state := result.GetOrElse(func(error) State { return State{} })(r)
|
||||
assert.Equal(t, 42, state.Value1)
|
||||
assert.Equal(t, 100, state.Value2)
|
||||
})
|
||||
|
||||
t.Run("error in first value", func(t *testing.T) {
|
||||
parseValue1 := result.Left[int](assert.AnError)
|
||||
parseValue2 := result.Of(100)
|
||||
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue1,
|
||||
),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue2,
|
||||
),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
assert.True(t, result.IsLeft(computation(context.Background())))
|
||||
})
|
||||
|
||||
t.Run("error in second value", func(t *testing.T) {
|
||||
parseValue1 := result.Of(42)
|
||||
parseValue2 := result.Left[int](assert.AnError)
|
||||
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue1,
|
||||
),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue2,
|
||||
),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
assert.True(t, result.IsLeft(computation(context.Background())))
|
||||
})
|
||||
}
|
||||
105
v2/readerresult/curry.go
Normal file
105
v2/readerresult/curry.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
)
|
||||
|
||||
// These functions curry/uncurry Go functions with context as the first parameter into/from ReaderResult form.
|
||||
// This follows the Go convention of putting context as the first parameter as advised in https://pkg.go.dev/context.
|
||||
//
|
||||
// Unlike the From* functions which return partially applied functions, Curry* functions return fully curried
|
||||
// functions where each parameter is applied one at a time.
|
||||
|
||||
// Curry0 converts a context-only function into a ReaderResult (same as From0 but emphasizes immediate application).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getConfig := func(ctx context.Context) (Config, error) { ... }
|
||||
// rr := readerresult.Curry0(getConfig)
|
||||
// // rr is a ReaderResult[context.Context, Config]
|
||||
func Curry0[R, A any](f func(R) (A, error)) ReaderResult[R, A] {
|
||||
return G.Curry0[ReaderResult[R, A]](f)
|
||||
}
|
||||
|
||||
// Curry1 converts a function with one parameter into a curried function returning a ReaderResult.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := func(ctx context.Context, id int) (User, error) { ... }
|
||||
// curried := readerresult.Curry1(getUser)
|
||||
// // curried(42) returns ReaderResult[context.Context, User]
|
||||
func Curry1[R, T1, A any](f func(R, T1) (A, error)) func(T1) ReaderResult[R, A] {
|
||||
return G.Curry1[ReaderResult[R, A]](f)
|
||||
}
|
||||
|
||||
// Curry2 converts a function with two parameters into a fully curried function.
|
||||
// Each parameter is applied one at a time.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// queryDB := func(ctx context.Context, table string, id int) (Record, error) { ... }
|
||||
// curried := readerresult.Curry2(queryDB)
|
||||
// // curried("users")(42) returns ReaderResult[context.Context, Record]
|
||||
func Curry2[R, T1, T2, A any](f func(R, T1, T2) (A, error)) func(T1) func(T2) ReaderResult[R, A] {
|
||||
return G.Curry2[ReaderResult[R, A]](f)
|
||||
}
|
||||
|
||||
// Curry3 converts a function with three parameters into a fully curried function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// updateRecord := func(ctx context.Context, table string, id int, data string) (Result, error) { ... }
|
||||
// curried := readerresult.Curry3(updateRecord)
|
||||
// // curried("users")(42)("data") returns ReaderResult[context.Context, Result]
|
||||
func Curry3[R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderResult[R, A] {
|
||||
return G.Curry3[ReaderResult[R, A]](f)
|
||||
}
|
||||
|
||||
// Uncurry1 converts a ReaderResult-returning function back into an idiomatic Go function.
|
||||
// This is useful for adapting functional code to work with traditional Go APIs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(id int) readerresult.ReaderResult[context.Context, User] { ... }
|
||||
// gofunc := readerresult.Uncurry1(rrf)
|
||||
// // gofunc(ctx, 42) returns (User, error)
|
||||
func Uncurry1[R, T1, A any](f func(T1) ReaderResult[R, A]) func(R, T1) (A, error) {
|
||||
return G.Uncurry1(f)
|
||||
}
|
||||
|
||||
// Uncurry2 converts a curried two-parameter ReaderResult function into an idiomatic Go function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(table string) func(int) readerresult.ReaderResult[context.Context, Record] { ... }
|
||||
// gofunc := readerresult.Uncurry2(rrf)
|
||||
// // gofunc(ctx, "users", 42) returns (Record, error)
|
||||
func Uncurry2[R, T1, T2, A any](f func(T1) func(T2) ReaderResult[R, A]) func(R, T1, T2) (A, error) {
|
||||
return G.Uncurry2(f)
|
||||
}
|
||||
|
||||
// Uncurry3 converts a curried three-parameter ReaderResult function into an idiomatic Go function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(table string) func(int) func(string) readerresult.ReaderResult[context.Context, Result] { ... }
|
||||
// gofunc := readerresult.Uncurry3(rrf)
|
||||
// // gofunc(ctx, "users", 42, "data") returns (Result, error)
|
||||
func Uncurry3[R, T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderResult[R, A]) func(R, T1, T2, T3) (A, error) {
|
||||
return G.Uncurry3(f)
|
||||
}
|
||||
175
v2/readerresult/curry_test.go
Normal file
175
v2/readerresult/curry_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCurry0(t *testing.T) {
|
||||
getConfig := func(ctx context.Context) (string, error) {
|
||||
return "config", nil
|
||||
}
|
||||
|
||||
rr := Curry0(getConfig)
|
||||
res := rr(context.Background())
|
||||
assert.Equal(t, result.Of("config"), res)
|
||||
}
|
||||
|
||||
func TestCurry1(t *testing.T) {
|
||||
getUser := func(ctx context.Context, id int) (string, error) {
|
||||
if id == 42 {
|
||||
return "Alice", nil
|
||||
}
|
||||
return "", errors.New("user not found")
|
||||
}
|
||||
|
||||
curried := Curry1(getUser)
|
||||
|
||||
res1 := curried(42)(context.Background())
|
||||
assert.Equal(t, result.Of("Alice"), res1)
|
||||
|
||||
res2 := curried(99)(context.Background())
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestCurry2(t *testing.T) {
|
||||
queryDB := func(ctx context.Context, table string, id int) (string, error) {
|
||||
if table == "users" && id == 42 {
|
||||
return "record", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
curried := Curry2(queryDB)
|
||||
|
||||
res1 := curried("users")(42)(context.Background())
|
||||
assert.Equal(t, result.Of("record"), res1)
|
||||
|
||||
res2 := curried("posts")(1)(context.Background())
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestCurry3(t *testing.T) {
|
||||
updateRecord := func(ctx context.Context, table string, id int, data string) (string, error) {
|
||||
if table == "users" && id == 42 && data == "updated" {
|
||||
return "success", nil
|
||||
}
|
||||
return "", errors.New("update failed")
|
||||
}
|
||||
|
||||
curried := Curry3(updateRecord)
|
||||
|
||||
res1 := curried("users")(42)("updated")(context.Background())
|
||||
assert.Equal(t, result.Of("success"), res1)
|
||||
|
||||
res2 := curried("posts")(1)("data")(context.Background())
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestUncurry1(t *testing.T) {
|
||||
rrf := func(id int) ReaderResult[context.Context, string] {
|
||||
if id == 42 {
|
||||
return Of[context.Context]("Alice")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("user not found"))
|
||||
}
|
||||
|
||||
gofunc := Uncurry1(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), 42)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "Alice", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), 99)
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
func TestUncurry2(t *testing.T) {
|
||||
rrf := func(table string) func(int) ReaderResult[context.Context, string] {
|
||||
return func(id int) ReaderResult[context.Context, string] {
|
||||
if table == "users" && id == 42 {
|
||||
return Of[context.Context]("record")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("not found"))
|
||||
}
|
||||
}
|
||||
|
||||
gofunc := Uncurry2(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), "users", 42)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "record", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), "posts", 1)
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
func TestUncurry3(t *testing.T) {
|
||||
rrf := func(table string) func(int) func(string) ReaderResult[context.Context, string] {
|
||||
return func(id int) func(string) ReaderResult[context.Context, string] {
|
||||
return func(data string) ReaderResult[context.Context, string] {
|
||||
if table == "users" && id == 42 && data == "updated" {
|
||||
return Of[context.Context]("success")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("update failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gofunc := Uncurry3(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), "users", 42, "updated")
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "success", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), "posts", 1, "data")
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
// Test round-trip conversions
|
||||
func TestCurryUncurryRoundTrip(t *testing.T) {
|
||||
// Original Go function
|
||||
original := func(ctx context.Context, id int) (string, error) {
|
||||
if id == 42 {
|
||||
return "Alice", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
// Curry then uncurry
|
||||
curried := Curry1(original)
|
||||
uncurried := Uncurry1(curried)
|
||||
|
||||
// Should behave the same as original
|
||||
res1, err1 := original(context.Background(), 42)
|
||||
res2, err2 := uncurried(context.Background(), 42)
|
||||
assert.Equal(t, res1, res2)
|
||||
assert.Equal(t, err1, err2)
|
||||
|
||||
res3, err3 := original(context.Background(), 99)
|
||||
res4, err4 := uncurried(context.Background(), 99)
|
||||
assert.Equal(t, res3, res4)
|
||||
assert.Equal(t, err3, err4)
|
||||
}
|
||||
110
v2/readerresult/doc.go
Normal file
110
v2/readerresult/doc.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// 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 readerresult provides a ReaderResult monad that combines the Reader and Result monads.
|
||||
//
|
||||
// A ReaderResult[R, A] represents a computation that:
|
||||
// - Depends on an environment of type R (Reader aspect)
|
||||
// - May fail with an error (Result aspect, which is Either[error, A])
|
||||
//
|
||||
// This is equivalent to Reader[R, Result[A]] or Reader[R, Either[error, A]].
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// ReaderResult is particularly useful for:
|
||||
//
|
||||
// 1. Dependency injection with error handling - pass configuration/services through
|
||||
// computations that may fail
|
||||
// 2. Functional error handling - compose operations that depend on context and may error
|
||||
// 3. Testing - easily mock dependencies by changing the environment value
|
||||
//
|
||||
// # Basic Example
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // Function that needs config and may fail
|
||||
// func getUser(id int) readerresult.ReaderResult[Config, User] {
|
||||
// return readerresult.Asks(func(cfg Config) result.Result[User] {
|
||||
// // Use cfg.DatabaseURL to fetch user
|
||||
// return result.Of(user)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Execute by providing the config
|
||||
// cfg := Config{DatabaseURL: "postgres://..."}
|
||||
// res := getUser(42)(cfg) // Returns result.Result[User]
|
||||
//
|
||||
// # Composition
|
||||
//
|
||||
// ReaderResult provides several ways to compose computations:
|
||||
//
|
||||
// 1. Map - transform successful values
|
||||
// 2. Chain (FlatMap) - sequence dependent operations
|
||||
// 3. Ap - combine independent computations
|
||||
// 4. Do-notation - imperative-style composition with Bind
|
||||
//
|
||||
// # Do-Notation Example
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[Config](State{}),
|
||||
// readerresult.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readerresult.ReaderResult[Config, User] {
|
||||
// return getUser(42)
|
||||
// },
|
||||
// ),
|
||||
// readerresult.Bind(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// func(s State) readerresult.ReaderResult[Config, []Post] {
|
||||
// return getPosts(s.User.ID)
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// ReaderResult provides several functions for error handling:
|
||||
//
|
||||
// - Left/Right - create failed/successful values
|
||||
// - GetOrElse - provide a default value for errors
|
||||
// - OrElse - recover from errors with an alternative computation
|
||||
// - Fold - handle both success and failure cases
|
||||
// - ChainEitherK - lift result.Result computations into ReaderResult
|
||||
//
|
||||
// # Relationship to Other Monads
|
||||
//
|
||||
// ReaderResult is related to several other monads in this library:
|
||||
//
|
||||
// - Reader[R, A] - ReaderResult without error handling
|
||||
// - Result[A] (Either[error, A]) - error handling without environment
|
||||
// - ReaderEither[R, E, A] - like ReaderResult but with custom error type E
|
||||
// - IOResult[A] - like ReaderResult but with no environment (IO with errors)
|
||||
//
|
||||
// # Performance Note
|
||||
//
|
||||
// ReaderResult is a zero-cost abstraction - it compiles to a simple function type
|
||||
// with no runtime overhead beyond the underlying computation.
|
||||
package readerresult
|
||||
71
v2/readerresult/from.go
Normal file
71
v2/readerresult/from.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
)
|
||||
|
||||
// These functions convert idiomatic Go functions (with context as first parameter and (value, error) return)
|
||||
// into ReaderResult computations. This follows the Go convention of putting context as the first parameter
|
||||
// as advised in https://pkg.go.dev/context.
|
||||
|
||||
// From0 converts a function that takes only a context and returns (A, error) into a ReaderResult.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getConfig := func(ctx context.Context) (Config, error) { ... }
|
||||
// rr := readerresult.From0(getConfig)()
|
||||
// // rr is a ReaderResult[context.Context, Config]
|
||||
func From0[R, A any](f func(R) (A, error)) func() ReaderResult[R, A] {
|
||||
return G.From0[ReaderResult[R, A]](f)
|
||||
}
|
||||
|
||||
// From1 converts a function with one parameter into a ReaderResult-returning function.
|
||||
// The context parameter is moved to the end (ReaderResult style).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := func(ctx context.Context, id int) (User, error) { ... }
|
||||
// rr := readerresult.From1(getUser)
|
||||
// // rr(42) returns ReaderResult[context.Context, User]
|
||||
func From1[R, T1, A any](f func(R, T1) (A, error)) func(T1) ReaderResult[R, A] {
|
||||
return G.From1[ReaderResult[R, A]](f)
|
||||
}
|
||||
|
||||
// From2 converts a function with two parameters into a ReaderResult-returning function.
|
||||
// The context parameter is moved to the end (ReaderResult style).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// queryDB := func(ctx context.Context, table string, id int) (Record, error) { ... }
|
||||
// rr := readerresult.From2(queryDB)
|
||||
// // rr("users", 42) returns ReaderResult[context.Context, Record]
|
||||
func From2[R, T1, T2, A any](f func(R, T1, T2) (A, error)) func(T1, T2) ReaderResult[R, A] {
|
||||
return G.From2[ReaderResult[R, A]](f)
|
||||
}
|
||||
|
||||
// From3 converts a function with three parameters into a ReaderResult-returning function.
|
||||
// The context parameter is moved to the end (ReaderResult style).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// updateRecord := func(ctx context.Context, table string, id int, data string) (Result, error) { ... }
|
||||
// rr := readerresult.From3(updateRecord)
|
||||
// // rr("users", 42, "data") returns ReaderResult[context.Context, Result]
|
||||
func From3[R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, error)) func(T1, T2, T3) ReaderResult[R, A] {
|
||||
return G.From3[ReaderResult[R, A]](f)
|
||||
}
|
||||
95
v2/readerresult/from_test.go
Normal file
95
v2/readerresult/from_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFrom0(t *testing.T) {
|
||||
getConfig := func(ctx context.Context) (string, error) {
|
||||
return "config", nil
|
||||
}
|
||||
|
||||
rr := From0(getConfig)()
|
||||
res := rr(context.Background())
|
||||
assert.Equal(t, result.Of("config"), res)
|
||||
|
||||
// Test with error
|
||||
getConfigErr := func(ctx context.Context) (string, error) {
|
||||
return "", errors.New("config error")
|
||||
}
|
||||
|
||||
rrErr := From0(getConfigErr)()
|
||||
resErr := rrErr(context.Background())
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
}
|
||||
|
||||
func TestFrom1(t *testing.T) {
|
||||
getUser := func(ctx context.Context, id int) (string, error) {
|
||||
if id == 42 {
|
||||
return "Alice", nil
|
||||
}
|
||||
return "", errors.New("user not found")
|
||||
}
|
||||
|
||||
rr := From1(getUser)
|
||||
|
||||
res1 := rr(42)(context.Background())
|
||||
assert.Equal(t, result.Of("Alice"), res1)
|
||||
|
||||
res2 := rr(99)(context.Background())
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestFrom2(t *testing.T) {
|
||||
queryDB := func(ctx context.Context, table string, id int) (string, error) {
|
||||
if table == "users" && id == 42 {
|
||||
return "record", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
rr := From2(queryDB)
|
||||
|
||||
res1 := rr("users", 42)(context.Background())
|
||||
assert.Equal(t, result.Of("record"), res1)
|
||||
|
||||
res2 := rr("posts", 1)(context.Background())
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestFrom3(t *testing.T) {
|
||||
updateRecord := func(ctx context.Context, table string, id int, data string) (string, error) {
|
||||
if table == "users" && id == 42 && data == "updated" {
|
||||
return "success", nil
|
||||
}
|
||||
return "", errors.New("update failed")
|
||||
}
|
||||
|
||||
rr := From3(updateRecord)
|
||||
|
||||
res1 := rr("users", 42, "updated")(context.Background())
|
||||
assert.Equal(t, result.Of("success"), res1)
|
||||
|
||||
res2 := rr("posts", 1, "data")(context.Background())
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
104
v2/readerresult/monoid.go
Normal file
104
v2/readerresult/monoid.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// AlternativeMonoid creates a Monoid for ReaderResult that combines both Alternative and Applicative behavior.
|
||||
// It uses the provided monoid for the success values and falls back to alternative computations on failure.
|
||||
//
|
||||
// The empty element is Of(m.Empty()), and concat tries the first computation, falling back to the second
|
||||
// if it fails, then combines successful values using the underlying monoid.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intAdd := monoid.MakeMonoid(0, func(a, b int) int { return a + b })
|
||||
// rrMonoid := readerresult.AlternativeMonoid[Config](intAdd)
|
||||
//
|
||||
// rr1 := readerresult.Of[Config](5)
|
||||
// rr2 := readerresult.Of[Config](3)
|
||||
// combined := rrMonoid.Concat(rr1, rr2)
|
||||
// // combined(cfg) returns result.Of(8)
|
||||
//
|
||||
//go:inline
|
||||
func AlternativeMonoid[R, A any](m M.Monoid[A]) Monoid[R, A] {
|
||||
return M.AlternativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[A, R, A],
|
||||
MonadAlt[R, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid for ReaderResult based on the Alternative pattern.
|
||||
// The empty element is the provided zero computation, and concat tries the first computation,
|
||||
// falling back to the second if it fails.
|
||||
//
|
||||
// This is useful for combining computations where you want to try alternatives until one succeeds.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// zero := func() readerresult.ReaderResult[Config, User] {
|
||||
// return readerresult.Left[Config, User](errors.New("no user"))
|
||||
// }
|
||||
// userMonoid := readerresult.AltMonoid[Config](zero)
|
||||
//
|
||||
// primary := getPrimaryUser()
|
||||
// backup := getBackupUser()
|
||||
// combined := userMonoid.Concat(primary, backup)
|
||||
// // Tries primary, falls back to backup if primary fails
|
||||
//
|
||||
//go:inline
|
||||
func AltMonoid[R, A any](zero Lazy[ReaderResult[R, A]]) Monoid[R, A] {
|
||||
return M.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[R, A],
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid creates a Monoid for ReaderResult based on Applicative functor composition.
|
||||
// The empty element is Of(m.Empty()), and concat combines two computations using the underlying monoid.
|
||||
// Both computations must succeed for the result to succeed.
|
||||
//
|
||||
// This is useful for accumulating results from multiple independent computations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intAdd := monoid.MakeMonoid(0, func(a, b int) int { return a + b })
|
||||
// rrMonoid := readerresult.ApplicativeMonoid[Config](intAdd)
|
||||
//
|
||||
// rr1 := readerresult.Of[Config](5)
|
||||
// rr2 := readerresult.Of[Config](3)
|
||||
// combined := rrMonoid.Concat(rr1, rr2)
|
||||
// // combined(cfg) returns result.Of(8)
|
||||
//
|
||||
// // If either fails, the whole computation fails
|
||||
// rr3 := readerresult.Left[Config, int](errors.New("error"))
|
||||
// failed := rrMonoid.Concat(rr1, rr3)
|
||||
// // failed(cfg) returns result.Left[int](error)
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[R, A any](m M.Monoid[A]) Monoid[R, A] {
|
||||
return M.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[A, R, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
276
v2/readerresult/monoid_test.go
Normal file
276
v2/readerresult/monoid_test.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
intAddMonoid = N.MonoidSum[int]()
|
||||
strMonoid = S.Monoid
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
rrMonoid := ApplicativeMonoid[MyContext](intAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
assert.Equal(t, result.Of(0), empty(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat two success values", func(t *testing.T) {
|
||||
rr1 := Of[MyContext](5)
|
||||
rr2 := Of[MyContext](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
assert.Equal(t, result.Of(8), combined(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
assert.Equal(t, result.Of(42), combined1(defaultContext))
|
||||
assert.Equal(t, result.Of(42), combined2(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat with left failure", func(t *testing.T) {
|
||||
rrSuccess := Of[MyContext](5)
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
assert.True(t, result.IsLeft(combined(defaultContext)))
|
||||
})
|
||||
|
||||
t.Run("concat with right failure", func(t *testing.T) {
|
||||
rrSuccess := Of[MyContext](5)
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
assert.True(t, result.IsLeft(combined(defaultContext)))
|
||||
})
|
||||
|
||||
t.Run("concat multiple values", func(t *testing.T) {
|
||||
rr1 := Of[MyContext](1)
|
||||
rr2 := Of[MyContext](2)
|
||||
rr3 := Of[MyContext](3)
|
||||
rr4 := Of[MyContext](4)
|
||||
|
||||
// Chain concat calls: ((1 + 2) + 3) + 4
|
||||
combined := rrMonoid.Concat(
|
||||
rrMonoid.Concat(
|
||||
rrMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
),
|
||||
rr4,
|
||||
)
|
||||
assert.Equal(t, result.Of(10), combined(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
strRRMonoid := ApplicativeMonoid[MyContext](strMonoid)
|
||||
|
||||
rr1 := Of[MyContext]("Hello")
|
||||
rr2 := Of[MyContext](" ")
|
||||
rr3 := Of[MyContext]("World")
|
||||
|
||||
combined := strRRMonoid.Concat(
|
||||
strRRMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
)
|
||||
assert.Equal(t, result.Of("Hello World"), combined(defaultContext))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
zero := func() ReaderResult[MyContext, int] {
|
||||
return Left[MyContext, int](errors.New("empty"))
|
||||
}
|
||||
|
||||
rrMonoid := AltMonoid(zero)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
assert.True(t, result.IsLeft(empty(defaultContext)))
|
||||
})
|
||||
|
||||
t.Run("concat two success values - uses first", func(t *testing.T) {
|
||||
rr1 := Of[MyContext](5)
|
||||
rr2 := Of[MyContext](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
// AltMonoid takes the first successful value
|
||||
assert.Equal(t, result.Of(5), combined(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat failure then success", func(t *testing.T) {
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
rrSuccess := Of[MyContext](42)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
// Should fall back to second when first fails
|
||||
assert.Equal(t, result.Of(42), combined(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat success then failure", func(t *testing.T) {
|
||||
rrSuccess := Of[MyContext](42)
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
// Should use first successful value
|
||||
assert.Equal(t, result.Of(42), combined(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat two failures", func(t *testing.T) {
|
||||
err1 := errors.New("error 1")
|
||||
err2 := errors.New("error 2")
|
||||
|
||||
rr1 := Left[MyContext, int](err1)
|
||||
rr2 := Left[MyContext, int](err2)
|
||||
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
// Should use second error when both fail
|
||||
assert.True(t, result.IsLeft(combined(defaultContext)))
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
assert.Equal(t, result.Of(42), combined1(defaultContext))
|
||||
assert.Equal(t, result.Of(42), combined2(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("fallback chain", func(t *testing.T) {
|
||||
// Simulate trying multiple sources until one succeeds
|
||||
primary := Left[MyContext, string](errors.New("primary failed"))
|
||||
secondary := Left[MyContext, string](errors.New("secondary failed"))
|
||||
tertiary := Of[MyContext]("tertiary success")
|
||||
|
||||
strZero := func() ReaderResult[MyContext, string] {
|
||||
return Left[MyContext, string](errors.New("all failed"))
|
||||
}
|
||||
strMonoid := AltMonoid(strZero)
|
||||
|
||||
// Chain concat: try primary, then secondary, then tertiary
|
||||
combined := strMonoid.Concat(
|
||||
strMonoid.Concat(primary, secondary),
|
||||
tertiary,
|
||||
)
|
||||
assert.Equal(t, result.Of("tertiary success"), combined(defaultContext))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
rrMonoid := AlternativeMonoid[MyContext](intAddMonoid)
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
assert.Equal(t, result.Of(0), empty(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat two success values", func(t *testing.T) {
|
||||
rr1 := Of[MyContext](5)
|
||||
rr2 := Of[MyContext](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
assert.Equal(t, result.Of(8), combined(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat failure then success", func(t *testing.T) {
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
rrSuccess := Of[MyContext](42)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
// Alternative falls back to second when first fails
|
||||
assert.Equal(t, result.Of(42), combined(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat success then failure", func(t *testing.T) {
|
||||
rrSuccess := Of[MyContext](42)
|
||||
rrFailure := Left[MyContext, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
// Should use first successful value
|
||||
assert.Equal(t, result.Of(42), combined(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
assert.Equal(t, result.Of(42), combined1(defaultContext))
|
||||
assert.Equal(t, result.Of(42), combined2(defaultContext))
|
||||
})
|
||||
|
||||
t.Run("multiple values with some failures", func(t *testing.T) {
|
||||
rr1 := Left[MyContext, int](errors.New("fail 1"))
|
||||
rr2 := Of[MyContext](5)
|
||||
rr3 := Left[MyContext, int](errors.New("fail 2"))
|
||||
rr4 := Of[MyContext](10)
|
||||
|
||||
// Alternative should skip failures and accumulate successes
|
||||
combined := rrMonoid.Concat(
|
||||
rrMonoid.Concat(
|
||||
rrMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
),
|
||||
rr4,
|
||||
)
|
||||
// Should accumulate successful values: 5 + 10 = 15
|
||||
assert.Equal(t, result.Of(15), combined(defaultContext))
|
||||
})
|
||||
}
|
||||
|
||||
// Test monoid laws
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
rrMonoid := ApplicativeMonoid[MyContext](intAddMonoid)
|
||||
|
||||
// Left identity: empty <> x == x
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Of[MyContext](42)
|
||||
result1 := rrMonoid.Concat(rrMonoid.Empty(), x)(defaultContext)
|
||||
result2 := x(defaultContext)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
// Right identity: x <> empty == x
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Of[MyContext](42)
|
||||
result1 := rrMonoid.Concat(x, rrMonoid.Empty())(defaultContext)
|
||||
result2 := x(defaultContext)
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
// Associativity: (x <> y) <> z == x <> (y <> z)
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
x := Of[MyContext](1)
|
||||
y := Of[MyContext](2)
|
||||
z := Of[MyContext](3)
|
||||
|
||||
left := rrMonoid.Concat(rrMonoid.Concat(x, y), z)(defaultContext)
|
||||
right := rrMonoid.Concat(x, rrMonoid.Concat(y, z))(defaultContext)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
515
v2/readerresult/reader.go
Normal file
515
v2/readerresult/reader.go
Normal file
@@ -0,0 +1,515 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/eithert"
|
||||
"github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromEither lifts a Result[A] into a ReaderResult[R, A] that ignores the environment.
|
||||
// The resulting computation will always produce the same result regardless of the environment provided.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// res := result.Of(42)
|
||||
// rr := readerresult.FromEither[Config](res)
|
||||
// // rr(anyConfig) will always return result.Of(42)
|
||||
//
|
||||
//go:inline
|
||||
func FromEither[R, A any](e Result[A]) ReaderResult[R, A] {
|
||||
return reader.Of[R](e)
|
||||
}
|
||||
|
||||
// FromResult is an alias for FromEither.
|
||||
// It lifts a Result[A] into a ReaderResult[R, A] that ignores the environment.
|
||||
//
|
||||
//go:inline
|
||||
func FromResult[R, A any](e Result[A]) ReaderResult[R, A] {
|
||||
return reader.Of[R](e)
|
||||
}
|
||||
|
||||
// RightReader lifts a Reader[R, A] into a ReaderResult[R, A] that always succeeds.
|
||||
// The resulting computation reads a value from the environment and wraps it in a successful Result.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPort := func(cfg Config) int { return cfg.Port }
|
||||
// rr := readerresult.RightReader[Config](getPort)
|
||||
// // rr(cfg) returns result.Of(cfg.Port)
|
||||
func RightReader[R, A any](r Reader[R, A]) ReaderResult[R, A] {
|
||||
return eithert.RightF(reader.MonadMap[R, A, Result[A]], r)
|
||||
}
|
||||
|
||||
// LeftReader lifts a Reader[R, error] into a ReaderResult[R, A] that always fails.
|
||||
// The resulting computation reads an error from the environment and wraps it in a failed Result.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getError := func(cfg Config) error { return cfg.InitError }
|
||||
// rr := readerresult.LeftReader[User](getError)
|
||||
// // rr(cfg) returns result.Left[User](cfg.InitError)
|
||||
func LeftReader[A, R any](l Reader[R, error]) ReaderResult[R, A] {
|
||||
return eithert.LeftF(reader.MonadMap[R, error, Result[A]], l)
|
||||
}
|
||||
|
||||
// Left creates a ReaderResult that always fails with the given error, ignoring the environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Left[Config, User](errors.New("not found"))
|
||||
// // rr(anyConfig) always returns result.Left[User](error)
|
||||
//
|
||||
//go:inline
|
||||
func Left[R, A any](l error) ReaderResult[R, A] {
|
||||
return eithert.Left(reader.Of[R, Result[A]], l)
|
||||
}
|
||||
|
||||
// Right creates a ReaderResult that always succeeds with the given value, ignoring the environment.
|
||||
// This is the "pure" or "return" operation for the ReaderResult monad.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Right[Config](42)
|
||||
// // rr(anyConfig) always returns result.Of(42)
|
||||
//
|
||||
//go:inline
|
||||
func Right[R, A any](r A) ReaderResult[R, A] {
|
||||
return eithert.Right(reader.Of[R, Result[A]], r)
|
||||
}
|
||||
|
||||
// FromReader is an alias for RightReader.
|
||||
// It lifts a Reader[R, A] into a ReaderResult[R, A] that always succeeds.
|
||||
//
|
||||
//go:inline
|
||||
func FromReader[R, A any](r Reader[R, A]) ReaderResult[R, A] {
|
||||
return RightReader(r)
|
||||
}
|
||||
|
||||
// MonadMap transforms the success value of a ReaderResult using the given function.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of[Config](5)
|
||||
// doubled := readerresult.MonadMap(rr, func(x int) int { return x * 2 })
|
||||
// // doubled(cfg) returns result.Of(10)
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[R, A, B any](fa ReaderResult[R, A], f func(A) B) ReaderResult[R, B] {
|
||||
return readert.MonadMap[ReaderResult[R, A], ReaderResult[R, B]](ET.MonadMap[error, A, B], fa, f)
|
||||
}
|
||||
|
||||
// Map is the curried version of MonadMap.
|
||||
// It returns an Operator that can be used in function composition pipelines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := readerresult.Map[Config](func(x int) int { return x * 2 })
|
||||
// result := F.Pipe1(readerresult.Of[Config](5), double)
|
||||
//
|
||||
//go:inline
|
||||
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
|
||||
return readert.Map[ReaderResult[R, A], ReaderResult[R, B]](ET.Map[error, A, B], f)
|
||||
}
|
||||
|
||||
// MonadChain sequences two ReaderResult computations, where the second depends on the result of the first.
|
||||
// This is also known as "flatMap" or "bind". If the first computation fails, the second is not executed.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
|
||||
// getPosts := func(user User) readerresult.ReaderResult[DB, []Post] { ... }
|
||||
// userPosts := readerresult.MonadChain(getUser(42), getPosts)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[R, A, B any](ma ReaderResult[R, A], f Kleisli[R, A, B]) ReaderResult[R, B] {
|
||||
return readert.MonadChain(ET.MonadChain[error, A, B], ma, f)
|
||||
}
|
||||
|
||||
// Chain is the curried version of MonadChain.
|
||||
// It returns an Operator that can be used in function composition pipelines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPosts := func(user User) readerresult.ReaderResult[DB, []Post] { ... }
|
||||
// result := F.Pipe1(getUser(42), readerresult.Chain[DB](getPosts))
|
||||
//
|
||||
//go:inline
|
||||
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return readert.Chain[ReaderResult[R, A]](ET.Chain[error, A, B], f)
|
||||
}
|
||||
|
||||
// Of creates a ReaderResult that always succeeds with the given value.
|
||||
// This is an alias for Right and is the "pure" or "return" operation for the ReaderResult monad.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of[Config](42)
|
||||
// // rr(anyConfig) always returns result.Of(42)
|
||||
//
|
||||
//go:inline
|
||||
func Of[R, A any](a A) ReaderResult[R, A] {
|
||||
return readert.MonadOf[ReaderResult[R, A]](ET.Of[error, A], a)
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.
|
||||
// Both computations share the same environment. This is useful for combining independent
|
||||
// computations that don't depend on each other's results.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// add := func(x int) func(int) int { return func(y int) int { return x + y } }
|
||||
// fabr := readerresult.Of[Config](add(5))
|
||||
// fa := readerresult.Of[Config](3)
|
||||
// result := readerresult.MonadAp(fabr, fa) // Returns Of(8)
|
||||
//
|
||||
//go:inline
|
||||
func MonadAp[B, R, A any](fab ReaderResult[R, func(A) B], fa ReaderResult[R, A]) ReaderResult[R, B] {
|
||||
return readert.MonadAp[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.MonadAp[B, error, A], fab, fa)
|
||||
}
|
||||
|
||||
// Ap is the curried version of MonadAp.
|
||||
// It returns an Operator that can be used in function composition pipelines.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, R, A any](fa ReaderResult[R, A]) Operator[R, func(A) B, B] {
|
||||
return readert.Ap[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.Ap[B, error, A], fa)
|
||||
}
|
||||
|
||||
// FromPredicate creates a Kleisli arrow that tests a predicate and returns either the input value
|
||||
// or an error. If the predicate returns true, the value is returned as a success. If false,
|
||||
// the onFalse function is called to generate an error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := readerresult.FromPredicate[Config](
|
||||
// func(x int) bool { return x > 0 },
|
||||
// func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
// )
|
||||
// result := isPositive(5) // Returns ReaderResult that succeeds with 5
|
||||
// result := isPositive(-1) // Returns ReaderResult that fails with error
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) error) Kleisli[R, A, A] {
|
||||
return fromeither.FromPredicate(FromEither[R, A], pred, onFalse)
|
||||
}
|
||||
|
||||
// Fold handles both success and failure cases by providing functions for each.
|
||||
// The result is always a Reader[R, B] (without the error channel).
|
||||
// This is useful for converting a ReaderResult into a plain Reader by handling the error case.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// handleError := func(err error) reader.Reader[Config, string] {
|
||||
// return func(cfg Config) string { return "Error: " + err.Error() }
|
||||
// }
|
||||
// handleSuccess := func(user User) reader.Reader[Config, string] {
|
||||
// return func(cfg Config) string { return user.Name }
|
||||
// }
|
||||
// result := readerresult.Fold[Config, User, string](handleError, handleSuccess)(getUserRR)
|
||||
//
|
||||
//go:inline
|
||||
func Fold[R, A, B any](onLeft reader.Kleisli[R, error, B], onRight reader.Kleisli[R, A, B]) func(ReaderResult[R, A]) Reader[R, B] {
|
||||
return eithert.MatchE(reader.MonadChain[R, Result[A], B], onLeft, onRight)
|
||||
}
|
||||
|
||||
// GetOrElse extracts the success value or computes a default value from the error.
|
||||
// The result is a Reader[R, A] that always succeeds.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// defaultUser := func(err error) reader.Reader[Config, User] {
|
||||
// return func(cfg Config) User { return User{Name: "Guest"} }
|
||||
// }
|
||||
// result := readerresult.GetOrElse[Config](defaultUser)(getUserRR)
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[R, A any](onLeft reader.Kleisli[R, error, A]) func(ReaderResult[R, A]) Reader[R, A] {
|
||||
return eithert.GetOrElse(reader.MonadChain[R, Result[A], A], reader.Of[R, A], onLeft)
|
||||
}
|
||||
|
||||
// OrElse provides an alternative ReaderResult computation if the first one fails.
|
||||
// This is useful for fallback logic or retry scenarios.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPrimaryUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
|
||||
// getBackupUser := func(err error) readerresult.ReaderResult[DB, User] {
|
||||
// return readerresult.Of[DB](User{Name: "Guest"})
|
||||
// }
|
||||
// result := F.Pipe1(getPrimaryUser(42), readerresult.OrElse[DB](getBackupUser))
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
|
||||
return eithert.OrElse(reader.MonadChain[R, Result[A], Result[A]], reader.Of[R, Result[A]], onLeft)
|
||||
}
|
||||
|
||||
// OrLeft transforms the error value if the computation fails, leaving successful values unchanged.
|
||||
// This is useful for error mapping or enriching error information.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichError := func(err error) reader.Reader[Config, error] {
|
||||
// return func(cfg Config) error {
|
||||
// return fmt.Errorf("DB error on %s: %w", cfg.DBHost, err)
|
||||
// }
|
||||
// }
|
||||
// result := F.Pipe1(getUserRR, readerresult.OrLeft[Config](enrichError))
|
||||
//
|
||||
//go:inline
|
||||
func OrLeft[R, A any](onLeft reader.Kleisli[R, error, error]) Operator[R, A, A] {
|
||||
return eithert.OrLeft(
|
||||
reader.MonadChain[R, Result[A], Result[A]],
|
||||
reader.MonadMap[R, error, Result[A]],
|
||||
reader.Of[R, Result[A]],
|
||||
onLeft,
|
||||
)
|
||||
}
|
||||
|
||||
// Ask retrieves the current environment as a successful ReaderResult.
|
||||
// This is useful for accessing configuration or context within a ReaderResult computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Ask[Config](),
|
||||
// readerresult.Map[Config](func(cfg Config) int { return cfg.Port }),
|
||||
// )
|
||||
// // result(cfg) returns result.Of(cfg.Port)
|
||||
//
|
||||
//go:inline
|
||||
func Ask[R any]() ReaderResult[R, R] {
|
||||
return fromreader.Ask(FromReader[R, R])()
|
||||
}
|
||||
|
||||
// Asks retrieves a value from the environment using the provided Reader function.
|
||||
// This lifts a Reader computation into a ReaderResult that always succeeds.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPort := func(cfg Config) int { return cfg.Port }
|
||||
// result := readerresult.Asks[Config](getPort)
|
||||
// // result(cfg) returns result.Of(cfg.Port)
|
||||
//
|
||||
//go:inline
|
||||
func Asks[R, A any](r Reader[R, A]) ReaderResult[R, A] {
|
||||
return fromreader.Asks(FromReader[R, A])(r)
|
||||
}
|
||||
|
||||
// MonadChainEitherK chains a ReaderResult with a function that returns a plain Result.
|
||||
// This is useful for integrating functions that don't need environment access.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := readerresult.MonadChainEitherK(getUserDataRR, parseUser)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B]) ReaderResult[R, B] {
|
||||
return fromeither.MonadChainEitherK(
|
||||
MonadChain[R, A, B],
|
||||
FromEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainEitherK is the curried version of MonadChainEitherK.
|
||||
// It lifts a Result-returning function into a ReaderResult operator.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := F.Pipe1(getUserDataRR, readerresult.ChainEitherK[Config](parseUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainEitherK(
|
||||
Chain[R, A, B],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainOptionK chains with a function that returns an Option, converting None to an error.
|
||||
// This is useful for integrating functions that return optional values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// findUser := func(id int) option.Option[User] { ... }
|
||||
// notFound := func() error { return errors.New("user not found") }
|
||||
// chain := readerresult.ChainOptionK[Config, int, User](notFound)
|
||||
// result := F.Pipe1(readerresult.Of[Config](42), chain(findUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[R, A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainOptionK(MonadChain[R, A, B], FromEither[R, B], onNone)
|
||||
}
|
||||
|
||||
// Flatten removes one level of nesting from a nested ReaderResult.
|
||||
// This converts ReaderResult[R, ReaderResult[R, A]] into ReaderResult[R, A].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nested := readerresult.Of[Config](readerresult.Of[Config](42))
|
||||
// flat := readerresult.Flatten(nested)
|
||||
// // flat(cfg) returns result.Of(42)
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[R, A any](mma ReaderResult[R, ReaderResult[R, A]]) ReaderResult[R, A] {
|
||||
return MonadChain(mma, function.Identity[ReaderResult[R, A]])
|
||||
}
|
||||
|
||||
// MonadBiMap maps functions over both the error and success channels simultaneously.
|
||||
// This transforms both the error type and the success type in a single operation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// result := readerresult.MonadBiMap(rr, enrichErr, double)
|
||||
//
|
||||
//go:inline
|
||||
func MonadBiMap[R, A, B any](fa ReaderResult[R, A], f Endomorphism[error], g func(A) B) ReaderResult[R, B] {
|
||||
return eithert.MonadBiMap(reader.MonadMap[R, Result[A], Result[B]], fa, f, g)
|
||||
}
|
||||
|
||||
// BiMap is the curried version of MonadBiMap.
|
||||
// It maps a pair of functions over the error and success channels.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// result := F.Pipe1(rr, readerresult.BiMap[Config](enrichErr, double))
|
||||
//
|
||||
//go:inline
|
||||
func BiMap[R, A, B any](f Endomorphism[error], g func(A) B) Operator[R, A, B] {
|
||||
return eithert.BiMap(reader.Map[R, Result[A], Result[B]], f, g)
|
||||
}
|
||||
|
||||
// Local changes the environment type during execution of a ReaderResult.
|
||||
// This is similar to Contravariant's contramap and allows adapting computations
|
||||
// to work with different environment types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Convert DB environment to Config environment
|
||||
// toConfig := func(db DB) Config { return db.Config }
|
||||
// rr := readerresult.Of[Config](42)
|
||||
// adapted := readerresult.Local[int](toConfig)(rr)
|
||||
// // adapted now accepts DB instead of Config
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R2, R1 any](f func(R2) R1) func(ReaderResult[R1, A]) ReaderResult[R2, A] {
|
||||
return reader.Local[Result[A]](f)
|
||||
}
|
||||
|
||||
// Read applies an environment value to a ReaderResult to execute it and obtain the Result.
|
||||
// This is the primary way to "run" a ReaderResult computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Asks(func(cfg Config) int { return cfg.Port })
|
||||
// run := readerresult.Read[int](myConfig)
|
||||
// res := run(rr) // Returns result.Result[int]
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, R any](r R) func(ReaderResult[R, A]) Result[A] {
|
||||
return reader.Read[Result[A]](r)
|
||||
}
|
||||
|
||||
// MonadFlap applies a wrapped function to a concrete value (reverse of Ap).
|
||||
// This is useful when you have a function in a context and a plain value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fabr := readerresult.Of[Config](func(x int) int { return x * 2 })
|
||||
// result := readerresult.MonadFlap(fabr, 5) // Returns Of(10)
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[R, A, B any](fab ReaderResult[R, func(A) B], a A) ReaderResult[R, B] {
|
||||
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
// Flap is the curried version of MonadFlap.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
return functor.Flap(Map[R, func(A) B, B], a)
|
||||
}
|
||||
|
||||
// MonadMapLeft transforms the error value without affecting successful results.
|
||||
// This is useful for error enrichment, wrapping, or transformation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("DB error: %w", e) }
|
||||
// result := readerresult.MonadMapLeft(getUserRR, enrichErr)
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapLeft[R, A any](fa ReaderResult[R, A], f Endomorphism[error]) ReaderResult[R, A] {
|
||||
return eithert.MonadMapLeft(reader.MonadMap[R, Result[A], Result[A]], fa, f)
|
||||
}
|
||||
|
||||
// MapLeft is the curried version of MonadMapLeft.
|
||||
// It applies a mapping function to the error channel only.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("DB error: %w", e) }
|
||||
// result := F.Pipe1(getUserRR, readerresult.MapLeft[Config](enrichErr))
|
||||
//
|
||||
//go:inline
|
||||
func MapLeft[R, A any](f Endomorphism[error]) Operator[R, A, A] {
|
||||
return eithert.MapLeft(reader.Map[R, Result[A], Result[A]], f)
|
||||
}
|
||||
|
||||
// MonadAlt tries the first computation, and if it fails, tries the second.
|
||||
// This implements the Alternative pattern for error recovery.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAlt[R, A any](first ReaderResult[R, A], second Lazy[ReaderResult[R, A]]) ReaderResult[R, A] {
|
||||
return eithert.MonadAlt(
|
||||
reader.Of[R, Result[A]],
|
||||
reader.MonadChain[R, Result[A], Result[A]],
|
||||
|
||||
first,
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
// Alt tries the first computation, and if it fails, tries the second.
|
||||
// This implements the Alternative pattern for error recovery.
|
||||
//
|
||||
//go:inline
|
||||
func Alt[R, A any](second Lazy[ReaderResult[R, A]]) Operator[R, A, A] {
|
||||
return eithert.Alt(
|
||||
reader.Of[R, Result[A]],
|
||||
reader.Chain[R, Result[A], Result[A]],
|
||||
|
||||
second,
|
||||
)
|
||||
}
|
||||
324
v2/readerresult/reader_test.go
Normal file
324
v2/readerresult/reader_test.go
Normal file
@@ -0,0 +1,324 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MyContext string
|
||||
|
||||
const defaultContext MyContext = "default"
|
||||
|
||||
var (
|
||||
testError = errors.New("test error")
|
||||
)
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
rr := FromEither[MyContext](result.Of(42))
|
||||
assert.Equal(t, result.Of(42), rr(defaultContext))
|
||||
|
||||
rrErr := FromEither[MyContext](result.Left[int](testError))
|
||||
assert.Equal(t, result.Left[int](testError), rrErr(defaultContext))
|
||||
}
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
rr := FromResult[MyContext](result.Of(42))
|
||||
assert.Equal(t, result.Of(42), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestRightReader(t *testing.T) {
|
||||
r := func(ctx MyContext) int { return 42 }
|
||||
rr := RightReader(r)
|
||||
assert.Equal(t, result.Of(42), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestLeftReader(t *testing.T) {
|
||||
r := func(ctx MyContext) error { return testError }
|
||||
rr := LeftReader[int](r)
|
||||
assert.Equal(t, result.Left[int](testError), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
rr := Left[MyContext, int](testError)
|
||||
assert.Equal(t, result.Left[int](testError), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
rr := Right[MyContext](42)
|
||||
assert.Equal(t, result.Of(42), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
assert.Equal(t, result.Of(42), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
r := func(ctx MyContext) string { return string(ctx) }
|
||||
rr := FromReader(r)
|
||||
assert.Equal(t, result.Of("default"), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](1),
|
||||
Map[MyContext](utils.Double),
|
||||
)
|
||||
assert.Equal(t, result.Of(2), g(defaultContext))
|
||||
|
||||
// Test with error
|
||||
gErr := F.Pipe1(
|
||||
Left[MyContext, int](testError),
|
||||
Map[MyContext](utils.Double),
|
||||
)
|
||||
assert.Equal(t, result.Left[int](testError), gErr(defaultContext))
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
rr := Of[MyContext](5)
|
||||
doubled := MonadMap(rr, func(x int) int { return x * 2 })
|
||||
assert.Equal(t, result.Of(10), doubled(defaultContext))
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
addOne := func(x int) ReaderResult[MyContext, int] {
|
||||
return Of[MyContext](x + 1)
|
||||
}
|
||||
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](5),
|
||||
Chain(addOne),
|
||||
)
|
||||
assert.Equal(t, result.Of(6), g(defaultContext))
|
||||
|
||||
// Test error propagation
|
||||
gErr := F.Pipe1(
|
||||
Left[MyContext, int](testError),
|
||||
Chain(addOne),
|
||||
)
|
||||
assert.Equal(t, result.Left[int](testError), gErr(defaultContext))
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
addOne := func(x int) ReaderResult[MyContext, int] {
|
||||
return Of[MyContext](x + 1)
|
||||
}
|
||||
|
||||
rr := Of[MyContext](5)
|
||||
res := MonadChain(rr, addOne)
|
||||
assert.Equal(t, result.Of(6), res(defaultContext))
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](utils.Double),
|
||||
Ap[int](Of[MyContext](1)),
|
||||
)
|
||||
assert.Equal(t, result.Of(2), g(defaultContext))
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
add := func(x int) func(int) int {
|
||||
return func(y int) int { return x + y }
|
||||
}
|
||||
fabr := Of[MyContext](add(5))
|
||||
fa := Of[MyContext](3)
|
||||
res := MonadAp(fabr, fa)
|
||||
assert.Equal(t, result.Of(8), res(defaultContext))
|
||||
}
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate[MyContext](
|
||||
func(x int) bool { return x > 0 },
|
||||
func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
)
|
||||
|
||||
assert.Equal(t, result.Of(5), isPositive(5)(defaultContext))
|
||||
res := isPositive(-1)(defaultContext)
|
||||
assert.True(t, result.IsLeft(res))
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
handleError := func(err error) reader.Reader[MyContext, string] {
|
||||
return func(ctx MyContext) string { return "Error: " + err.Error() }
|
||||
}
|
||||
handleSuccess := func(x int) reader.Reader[MyContext, string] {
|
||||
return func(ctx MyContext) string { return fmt.Sprintf("Success: %d", x) }
|
||||
}
|
||||
|
||||
fold := Fold(handleError, handleSuccess)
|
||||
|
||||
res1 := fold(Of[MyContext](42))(defaultContext)
|
||||
assert.Equal(t, "Success: 42", res1)
|
||||
|
||||
res2 := fold(Left[MyContext, int](testError))(defaultContext)
|
||||
assert.Equal(t, "Error: "+testError.Error(), res2)
|
||||
}
|
||||
|
||||
func TestGetOrElse(t *testing.T) {
|
||||
defaultVal := func(err error) reader.Reader[MyContext, int] {
|
||||
return func(ctx MyContext) int { return 0 }
|
||||
}
|
||||
|
||||
getOrElse := GetOrElse(defaultVal)
|
||||
|
||||
res1 := getOrElse(Of[MyContext](42))(defaultContext)
|
||||
assert.Equal(t, 42, res1)
|
||||
|
||||
res2 := getOrElse(Left[MyContext, int](testError))(defaultContext)
|
||||
assert.Equal(t, 0, res2)
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
fallback := func(err error) ReaderResult[MyContext, int] {
|
||||
return Of[MyContext](99)
|
||||
}
|
||||
|
||||
orElse := OrElse(fallback)
|
||||
|
||||
res1 := F.Pipe1(Of[MyContext](42), orElse)(defaultContext)
|
||||
assert.Equal(t, result.Of(42), res1)
|
||||
|
||||
res2 := F.Pipe1(Left[MyContext, int](testError), orElse)(defaultContext)
|
||||
assert.Equal(t, result.Of(99), res2)
|
||||
}
|
||||
|
||||
func TestOrLeft(t *testing.T) {
|
||||
enrichErr := func(err error) reader.Reader[MyContext, error] {
|
||||
return func(ctx MyContext) error {
|
||||
return fmt.Errorf("enriched: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
orLeft := OrLeft[MyContext, int](enrichErr)
|
||||
|
||||
res1 := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext)
|
||||
assert.Equal(t, result.Of(42), res1)
|
||||
|
||||
res2 := F.Pipe1(Left[MyContext, int](testError), orLeft)(defaultContext)
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
rr := Ask[MyContext]()
|
||||
assert.Equal(t, result.Of(defaultContext), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestAsks(t *testing.T) {
|
||||
getLen := func(ctx MyContext) int { return len(string(ctx)) }
|
||||
rr := Asks(getLen)
|
||||
assert.Equal(t, result.Of(7), rr(defaultContext)) // "default" has 7 chars
|
||||
}
|
||||
|
||||
func TestChainEitherK(t *testing.T) {
|
||||
parseInt := func(s string) result.Result[int] {
|
||||
if s == "42" {
|
||||
return result.Of(42)
|
||||
}
|
||||
return result.Left[int](errors.New("parse error"))
|
||||
}
|
||||
|
||||
chain := ChainEitherK[MyContext](parseInt)
|
||||
|
||||
res1 := F.Pipe1(Of[MyContext]("42"), chain)(defaultContext)
|
||||
assert.Equal(t, result.Of(42), res1)
|
||||
|
||||
res2 := F.Pipe1(Of[MyContext]("invalid"), chain)(defaultContext)
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
findEven := func(x int) option.Option[int] {
|
||||
if x%2 == 0 {
|
||||
return option.Some(x)
|
||||
}
|
||||
return option.None[int]()
|
||||
}
|
||||
|
||||
notFound := func() error { return errors.New("not even") }
|
||||
chain := ChainOptionK[MyContext, int, int](notFound)(findEven)
|
||||
|
||||
res1 := F.Pipe1(Of[MyContext](4), chain)(defaultContext)
|
||||
assert.Equal(t, result.Of(4), res1)
|
||||
|
||||
res2 := F.Pipe1(Of[MyContext](3), chain)(defaultContext)
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](Of[MyContext]("a")),
|
||||
Flatten[MyContext, string],
|
||||
)
|
||||
assert.Equal(t, result.Of("a"), g(defaultContext))
|
||||
}
|
||||
|
||||
func TestBiMap(t *testing.T) {
|
||||
enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
res1 := F.Pipe1(Of[MyContext](5), BiMap[MyContext](enrichErr, double))(defaultContext)
|
||||
assert.Equal(t, result.Of(10), res1)
|
||||
|
||||
res2 := F.Pipe1(Left[MyContext, int](testError), BiMap[MyContext](enrichErr, double))(defaultContext)
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
type OtherContext int
|
||||
toMyContext := func(oc OtherContext) MyContext {
|
||||
return MyContext(fmt.Sprintf("ctx-%d", oc))
|
||||
}
|
||||
|
||||
rr := Asks(func(ctx MyContext) string { return string(ctx) })
|
||||
adapted := Local[string](toMyContext)(rr)
|
||||
|
||||
res := adapted(OtherContext(42))
|
||||
assert.Equal(t, result.Of("ctx-42"), res)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
rr := Of[MyContext](42)
|
||||
read := Read[int](defaultContext)
|
||||
res := read(rr)
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
fabr := Of[MyContext](func(x int) int { return x * 2 })
|
||||
flapped := MonadFlap(fabr, 5)
|
||||
assert.Equal(t, result.Of(10), flapped(defaultContext))
|
||||
}
|
||||
|
||||
func TestMapLeft(t *testing.T) {
|
||||
enrichErr := func(e error) error { return fmt.Errorf("DB error: %w", e) }
|
||||
|
||||
res1 := F.Pipe1(Of[MyContext](42), MapLeft[MyContext, int](enrichErr))(defaultContext)
|
||||
assert.Equal(t, result.Of(42), res1)
|
||||
|
||||
res2 := F.Pipe1(Left[MyContext, int](testError), MapLeft[MyContext, int](enrichErr))(defaultContext)
|
||||
assert.True(t, result.IsLeft(res2))
|
||||
}
|
||||
103
v2/readerresult/sequence.go
Normal file
103
v2/readerresult/sequence.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 readerresult
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
// SequenceT functions convert multiple ReaderResult values into a single ReaderResult of a tuple.
|
||||
// These are useful for combining independent computations that share the same environment.
|
||||
// If any computation fails, the entire sequence fails with the first error.
|
||||
|
||||
// SequenceT1 wraps a single ReaderResult value in a Tuple1.
|
||||
// This is primarily for consistency with the other SequenceT functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of[Config](42)
|
||||
// result := readerresult.SequenceT1(rr)
|
||||
// // result(cfg) returns result.Of(Tuple1{42})
|
||||
func SequenceT1[L, A any](a ReaderResult[L, A]) ReaderResult[L, T.Tuple1[A]] {
|
||||
return G.SequenceT1[
|
||||
ReaderResult[L, A],
|
||||
ReaderResult[L, T.Tuple1[A]],
|
||||
](a)
|
||||
}
|
||||
|
||||
// SequenceT2 combines two independent ReaderResult computations into a tuple.
|
||||
// Both computations share the same environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPort := readerresult.Asks(func(cfg Config) int { return cfg.Port })
|
||||
// getHost := readerresult.Asks(func(cfg Config) string { return cfg.Host })
|
||||
// result := readerresult.SequenceT2(getPort, getHost)
|
||||
// // result(cfg) returns result.Of(Tuple2{cfg.Port, cfg.Host})
|
||||
func SequenceT2[L, A, B any](
|
||||
a ReaderResult[L, A],
|
||||
b ReaderResult[L, B],
|
||||
) ReaderResult[L, T.Tuple2[A, B]] {
|
||||
return G.SequenceT2[
|
||||
ReaderResult[L, A],
|
||||
ReaderResult[L, B],
|
||||
ReaderResult[L, T.Tuple2[A, B]],
|
||||
](a, b)
|
||||
}
|
||||
|
||||
// SequenceT3 combines three independent ReaderResult computations into a tuple.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := getUserRR(42)
|
||||
// getConfig := getConfigRR()
|
||||
// getStats := getStatsRR()
|
||||
// result := readerresult.SequenceT3(getUser, getConfig, getStats)
|
||||
// // result(env) returns result.Of(Tuple3{user, config, stats})
|
||||
func SequenceT3[L, A, B, C any](
|
||||
a ReaderResult[L, A],
|
||||
b ReaderResult[L, B],
|
||||
c ReaderResult[L, C],
|
||||
) ReaderResult[L, T.Tuple3[A, B, C]] {
|
||||
return G.SequenceT3[
|
||||
ReaderResult[L, A],
|
||||
ReaderResult[L, B],
|
||||
ReaderResult[L, C],
|
||||
ReaderResult[L, T.Tuple3[A, B, C]],
|
||||
](a, b, c)
|
||||
}
|
||||
|
||||
// SequenceT4 combines four independent ReaderResult computations into a tuple.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := readerresult.SequenceT4(getUserRR, getConfigRR, getStatsRR, getMetadataRR)
|
||||
// // result(env) returns result.Of(Tuple4{user, config, stats, metadata})
|
||||
func SequenceT4[L, A, B, C, D any](
|
||||
a ReaderResult[L, A],
|
||||
b ReaderResult[L, B],
|
||||
c ReaderResult[L, C],
|
||||
d ReaderResult[L, D],
|
||||
) ReaderResult[L, T.Tuple4[A, B, C, D]] {
|
||||
return G.SequenceT4[
|
||||
ReaderResult[L, A],
|
||||
ReaderResult[L, B],
|
||||
ReaderResult[L, C],
|
||||
ReaderResult[L, D],
|
||||
ReaderResult[L, T.Tuple4[A, B, C, D]],
|
||||
](a, b, c, d)
|
||||
}
|
||||
92
v2/readerresult/sequence_test.go
Normal file
92
v2/readerresult/sequence_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
errFoo = fmt.Errorf("error")
|
||||
)
|
||||
|
||||
func TestSequenceT1(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := Left[MyContext, string](errFoo)
|
||||
|
||||
res1 := SequenceT1(t1)
|
||||
assert.Equal(t, result.Of(T.MakeTuple1("s1")), res1(defaultContext))
|
||||
|
||||
res2 := SequenceT1(e1)
|
||||
assert.Equal(t, result.Left[T.Tuple1[string]](errFoo), res2(defaultContext))
|
||||
}
|
||||
|
||||
func TestSequenceT2(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := Left[MyContext, string](errFoo)
|
||||
t2 := Of[MyContext](2)
|
||||
e2 := Left[MyContext, int](errFoo)
|
||||
|
||||
res1 := SequenceT2(t1, t2)
|
||||
assert.Equal(t, result.Of(T.MakeTuple2("s1", 2)), res1(defaultContext))
|
||||
|
||||
res2 := SequenceT2(e1, t2)
|
||||
assert.Equal(t, result.Left[T.Tuple2[string, int]](errFoo), res2(defaultContext))
|
||||
|
||||
res3 := SequenceT2(t1, e2)
|
||||
assert.Equal(t, result.Left[T.Tuple2[string, int]](errFoo), res3(defaultContext))
|
||||
}
|
||||
|
||||
func TestSequenceT3(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := Left[MyContext, string](errFoo)
|
||||
t2 := Of[MyContext](2)
|
||||
e2 := Left[MyContext, int](errFoo)
|
||||
t3 := Of[MyContext](true)
|
||||
e3 := Left[MyContext, bool](errFoo)
|
||||
|
||||
res1 := SequenceT3(t1, t2, t3)
|
||||
assert.Equal(t, result.Of(T.MakeTuple3("s1", 2, true)), res1(defaultContext))
|
||||
|
||||
res2 := SequenceT3(e1, t2, t3)
|
||||
assert.Equal(t, result.Left[T.Tuple3[string, int, bool]](errFoo), res2(defaultContext))
|
||||
|
||||
res3 := SequenceT3(t1, e2, t3)
|
||||
assert.Equal(t, result.Left[T.Tuple3[string, int, bool]](errFoo), res3(defaultContext))
|
||||
|
||||
res4 := SequenceT3(t1, t2, e3)
|
||||
assert.Equal(t, result.Left[T.Tuple3[string, int, bool]](errFoo), res4(defaultContext))
|
||||
}
|
||||
|
||||
func TestSequenceT4(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
t2 := Of[MyContext](2)
|
||||
t3 := Of[MyContext](true)
|
||||
t4 := Of[MyContext](1.0)
|
||||
|
||||
res := SequenceT4(t1, t2, t3, t4)
|
||||
|
||||
assert.Equal(t, result.Of(T.MakeTuple4("s1", 2, true, 1.0)), res(defaultContext))
|
||||
}
|
||||
41
v2/readerresult/types.go
Normal file
41
v2/readerresult/types.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
Option[A any] = option.Option[A]
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Result[A any] = result.Result[A]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
ReaderResult[R, A any] = Reader[R, Result[A]]
|
||||
Monoid[R, A any] = monoid.Monoid[ReaderResult[R, A]]
|
||||
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderResult[R, B]]
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderResult[R, A], B]
|
||||
)
|
||||
Reference in New Issue
Block a user