1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-31 11:19:23 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Dr. Carsten Leue
e42d765852 fix: readeriooption
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 16:59:32 +01:00
18 changed files with 4033 additions and 197 deletions

View File

@@ -460,8 +460,11 @@ func process() IOResult[string] {
- **Either** - Type-safe error handling with left/right values
- **Result** - Simplified Either with error as left type (recommended for error handling)
- **IO** - Lazy evaluation and side effect management
- **IOOption** - Combine IO with Option for optional values with side effects
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
- **Reader** - Dependency injection pattern
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations

View File

@@ -1,127 +0,0 @@
mode: set
github.com/IBM/fp-go/v2/readerioeither/bind.go:26.27,28.2 1 1
github.com/IBM/fp-go/v2/readerioeither/bind.go:34.26,36.2 1 1
github.com/IBM/fp-go/v2/readerioeither/bind.go:42.26,44.2 1 1
github.com/IBM/fp-go/v2/readerioeither/bind.go:50.26,52.2 1 1
github.com/IBM/fp-go/v2/readerioeither/bind.go:57.25,59.2 1 1
github.com/IBM/fp-go/v2/readerioeither/bind.go:65.26,67.2 1 1
github.com/IBM/fp-go/v2/readerioeither/bracket.go:31.27,33.2 1 1
github.com/IBM/fp-go/v2/readerioeither/eq.go:25.92,27.2 1 1
github.com/IBM/fp-go/v2/readerioeither/eq.go:30.88,32.2 1 1
github.com/IBM/fp-go/v2/readerioeither/gen.go:14.92,16.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:20.90,22.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:26.92,28.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:32.102,34.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:38.100,40.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:44.102,46.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:50.114,52.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:56.112,58.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:62.114,64.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:68.126,70.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:74.124,76.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:80.126,82.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:86.138,88.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:92.136,94.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:98.138,100.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:104.150,106.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:110.148,112.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:116.150,118.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:122.162,124.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:128.160,130.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:134.162,136.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:140.174,142.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:146.172,148.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:152.174,154.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:158.186,160.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:164.184,166.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:170.186,172.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:176.198,178.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:182.196,184.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:188.198,190.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:194.211,196.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:200.209,202.2 1 0
github.com/IBM/fp-go/v2/readerioeither/gen.go:206.211,208.2 1 0
github.com/IBM/fp-go/v2/readerioeither/monad.go:26.73,28.2 1 1
github.com/IBM/fp-go/v2/readerioeither/monad.go:31.104,33.2 1 1
github.com/IBM/fp-go/v2/readerioeither/monad.go:36.131,38.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:40.92,46.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:50.90,52.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:55.76,60.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:63.75,68.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:73.96,75.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:79.60,81.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:85.90,87.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:91.54,93.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:98.120,104.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:108.125,114.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:118.123,125.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:129.87,135.2 1 0
github.com/IBM/fp-go/v2/readerioeither/reader.go:139.128,147.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:151.92,158.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:162.116,169.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:173.80,179.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:183.124,190.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:194.88,200.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:204.108,211.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:215.72,221.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:225.113,233.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:237.77,244.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:248.99,254.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:258.119,265.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:268.122,275.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:278.122,285.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:289.119,291.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:295.84,300.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:304.89,309.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:312.54,314.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:317.53,319.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:323.59,325.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:329.51,331.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:335.102,337.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:341.77,343.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:346.72,348.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:351.71,353.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:357.71,359.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:362.64,364.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:367.63,369.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:373.63,375.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:379.79,381.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:385.89,387.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:391.46,393.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:397.64,399.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:403.89,405.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:409.103,411.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:415.135,417.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:421.105,423.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:427.129,429.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:433.120,440.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:444.120,449.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:453.117,455.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:459.77,461.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:465.86,467.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:471.104,472.34 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:472.34,474.3 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:479.123,487.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:491.84,498.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:503.68,505.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:509.98,511.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:515.94,517.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:520.106,522.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:526.103,528.2 1 1
github.com/IBM/fp-go/v2/readerioeither/reader.go:533.101,535.2 1 1
github.com/IBM/fp-go/v2/readerioeither/resource.go:21.181,22.73 1 1
github.com/IBM/fp-go/v2/readerioeither/resource.go:22.73,23.44 1 1
github.com/IBM/fp-go/v2/readerioeither/resource.go:23.44,27.41 1 1
github.com/IBM/fp-go/v2/readerioeither/resource.go:27.41,29.6 1 1
github.com/IBM/fp-go/v2/readerioeither/resource.go:30.40,32.5 1 1
github.com/IBM/fp-go/v2/readerioeither/sequence.go:25.91,30.2 1 1
github.com/IBM/fp-go/v2/readerioeither/sequence.go:32.124,38.2 1 1
github.com/IBM/fp-go/v2/readerioeither/sequence.go:40.157,47.2 1 1
github.com/IBM/fp-go/v2/readerioeither/sequence.go:49.190,57.2 1 1
github.com/IBM/fp-go/v2/readerioeither/sync.go:26.81,28.2 1 1
github.com/IBM/fp-go/v2/readerioeither/traverse.go:23.107,25.2 1 1
github.com/IBM/fp-go/v2/readerioeither/traverse.go:28.121,30.2 1 1
github.com/IBM/fp-go/v2/readerioeither/traverse.go:33.89,35.2 1 1
github.com/IBM/fp-go/v2/readerioeither/traverse.go:38.134,40.2 1 1
github.com/IBM/fp-go/v2/readerioeither/traverse.go:43.146,45.2 1 1
github.com/IBM/fp-go/v2/readerioeither/traverse.go:48.116,50.2 1 1

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeriooption
import (
RA "github.com/IBM/fp-go/v2/internal/array"
)
// TraverseArray transforms an array by applying a function that returns a ReaderIOOption to each element.
// If any element results in None, the entire result is None.
// Otherwise, returns Some containing an array of all the unwrapped values.
//
// This is useful for performing a sequence of operations that may fail on each element of an array,
// where you want all operations to succeed or the entire computation to fail.
//
// Example:
//
// type DB struct { ... }
//
// findUser := func(id int) readeroption.ReaderIOOption[DB, User] { ... }
//
// userIDs := []int{1, 2, 3}
// result := F.Pipe1(
// readeroption.Of[DB](userIDs),
// readeroption.Chain(readeroption.TraverseArray[DB](findUser)),
// )
// // result will be Some([]User) if all users are found, None otherwise
func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
return RA.Traverse[[]A, []B](
Of,
Map,
Ap,
f,
)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index of each element.
//
// Example:
//
// type DB struct { ... }
//
// processWithIndex := func(idx int, value string) readeroption.ReaderIOOption[DB, Result] {
// // Use idx in processing
// return readeroption.Asks(func(db DB) option.Option[Result] { ... })
// }
//
// values := []string{"a", "b", "c"}
// result := readeroption.TraverseArrayWithIndex[DB](processWithIndex)(values)
func TraverseArrayWithIndex[E, A, B any](f func(int, A) ReaderIOOption[E, B]) func([]A) ReaderIOOption[E, []B] {
return RA.TraverseWithIndex[[]A, []B](
Of,
Map,
Ap,
f,
)
}

View File

@@ -0,0 +1,258 @@
// 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 readeriooption
import (
"context"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestTraverseArray_AllSuccess(t *testing.T) {
// Test traversing an array where all operations succeed
double := func(x int) ReaderIOOption[context.Context, int] {
return Of[context.Context](x * 2)
}
input := []int{1, 2, 3, 4, 5}
result := TraverseArray[context.Context](double)(input)
expected := O.Of([]int{2, 4, 6, 8, 10})
assert.Equal(t, expected, result(context.Background())())
}
func TestTraverseArray_OneFailure(t *testing.T) {
// Test traversing an array where one operation fails
failOnThree := func(x int) ReaderIOOption[context.Context, int] {
if x == 3 {
return None[context.Context, int]()
}
return Of[context.Context](x * 2)
}
input := []int{1, 2, 3, 4, 5}
result := TraverseArray[context.Context](failOnThree)(input)
expected := O.None[[]int]()
assert.Equal(t, expected, result(context.Background())())
}
func TestTraverseArray_EmptyArray(t *testing.T) {
// Test traversing an empty array
double := func(x int) ReaderIOOption[context.Context, int] {
return Of[context.Context](x * 2)
}
input := []int{}
result := TraverseArray[context.Context](double)(input)
expected := O.Of([]int{})
assert.Equal(t, expected, result(context.Background())())
}
func TestTraverseArray_WithEnvironment(t *testing.T) {
// Test that the environment is properly passed through
type Config struct {
Multiplier int
}
multiply := func(x int) ReaderIOOption[Config, int] {
return func(cfg Config) IOOption[int] {
return func() Option[int] {
return O.Of(x * cfg.Multiplier)
}
}
}
input := []int{1, 2, 3}
result := TraverseArray[Config](multiply)(input)
cfg := Config{Multiplier: 10}
expected := O.Of([]int{10, 20, 30})
assert.Equal(t, expected, result(cfg)())
}
func TestTraverseArray_ChainedOperation(t *testing.T) {
// Test traversing as part of a chain
type Config struct {
Factor int
}
multiplyByFactor := func(x int) ReaderIOOption[Config, int] {
return func(cfg Config) IOOption[int] {
return func() Option[int] {
return O.Of(x * cfg.Factor)
}
}
}
result := F.Pipe1(
Of[Config]([]int{1, 2, 3, 4}),
Chain(TraverseArray[Config](multiplyByFactor)),
)
cfg := Config{Factor: 5}
expected := O.Of([]int{5, 10, 15, 20})
assert.Equal(t, expected, result(cfg)())
}
func TestTraverseArrayWithIndex_AllSuccess(t *testing.T) {
// Test traversing with index where all operations succeed
addIndex := func(idx int, x string) ReaderIOOption[context.Context, string] {
return Of[context.Context](fmt.Sprintf("%d:%s", idx, x))
}
input := []string{"a", "b", "c"}
result := TraverseArrayWithIndex[context.Context](addIndex)(input)
expected := O.Of([]string{"0:a", "1:b", "2:c"})
assert.Equal(t, expected, result(context.Background())())
}
func TestTraverseArrayWithIndex_OneFailure(t *testing.T) {
// Test traversing with index where one operation fails
failOnIndex := func(idx int, x string) ReaderIOOption[context.Context, string] {
if idx == 1 {
return None[context.Context, string]()
}
return Of[context.Context](fmt.Sprintf("%d:%s", idx, x))
}
input := []string{"a", "b", "c"}
result := TraverseArrayWithIndex[context.Context](failOnIndex)(input)
expected := O.None[[]string]()
assert.Equal(t, expected, result(context.Background())())
}
func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
// Test traversing an empty array with index
addIndex := func(idx int, x string) ReaderIOOption[context.Context, string] {
return Of[context.Context](fmt.Sprintf("%d:%s", idx, x))
}
input := []string{}
result := TraverseArrayWithIndex[context.Context](addIndex)(input)
expected := O.Of([]string{})
assert.Equal(t, expected, result(context.Background())())
}
func TestTraverseArrayWithIndex_WithEnvironment(t *testing.T) {
// Test that environment is properly passed with index
type Config struct {
Prefix string
}
formatWithIndex := func(idx int, x string) ReaderIOOption[Config, string] {
return func(cfg Config) IOOption[string] {
return func() Option[string] {
return O.Of(fmt.Sprintf("%s%d:%s", cfg.Prefix, idx, x))
}
}
}
input := []string{"a", "b", "c"}
result := TraverseArrayWithIndex[Config](formatWithIndex)(input)
cfg := Config{Prefix: "item-"}
expected := O.Of([]string{"item-0:a", "item-1:b", "item-2:c"})
assert.Equal(t, expected, result(cfg)())
}
func TestTraverseArrayWithIndex_IndexUsedInLogic(t *testing.T) {
// Test using index in computation logic
multiplyByIndex := func(idx int, x int) ReaderIOOption[context.Context, int] {
return Of[context.Context](x * idx)
}
input := []int{10, 20, 30, 40}
result := TraverseArrayWithIndex[context.Context](multiplyByIndex)(input)
// 10*0=0, 20*1=20, 30*2=60, 40*3=120
expected := O.Of([]int{0, 20, 60, 120})
assert.Equal(t, expected, result(context.Background())())
}
func TestTraverseArray_ComplexType(t *testing.T) {
// Test traversing with complex types
type User struct {
ID int
Name string
}
type UserProfile struct {
UserID int
DisplayName string
}
loadProfile := func(user User) ReaderIOOption[context.Context, UserProfile] {
return Of[context.Context](UserProfile{
UserID: user.ID,
DisplayName: "Profile: " + user.Name,
})
}
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Charlie"},
}
result := TraverseArray[context.Context](loadProfile)(users)
expected := O.Of([]UserProfile{
{UserID: 1, DisplayName: "Profile: Alice"},
{UserID: 2, DisplayName: "Profile: Bob"},
{UserID: 3, DisplayName: "Profile: Charlie"},
})
assert.Equal(t, expected, result(context.Background())())
}
func TestTraverseArray_ConditionalFailure(t *testing.T) {
// Test conditional failure based on environment
type Config struct {
MaxValue int
}
validateAndDouble := func(x int) ReaderIOOption[Config, int] {
return func(cfg Config) IOOption[int] {
return func() Option[int] {
if x > cfg.MaxValue {
return O.None[int]()
}
return O.Of(x * 2)
}
}
}
input := []int{1, 2, 3, 4, 5}
// With MaxValue=3, should fail on 4 and 5
cfg1 := Config{MaxValue: 3}
result1 := TraverseArray[Config](validateAndDouble)(input)
assert.Equal(t, O.None[[]int](), result1(cfg1)())
// With MaxValue=10, all should succeed
cfg2 := Config{MaxValue: 10}
result2 := TraverseArray[Config](validateAndDouble)(input)
expected := O.Of([]int{2, 4, 6, 8, 10})
assert.Equal(t, expected, result2(cfg2)())
}

326
v2/readeriooption/bind.go Normal file
View File

@@ -0,0 +1,326 @@
// 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 readeriooption
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
// result := readereither.Do[Env, error](State{})
func Do[R, S any](
empty S,
) ReaderIOOption[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.ReaderIOOption[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.ReaderIOOption[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)
// })
// },
// ),
// )
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[R, S1, T],
) Operator[R, S1, S2] {
return chain.Bind(
Chain,
Map,
setter,
f,
)
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
func Let[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[R, S1, S2] {
return functor.Let(
Map[R, S1, S2],
setter,
f,
)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
func LetTo[R, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[R, S1, S2] {
return functor.LetTo(
Map[R, S1, S2],
setter,
b,
)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[R, S1, T any](
setter func(T) S1,
) Operator[R, T, S1] {
return chain.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,
// ),
// )
func ApS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIOOption[R, T],
) Operator[R, S1, S2] {
return apply.ApS(
Ap[S2],
Map,
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),
// )
func ApSL[R, S, T any](
lens L.Lens[S, T],
fa ReaderIOOption[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 ReaderIOOption 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.ReaderIOOption[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// }),
// )
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 ReaderIOOption).
//
// 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
// }),
// )
func LetL[R, S, T any](
lens L.Lens[S, T],
f func(T) 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),
// )
func LetToL[R, S, T any](
lens L.Lens[S, T],
b T,
) Operator[R, S, S] {
return LetTo[R](lens.Set, b)
}

View File

@@ -0,0 +1,99 @@
// 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 readeriooption
import (
"context"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func getLastName(s utils.Initial) ReaderIOOption[context.Context, string] {
return Of[context.Context]("Doe")
}
func getGivenName(s utils.WithLastName) ReaderIOOption[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, O.Of("John Doe"), res(context.Background())())
}
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, O.Of("John Doe"), res(context.Background())())
}
func TestLet(t *testing.T) {
res := F.Pipe3(
Do[context.Context](utils.Empty),
Let[context.Context](utils.SetLastName, func(s utils.Initial) string {
return "Doe"
}),
Let[context.Context](utils.SetGivenName, func(s utils.WithLastName) string {
return "John"
}),
Map[context.Context](utils.GetFullName),
)
assert.Equal(t, O.Of("John Doe"), res(context.Background())())
}
func TestLetTo(t *testing.T) {
res := F.Pipe3(
Do[context.Context](utils.Empty),
LetTo[context.Context](utils.SetLastName, "Doe"),
LetTo[context.Context](utils.SetGivenName, "John"),
Map[context.Context](utils.GetFullName),
)
assert.Equal(t, O.Of("John Doe"), res(context.Background())())
}
func TestBindTo(t *testing.T) {
type State struct {
Value int
}
res := F.Pipe1(
Of[context.Context](42),
BindTo[context.Context](func(v int) State {
return State{Value: v}
}),
)
assert.Equal(t, O.Of(State{Value: 42}), res(context.Background())())
}

243
v2/readeriooption/doc.go Normal file
View File

@@ -0,0 +1,243 @@
// 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 readeriooption provides a monad transformer that combines the Reader, IO, and Option monads.
//
// # Overview
//
// ReaderIOOption[R, A] represents a computation that:
// - Depends on a shared environment of type R (Reader monad)
// - Performs side effects (IO monad)
// - May fail to produce a value of type A (Option monad)
//
// This is particularly useful for computations that need:
// - Dependency injection or configuration access
// - Side effects like I/O operations
// - Optional results without using error types
//
// The ReaderIOOption monad is defined as: Reader[R, IOOption[A]]
//
// # Fantasy Land Specification
//
// This package implements the following Fantasy Land algebras:
// - Functor: https://github.com/fantasyland/fantasy-land#functor
// - Apply: https://github.com/fantasyland/fantasy-land#apply
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
// - Chain: https://github.com/fantasyland/fantasy-land#chain
// - Monad: https://github.com/fantasyland/fantasy-land#monad
// - Alt: https://github.com/fantasyland/fantasy-land#alt
// - Profunctor: https://github.com/fantasyland/fantasy-land#profunctor
//
// # Core Operations
//
// Creating ReaderIOOption values:
// - Of/Some: Wraps a value in a successful ReaderIOOption
// - None: Creates a ReaderIOOption representing no value
// - FromOption: Lifts an Option into ReaderIOOption
// - FromReader: Lifts a Reader into ReaderIOOption
// - Ask/Asks: Accesses the environment
//
// Transforming values:
// - Map: Transforms the value inside a ReaderIOOption
// - Chain: Sequences ReaderIOOption computations
// - Ap: Applies a function wrapped in ReaderIOOption
// - Alt: Provides alternative computation on failure
//
// Extracting values:
// - Fold: Extracts value by providing handlers for both cases
// - GetOrElse: Returns value or default
// - Read: Executes the computation with an environment
//
// # Basic Example
//
// type Config struct {
// DatabaseURL string
// Timeout int
// }
//
// // A computation that may or may not find a user
// func findUser(id int) readeriooption.ReaderIOOption[Config, User] {
// return readeriooption.Asks(func(cfg Config) iooption.IOOption[User] {
// return func() option.Option[User] {
// // Use cfg.DatabaseURL to query database
// // Return Some(user) if found, None() if not found
// user, found := queryDB(cfg.DatabaseURL, id)
// if found {
// return option.Some(user)
// }
// return option.None[User]()
// }
// })
// }
//
// // Chain multiple operations
// result := F.Pipe2(
// findUser(123),
// readeriooption.Chain(func(user User) readeriooption.ReaderIOOption[Config, Profile] {
// return loadProfile(user.ProfileID)
// }),
// readeriooption.Map(func(profile Profile) string {
// return profile.DisplayName
// }),
// )
//
// // Execute with config
// config := Config{DatabaseURL: "localhost:5432", Timeout: 30}
// displayName := result(config)() // Returns Option[string]
//
// # Do-Notation Style
//
// The package supports do-notation style composition for building complex computations:
//
// type State struct {
// User User
// Profile Profile
// Posts []Post
// }
//
// result := F.Pipe3(
// readeriooption.Do[Config](State{}),
// readeriooption.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readeriooption.ReaderIOOption[Config, User] {
// return findUser(123)
// },
// ),
// readeriooption.Bind(
// func(profile Profile) func(State) State {
// return func(s State) State { s.Profile = profile; return s }
// },
// func(s State) readeriooption.ReaderIOOption[Config, Profile] {
// return loadProfile(s.User.ProfileID)
// },
// ),
// readeriooption.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readeriooption.ReaderIOOption[Config, []Post] {
// return loadPosts(s.User.ID)
// },
// ),
// )
//
// # Alternative Computations
//
// Use Alt to provide fallback behavior when computations fail:
//
// // Try cache first, fall back to database
// result := F.Pipe1(
// findUserInCache(123),
// readeriooption.Alt(func() readeriooption.ReaderIOOption[Config, User] {
// return findUserInDB(123)
// }),
// )
//
// # Array Operations
//
// Transform arrays where each element may fail:
//
// userIDs := []int{1, 2, 3, 4, 5}
// users := F.Pipe1(
// readeriooption.Of[Config](userIDs),
// readeriooption.Chain(readeriooption.TraverseArray[Config](findUser)),
// )
// // Returns Some([]User) if all users found, None otherwise
//
// # Monoid Operations
//
// Combine multiple ReaderIOOption computations:
//
// import N "github.com/IBM/fp-go/v2/number"
//
// // Applicative monoid - all must succeed
// intAdd := N.MonoidSum[int]()
// roMonoid := readeriooption.ApplicativeMonoid[Config](intAdd)
// combined := roMonoid.Concat(
// readeriooption.Of[Config](5),
// readeriooption.Of[Config](3),
// )
// // Returns Some(8)
//
// // Alternative monoid - provides fallback
// altMonoid := readeriooption.AlternativeMonoid[Config](intAdd)
// withFallback := altMonoid.Concat(
// readeriooption.None[Config, int](),
// readeriooption.Of[Config](10),
// )
// // Returns Some(10)
//
// # Profunctor Operations
//
// Transform both input and output:
//
// type GlobalConfig struct {
// DB DBConfig
// }
//
// type DBConfig struct {
// Host string
// }
//
// // Adapt environment and transform result
// adapted := F.Pipe1(
// queryDB, // ReaderIOOption[DBConfig, User]
// readeriooption.Promap(
// func(g GlobalConfig) DBConfig { return g.DB },
// func(u User) string { return u.Name },
// ),
// )
// // Now: ReaderIOOption[GlobalConfig, string]
//
// # Tail Recursion
//
// For recursive computations, use TailRec to avoid stack overflow:
//
// func factorial(n int) readeriooption.ReaderIOOption[Config, int] {
// return readeriooption.TailRec(func(acc int) readeriooption.ReaderIOOption[Config, tailrec.Trampoline[int, int]] {
// if n <= 1 {
// return readeriooption.Of[Config](tailrec.Done[int](acc))
// }
// return readeriooption.Of[Config](tailrec.Continue[int](acc * n))
// })(1)
// }
//
// # Relationship to Other Monads
//
// ReaderIOOption is related to other monads in the fp-go library:
// - reader: ReaderIOOption adds IO and Option capabilities
// - readerio: ReaderIOOption adds Option capability
// - readeroption: ReaderIOOption adds IO capability
// - iooption: ReaderIOOption adds Reader capability
// - option: ReaderIOOption adds Reader and IO capabilities
//
// # Type Safety
//
// The type system ensures:
// - Environment dependencies are explicit in the type signature
// - Side effects are tracked through the IO layer
// - Optional results are handled explicitly
// - Composition maintains type safety
//
// # Performance Considerations
//
// ReaderIOOption computations are lazy and only execute when:
// 1. An environment is provided (Reader layer)
// 2. The IO action is invoked (IO layer)
//
// This allows for efficient composition without premature execution.
package readeriooption

145
v2/readeriooption/monoid.go Normal file
View File

@@ -0,0 +1,145 @@
// 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 readeriooption
import "github.com/IBM/fp-go/v2/monoid"
// ApplicativeMonoid creates a Monoid for ReaderIOOption 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 (return Some) for the result to succeed.
//
// This is useful for accumulating results from multiple independent computations that all need
// to succeed. If any computation returns None, the entire result is None.
//
// The resulting monoid satisfies the monoid laws:
// - Left identity: Concat(Empty(), x) = x
// - Right identity: Concat(x, Empty()) = x
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Parameters:
// - m: The underlying monoid for combining success values of type A
//
// Returns:
// - A Monoid[ReaderIOOption[R, A]] that combines ReaderIOOption computations
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// RO "github.com/IBM/fp-go/v2/readeroption"
// )
//
// // Create a monoid for integer addition
// intAdd := N.MonoidSum[int]()
// roMonoid := RO.ApplicativeMonoid[Config](intAdd)
//
// // Combine successful computations
// ro1 := RO.Of[Config](5)
// ro2 := RO.Of[Config](3)
// combined := roMonoid.Concat(ro1, ro2)
// // combined(cfg) returns option.Some(8)
//
// // If either fails, the whole computation fails
// ro3 := RO.None[Config, int]()
// failed := roMonoid.Concat(ro1, ro3)
// // failed(cfg) returns option.None[int]()
//
// // Empty element is the identity
// withEmpty := roMonoid.Concat(ro1, roMonoid.Empty())
// // withEmpty(cfg) returns option.Some(5)
//
//go:inline
func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) monoid.Monoid[ReaderIOOption[R, A]] {
return monoid.ApplicativeMonoid(
Of[R, A],
MonadMap[R, A, func(A) A],
MonadAp[R, A, A],
m,
)
}
// AlternativeMonoid creates a Monoid for ReaderIOOption 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 (returns None), then combines successful values using the underlying monoid.
//
// This is particularly useful when you want to:
// - Try multiple computations and accumulate their results
// - Provide fallback behavior when computations fail
// - Combine results from computations that may or may not succeed
//
// The behavior differs from ApplicativeMonoid in that it provides fallback semantics:
// - If the first computation succeeds, use its value
// - If the first fails but the second succeeds, use the second's value
// - If both succeed, combine their values using the underlying monoid
// - If both fail, the result is None
//
// The resulting monoid satisfies the monoid laws:
// - Left identity: Concat(Empty(), x) = x
// - Right identity: Concat(x, Empty()) = x
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Parameters:
// - m: The underlying monoid for combining success values of type A
//
// Returns:
// - A Monoid[ReaderIOOption[R, A]] that combines ReaderIOOption computations with fallback
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// RO "github.com/IBM/fp-go/v2/readeroption"
// )
//
// // Create a monoid for integer addition with alternative behavior
// intAdd := N.MonoidSum[int]()
// roMonoid := RO.AlternativeMonoid[Config](intAdd)
//
// // Combine successful computations
// ro1 := RO.Of[Config](5)
// ro2 := RO.Of[Config](3)
// combined := roMonoid.Concat(ro1, ro2)
// // combined(cfg) returns option.Some(8)
//
// // Fallback when first fails
// ro3 := RO.None[Config, int]()
// ro4 := RO.Of[Config](10)
// withFallback := roMonoid.Concat(ro3, ro4)
// // withFallback(cfg) returns option.Some(10)
//
// // Use first success when available
// withFirst := roMonoid.Concat(ro1, ro3)
// // withFirst(cfg) returns option.Some(5)
//
// // Accumulate multiple values with some failures
// result := roMonoid.Concat(
// roMonoid.Concat(ro3, ro1), // None + 5 = 5
// ro2, // 5 + 3 = 8
// )
// // result(cfg) returns option.Some(8)
//
//go:inline
func AlternativeMonoid[R, A any](m monoid.Monoid[A]) monoid.Monoid[ReaderIOOption[R, A]] {
return monoid.AlternativeMonoid(
Of[R, A],
MonadMap[R, A, func(A) A],
MonadAp[R, A, A],
MonadAlt[R, A],
m,
)
}

View File

@@ -0,0 +1,357 @@
// 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 readeriooption
import (
"context"
"testing"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestApplicativeMonoid_BothSuccess(t *testing.T) {
// Test combining two successful computations
intAdd := N.MonoidSum[int]()
m := ApplicativeMonoid[context.Context](intAdd)
ro1 := Of[context.Context](5)
ro2 := Of[context.Context](3)
result := m.Concat(ro1, ro2)
expected := O.Of(8)
assert.Equal(t, expected, result(context.Background())())
}
func TestApplicativeMonoid_FirstFailure(t *testing.T) {
// Test when first computation fails
intAdd := N.MonoidSum[int]()
m := ApplicativeMonoid[context.Context](intAdd)
ro1 := None[context.Context, int]()
ro2 := Of[context.Context](3)
result := m.Concat(ro1, ro2)
expected := O.None[int]()
assert.Equal(t, expected, result(context.Background())())
}
func TestApplicativeMonoid_SecondFailure(t *testing.T) {
// Test when second computation fails
intAdd := N.MonoidSum[int]()
m := ApplicativeMonoid[context.Context](intAdd)
ro1 := Of[context.Context](5)
ro2 := None[context.Context, int]()
result := m.Concat(ro1, ro2)
expected := O.None[int]()
assert.Equal(t, expected, result(context.Background())())
}
func TestApplicativeMonoid_BothFailure(t *testing.T) {
// Test when both computations fail
intAdd := N.MonoidSum[int]()
m := ApplicativeMonoid[context.Context](intAdd)
ro1 := None[context.Context, int]()
ro2 := None[context.Context, int]()
result := m.Concat(ro1, ro2)
expected := O.None[int]()
assert.Equal(t, expected, result(context.Background())())
}
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
// Test left identity: Concat(Empty(), x) = x
intAdd := N.MonoidSum[int]()
m := ApplicativeMonoid[context.Context](intAdd)
ro := Of[context.Context](5)
result := m.Concat(m.Empty(), ro)
assert.Equal(t, O.Of(5), result(context.Background())())
}
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
// Test right identity: Concat(x, Empty()) = x
intAdd := N.MonoidSum[int]()
m := ApplicativeMonoid[context.Context](intAdd)
ro := Of[context.Context](5)
result := m.Concat(ro, m.Empty())
assert.Equal(t, O.Of(5), result(context.Background())())
}
func TestApplicativeMonoid_Associativity(t *testing.T) {
// Test associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
intAdd := N.MonoidSum[int]()
m := ApplicativeMonoid[context.Context](intAdd)
ro1 := Of[context.Context](2)
ro2 := Of[context.Context](3)
ro3 := Of[context.Context](5)
left := m.Concat(m.Concat(ro1, ro2), ro3)
right := m.Concat(ro1, m.Concat(ro2, ro3))
assert.Equal(t, O.Of(10), left(context.Background())())
assert.Equal(t, O.Of(10), right(context.Background())())
}
func TestApplicativeMonoid_StringConcat(t *testing.T) {
// Test with string concatenation monoid
strConcat := S.Monoid
m := ApplicativeMonoid[context.Context](strConcat)
ro1 := Of[context.Context]("Hello")
ro2 := Of[context.Context](" ")
ro3 := Of[context.Context]("World")
result := m.Concat(m.Concat(ro1, ro2), ro3)
expected := O.Of("Hello World")
assert.Equal(t, expected, result(context.Background())())
}
func TestApplicativeMonoid_WithEnvironment(t *testing.T) {
// Test that environment is properly passed through
type Config struct {
Factor int
}
intAdd := N.MonoidSum[int]()
m := ApplicativeMonoid[Config](intAdd)
ro1 := func(cfg Config) IOOption[int] {
return func() Option[int] {
return O.Of(10 * cfg.Factor)
}
}
ro2 := func(cfg Config) IOOption[int] {
return func() Option[int] {
return O.Of(5 * cfg.Factor)
}
}
result := m.Concat(ro1, ro2)
cfg := Config{Factor: 2}
expected := O.Of(30) // (10*2) + (5*2) = 30
assert.Equal(t, expected, result(cfg)())
}
func TestAlternativeMonoid_BothSuccess(t *testing.T) {
// Test combining two successful computations
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro1 := Of[context.Context](5)
ro2 := Of[context.Context](3)
result := m.Concat(ro1, ro2)
expected := O.Of(8)
assert.Equal(t, expected, result(context.Background())())
}
func TestAlternativeMonoid_FirstFailure(t *testing.T) {
// Test fallback when first computation fails
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro1 := None[context.Context, int]()
ro2 := Of[context.Context](10)
result := m.Concat(ro1, ro2)
expected := O.Of(10)
assert.Equal(t, expected, result(context.Background())())
}
func TestAlternativeMonoid_SecondFailure(t *testing.T) {
// Test using first success when second fails
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro1 := Of[context.Context](5)
ro2 := None[context.Context, int]()
result := m.Concat(ro1, ro2)
expected := O.Of(5)
assert.Equal(t, expected, result(context.Background())())
}
func TestAlternativeMonoid_BothFailure(t *testing.T) {
// Test when both computations fail
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro1 := None[context.Context, int]()
ro2 := None[context.Context, int]()
result := m.Concat(ro1, ro2)
expected := O.None[int]()
assert.Equal(t, expected, result(context.Background())())
}
func TestAlternativeMonoid_LeftIdentity(t *testing.T) {
// Test left identity: Concat(Empty(), x) = x
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro := Of[context.Context](5)
result := m.Concat(m.Empty(), ro)
assert.Equal(t, O.Of(5), result(context.Background())())
}
func TestAlternativeMonoid_RightIdentity(t *testing.T) {
// Test right identity: Concat(x, Empty()) = x
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro := Of[context.Context](5)
result := m.Concat(ro, m.Empty())
assert.Equal(t, O.Of(5), result(context.Background())())
}
func TestAlternativeMonoid_Associativity(t *testing.T) {
// Test associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro1 := Of[context.Context](2)
ro2 := Of[context.Context](3)
ro3 := Of[context.Context](5)
left := m.Concat(m.Concat(ro1, ro2), ro3)
right := m.Concat(ro1, m.Concat(ro2, ro3))
assert.Equal(t, O.Of(10), left(context.Background())())
assert.Equal(t, O.Of(10), right(context.Background())())
}
func TestAlternativeMonoid_FallbackChain(t *testing.T) {
// Test chaining multiple fallbacks
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro1 := None[context.Context, int]()
ro2 := None[context.Context, int]()
ro3 := Of[context.Context](7)
ro4 := Of[context.Context](3)
// None + None = None, then None + 7 = 7, then 7 + 3 = 10
result := m.Concat(m.Concat(m.Concat(ro1, ro2), ro3), ro4)
expected := O.Of(10)
assert.Equal(t, expected, result(context.Background())())
}
func TestAlternativeMonoid_WithEnvironment(t *testing.T) {
// Test that environment is properly passed through with fallback
type Config struct {
UseCache bool
Factor int
}
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[Config](intAdd)
cacheValue := func(cfg Config) IOOption[int] {
return func() Option[int] {
if cfg.UseCache {
return O.Of(100)
}
return O.None[int]()
}
}
dbValue := func(cfg Config) IOOption[int] {
return func() Option[int] {
return O.Of(50 * cfg.Factor)
}
}
result := m.Concat(cacheValue, dbValue)
// With cache enabled, both succeed so values are combined: 100 + (50*2) = 200
cfg1 := Config{UseCache: true, Factor: 2}
assert.Equal(t, O.Of(200), result(cfg1)())
// With cache disabled, should fall back to DB value: 0 + (50*2) = 100
cfg2 := Config{UseCache: false, Factor: 2}
assert.Equal(t, O.Of(100), result(cfg2)())
}
func TestAlternativeMonoid_StringConcat(t *testing.T) {
// Test with string concatenation and fallback
strConcat := S.Monoid
m := AlternativeMonoid[context.Context](strConcat)
ro1 := None[context.Context, string]()
ro2 := Of[context.Context]("Hello")
ro3 := Of[context.Context](" World")
result := m.Concat(m.Concat(ro1, ro2), ro3)
expected := O.Of("Hello World")
assert.Equal(t, expected, result(context.Background())())
}
func TestAlternativeMonoid_MultipleSuccesses(t *testing.T) {
// Test accumulating multiple successful values
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro1 := Of[context.Context](1)
ro2 := Of[context.Context](2)
ro3 := Of[context.Context](3)
ro4 := Of[context.Context](4)
result := m.Concat(m.Concat(m.Concat(ro1, ro2), ro3), ro4)
expected := O.Of(10)
assert.Equal(t, expected, result(context.Background())())
}
func TestAlternativeMonoid_InterspersedFailures(t *testing.T) {
// Test with failures interspersed between successes
intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[context.Context](intAdd)
ro1 := Of[context.Context](5)
ro2 := None[context.Context, int]()
ro3 := Of[context.Context](3)
ro4 := None[context.Context, int]()
ro5 := Of[context.Context](2)
result := m.Concat(m.Concat(m.Concat(m.Concat(ro1, ro2), ro3), ro4), ro5)
expected := O.Of(10) // 5 + 3 + 2
assert.Equal(t, expected, result(context.Background())())
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeriooption
import (
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/reader"
)
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOOption.
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Adapt the environment type before passing it to the ReaderIOOption (via f)
// - Transform the Some value after the computation completes (via g)
//
// The None case remains unchanged through the transformation.
//
// Type Parameters:
// - R: The original environment type expected by the ReaderIOOption
// - A: The original value type produced by the ReaderIOOption
// - D: The new input environment type
// - B: The new output value type
//
// Parameters:
// - f: Function to transform the input environment from D to R (contravariant)
// - g: Function to transform the output Some value from A to B (covariant)
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOOption[R, A] and returns a ReaderIOOption[D, B]
//
//go:inline
func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderIOOption[R, A], B] {
return reader.Promap(f, iooption.Map(g))
}
// Contramap changes the value of the local environment during the execution of a ReaderIOOption.
// This is the contravariant functor operation that transforms the input environment.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is useful for adapting a ReaderIOOption to work with a different environment type
// by providing a function that converts the new environment to the expected one.
//
// Type Parameters:
// - A: The value type (unchanged)
// - R2: The new input environment type
// - R1: The original environment type expected by the ReaderIOOption
//
// Parameters:
// - f: Function to transform the environment from R2 to R1
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOOption[R1, A] and returns a ReaderIOOption[R2, A]
//
//go:inline
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOOption[R1, A], A] {
return reader.Contramap[IOOption[A]](f)
}

View File

@@ -0,0 +1,395 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeriooption
import (
"context"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestPromap_TransformBoth(t *testing.T) {
// Test transforming both input environment and output value
type GlobalConfig struct {
Factor int
}
type LocalConfig struct {
Multiplier int
}
// Original computation expects LocalConfig and returns int
original := func(cfg LocalConfig) IOOption[int] {
return func() Option[int] {
return O.Of(10 * cfg.Multiplier)
}
}
// Transform GlobalConfig to LocalConfig (contravariant)
envTransform := func(g GlobalConfig) LocalConfig {
return LocalConfig{Multiplier: g.Factor}
}
// Transform int to string (covariant)
valueTransform := func(n int) string {
return fmt.Sprintf("%d", n)
}
// Apply Promap
adapted := F.Pipe1(
original,
Promap(envTransform, valueTransform),
)
globalCfg := GlobalConfig{Factor: 5}
result := adapted(globalCfg)()
expected := O.Of("50")
assert.Equal(t, expected, result)
}
func TestPromap_WithNone(t *testing.T) {
// Test that None is preserved through Promap
type Config1 struct {
Value int
}
type Config2 struct {
Data int
}
original := None[Config1, int]()
envTransform := func(c2 Config2) Config1 {
return Config1{Value: c2.Data}
}
valueTransform := func(n int) string {
return fmt.Sprintf("%d", n)
}
adapted := F.Pipe1(
original,
Promap(envTransform, valueTransform),
)
cfg := Config2{Data: 10}
result := adapted(cfg)()
expected := O.None[string]()
assert.Equal(t, expected, result)
}
func TestPromap_Identity(t *testing.T) {
// Test that Promap with identity functions is identity
original := Of[context.Context](42)
adapted := F.Pipe1(
original,
Promap(
F.Identity[context.Context],
F.Identity[int],
),
)
result := adapted(context.Background())()
expected := O.Of(42)
assert.Equal(t, expected, result)
}
func TestPromap_Composition(t *testing.T) {
// Test that Promap composes correctly
type Config1 struct{ A int }
type Config2 struct{ B int }
type Config3 struct{ C int }
original := func(c1 Config1) IOOption[int] {
return func() Option[int] {
return O.Of(c1.A * 2)
}
}
// First transformation
f1 := func(c2 Config2) Config1 { return Config1{A: c2.B + 1} }
g1 := func(n int) int { return n * 3 }
// Second transformation
f2 := func(c3 Config3) Config2 { return Config2{B: c3.C + 2} }
g2 := func(n int) string { return fmt.Sprintf("%d", n) }
// Apply transformations separately
step1 := F.Pipe1(original, Promap(f1, g1))
step2 := F.Pipe1(step1, Promap(f2, g2))
// Apply composed transformation
composed := F.Pipe1(
original,
Promap(
F.Flow2(f2, f1),
F.Flow2(g1, g2),
),
)
cfg := Config3{C: 5}
result1 := step2(cfg)()
result2 := composed(cfg)()
// Both should give the same result: ((5+2+1)*2)*3 = 48
expected := O.Of("48")
assert.Equal(t, expected, result1)
assert.Equal(t, expected, result2)
}
func TestContramap_TransformEnvironment(t *testing.T) {
// Test transforming only the environment
type GlobalConfig struct {
DatabaseURL string
Port int
}
type DBConfig struct {
URL string
}
// Original computation expects DBConfig
original := func(cfg DBConfig) IOOption[string] {
return func() Option[string] {
return O.Of("Connected to: " + cfg.URL)
}
}
// Transform GlobalConfig to DBConfig
envTransform := func(g GlobalConfig) DBConfig {
return DBConfig{URL: g.DatabaseURL}
}
// Apply Contramap
adapted := F.Pipe1(
original,
Contramap[string](envTransform),
)
globalCfg := GlobalConfig{
DatabaseURL: "localhost:5432",
Port: 8080,
}
result := adapted(globalCfg)()
expected := O.Of("Connected to: localhost:5432")
assert.Equal(t, expected, result)
}
func TestContramap_WithNone(t *testing.T) {
// Test that None is preserved through Contramap
type Config1 struct {
Value int
}
type Config2 struct {
Data int
}
original := None[Config1, string]()
envTransform := func(c2 Config2) Config1 {
return Config1{Value: c2.Data}
}
adapted := F.Pipe1(
original,
Contramap[string](envTransform),
)
cfg := Config2{Data: 10}
result := adapted(cfg)()
expected := O.None[string]()
assert.Equal(t, expected, result)
}
func TestContramap_Identity(t *testing.T) {
// Test that Contramap with identity function is identity
original := Of[context.Context](42)
adapted := F.Pipe1(
original,
Contramap[int](F.Identity[context.Context]),
)
result := adapted(context.Background())()
expected := O.Of(42)
assert.Equal(t, expected, result)
}
func TestContramap_Composition(t *testing.T) {
// Test that Contramap composes correctly
type Config1 struct{ A int }
type Config2 struct{ B int }
type Config3 struct{ C int }
original := func(c1 Config1) IOOption[int] {
return func() Option[int] {
return O.Of(c1.A * 10)
}
}
f1 := func(c2 Config2) Config1 { return Config1{A: c2.B + 1} }
f2 := func(c3 Config3) Config2 { return Config2{B: c3.C + 2} }
// Apply transformations separately
step1 := F.Pipe1(original, Contramap[int](f1))
step2 := F.Pipe1(step1, Contramap[int](f2))
// Apply composed transformation
composed := F.Pipe1(
original,
Contramap[int](F.Flow2(f2, f1)),
)
cfg := Config3{C: 5}
result1 := step2(cfg)()
result2 := composed(cfg)()
// Both should give the same result: (5+2+1)*10 = 80
expected := O.Of(80)
assert.Equal(t, expected, result1)
assert.Equal(t, expected, result2)
}
func TestPromap_RealWorldExample(t *testing.T) {
// Real-world example: adapting a database query function
type AppConfig struct {
DBHost string
DBPort int
DBUser string
DBPassword string
LogLevel string
}
type DBConnection struct {
ConnectionString string
}
type User struct {
ID int
Name string
}
type UserDTO struct {
UserID int
DisplayName string
}
// Original function that queries database
queryUser := func(conn DBConnection) IOOption[User] {
return func() Option[User] {
// Simulate database query
if conn.ConnectionString != "" {
return O.Of(User{ID: 1, Name: "Alice"})
}
return O.None[User]()
}
}
// Adapt to work with AppConfig and return UserDTO
adaptedQuery := F.Pipe1(
queryUser,
Promap(
// Extract DB connection from app config
func(cfg AppConfig) DBConnection {
return DBConnection{
ConnectionString: cfg.DBUser + "@" + cfg.DBHost,
}
},
// Convert User to UserDTO
func(u User) UserDTO {
return UserDTO{
UserID: u.ID,
DisplayName: "User: " + u.Name,
}
},
),
)
appCfg := AppConfig{
DBHost: "localhost",
DBPort: 5432,
DBUser: "admin",
DBPassword: "secret",
LogLevel: "info",
}
result := adaptedQuery(appCfg)()
expected := O.Of(UserDTO{UserID: 1, DisplayName: "User: Alice"})
assert.Equal(t, expected, result)
}
func TestContramap_RealWorldExample(t *testing.T) {
// Real-world example: adapting a service that needs specific config
type GlobalConfig struct {
ServiceURL string
APIKey string
Timeout int
RetryCount int
}
type ServiceConfig struct {
Endpoint string
Auth string
}
// Service function that needs ServiceConfig
callService := func(cfg ServiceConfig) IOOption[string] {
return func() Option[string] {
if cfg.Endpoint != "" && cfg.Auth != "" {
return O.Of("Response from " + cfg.Endpoint)
}
return O.None[string]()
}
}
// Adapt to work with GlobalConfig
adaptedService := F.Pipe1(
callService,
Contramap[string](func(g GlobalConfig) ServiceConfig {
return ServiceConfig{
Endpoint: g.ServiceURL,
Auth: "Bearer " + g.APIKey,
}
}),
)
globalCfg := GlobalConfig{
ServiceURL: "https://api.example.com",
APIKey: "secret-key",
Timeout: 30,
RetryCount: 3,
}
result := adaptedService(globalCfg)()
expected := O.Of("Response from https://api.example.com")
assert.Equal(t, expected, result)
}

434
v2/readeriooption/reader.go Normal file
View File

@@ -0,0 +1,434 @@
// 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 readeriooption
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/fromoption"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/optiont"
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
// FromOption lifts an Option[A] into a ReaderIOOption[R, A].
// The resulting computation ignores the environment and returns the given option.
//
//go:inline
func FromOption[R, A any](t Option[A]) ReaderIOOption[R, A] {
return readerio.Of[R](t)
}
// Some wraps a value in a ReaderIOOption, representing a successful computation.
// This is equivalent to Of but more explicit about the Option semantics.
//
//go:inline
func Some[R, A any](r A) ReaderIOOption[R, A] {
return optiont.Of(readerio.Of[R, Option[A]], r)
}
// FromReader lifts a Reader[R, A] into a ReaderIOOption[R, A].
// The resulting computation always succeeds (returns Some).
//
//go:inline
func FromReader[R, A any](r Reader[R, A]) ReaderIOOption[R, A] {
return SomeReader(r)
}
// SomeReader lifts a Reader[R, A] into a ReaderIOOption[R, A].
// The resulting computation always succeeds (returns Some).
//
//go:inline
func SomeReader[R, A any](r Reader[R, A]) ReaderIOOption[R, A] {
return function.Flow2(r, iooption.Some[A])
}
// MonadMap applies a function to the value inside a ReaderIOOption.
// If the ReaderIOOption contains None, the function is not applied.
//
// Example:
//
// ro := readeroption.Of[Config](42)
// doubled := readeroption.MonadMap(ro, N.Mul(2))
//
//go:inline
func MonadMap[R, A, B any](fa ReaderIOOption[R, A], f func(A) B) ReaderIOOption[R, B] {
return optiont.MonadMap(readerio.MonadMap[R, Option[A], Option[B]], fa, f)
}
// Map returns a function that applies a transformation to the value inside a ReaderIOOption.
// This is the curried version of MonadMap, useful for composition with F.Pipe.
//
// Example:
//
// doubled := F.Pipe1(
// readeroption.Of[Config](42),
// readeroption.Map[Config](N.Mul(2)),
// )
//
//go:inline
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
return optiont.Map(readerio.Map[R, Option[A], Option[B]], f)
}
// MonadChain sequences two ReaderIOOption computations, where the second depends on the result of the first.
// If the first computation returns None, the second is not executed.
//
// Example:
//
// findUser := func(id int) readeroption.ReaderIOOption[DB, User] { ... }
// loadProfile := func(user User) readeroption.ReaderIOOption[DB, Profile] { ... }
// result := readeroption.MonadChain(findUser(123), loadProfile)
//
//go:inline
func MonadChain[R, A, B any](ma ReaderIOOption[R, A], f Kleisli[R, A, B]) ReaderIOOption[R, B] {
return optiont.MonadChain(
readerio.MonadChain[R, Option[A], Option[B]],
readerio.Of[R, Option[B]],
ma,
f,
)
}
// Chain returns a function that sequences ReaderIOOption computations.
// This is the curried version of MonadChain, useful for composition with F.Pipe.
//
// Example:
//
// result := F.Pipe1(
// findUser(123),
// readeroption.Chain(loadProfile),
// )
//
//go:inline
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
return optiont.Chain(
readerio.Chain[R, Option[A], Option[B]],
readerio.Of[R, Option[B]],
f,
)
}
// Of wraps a value in a ReaderIOOption, representing a successful computation.
// The resulting computation ignores the environment and returns Some(a).
//
// Example:
//
// ro := readeroption.Of[Config](42)
// result := ro(config) // Returns option.Some(42)
//
//go:inline
func Of[R, A any](a A) ReaderIOOption[R, A] {
return Some[R](a)
}
// None creates a ReaderIOOption representing a failed computation.
// The resulting computation ignores the environment and returns None.
//
// Example:
//
// ro := readeroption.None[Config, int]()
// result := ro(config) // Returns option.None[int]()
//
//go:inline
func None[R, A any]() ReaderIOOption[R, A] {
return readerio.Of[R](O.None[A]())
}
// MonadAp applies a function wrapped in a ReaderIOOption to a value wrapped in a ReaderIOOption.
// Both computations are executed with the same environment.
// If either computation returns None, the result is None.
//
//go:inline
func MonadAp[R, A, B any](fab ReaderIOOption[R, func(A) B], fa ReaderIOOption[R, A]) ReaderIOOption[R, B] {
return optiont.MonadAp(
readerio.MonadAp[Option[B], R, Option[A]],
readerio.MonadMap[R, Option[func(A) B], func(Option[A]) Option[B]],
fab,
fa,
)
}
// Ap returns a function that applies a function wrapped in a ReaderIOOption to a value.
// This is the curried version of MonadAp.
//
//go:inline
func Ap[B, R, A any](fa ReaderIOOption[R, A]) Operator[R, func(A) B, B] {
return optiont.Ap(
readerio.Ap[Option[B], R, Option[A]],
readerio.Map[R, Option[func(A) B], func(Option[A]) Option[B]],
fa,
)
}
// FromPredicate creates a Kleisli arrow that filters a value based on a predicate.
// If the predicate returns true, the value is wrapped in Some; otherwise, None is returned.
//
// Example:
//
// isPositive := readeroption.FromPredicate[Config](N.MoreThan(0))
// result := F.Pipe1(
// readeroption.Of[Config](42),
// readeroption.Chain(isPositive),
// )
//
//go:inline
func FromPredicate[R, A any](pred Predicate[A]) Kleisli[R, A, A] {
return fromoption.FromPredicate(FromOption[R, A], pred)
}
// Fold extracts the value from a ReaderIOOption by providing handlers for both cases.
// The onNone handler is called if the computation returns None.
// The onRight handler is called if the computation returns Some(a).
//
// Example:
//
// result := readeroption.Fold(
// func() reader.Reader[Config, string] { return reader.Of[Config]("not found") },
// func(user User) reader.Reader[Config, string] { return reader.Of[Config](user.Name) },
// )(findUser(123))
//
//go:inline
func Fold[R, A, B any](onNone Reader[R, B], onRight reader.Kleisli[R, A, B]) reader.Operator[R, Option[A], B] {
return optiont.MatchE(reader.Chain[R, Option[A], B], lazy.Of(onNone), onRight)
}
// MonadFold extracts the value from a ReaderIOOption by providing handlers for both cases.
// This is the non-curried version of Fold.
// The onNone handler is called if the computation returns None.
// The onRight handler is called if the computation returns Some(a).
//
// Example:
//
// result := readeroption.MonadFold(
// findUser(123),
// reader.Of[Config]("not found"),
// func(user User) reader.Reader[Config, string] { return reader.Of[Config](user.Name) },
// )
//
//go:inline
func MonadFold[R, A, B any](fa ReaderIOOption[R, A], onNone ReaderIO[R, B], onRight readerio.Kleisli[R, A, B]) ReaderIO[R, B] {
return optiont.MonadMatchE(fa, readerio.MonadChain[R, Option[A], B], lazy.Of(onNone), onRight)
}
// GetOrElse returns the value from a ReaderIOOption, or a default value if it's None.
//
// Example:
//
// result := readeroption.GetOrElse(
// func() reader.Reader[Config, User] { return reader.Of[Config](defaultUser) },
// )(findUser(123))
//
//go:inline
func GetOrElse[R, A any](onNone Reader[R, A]) reader.Operator[R, Option[A], A] {
return optiont.GetOrElse(reader.Chain[R, Option[A], A], lazy.Of(onNone), reader.Of[R, A])
}
// Ask retrieves the current environment as a ReaderIOOption.
// This always succeeds and returns Some(environment).
//
// Example:
//
// getConfig := readeroption.Ask[Config]()
// result := getConfig(myConfig) // Returns option.Some(myConfig)
//
//go:inline
func Ask[R any]() ReaderIOOption[R, R] {
return fromreader.Ask(FromReader[R, R])()
}
// Asks creates a ReaderIOOption that applies a function to the environment.
// This always succeeds and returns Some(f(environment)).
//
// Example:
//
// getTimeout := readeroption.Asks(func(cfg Config) int { return cfg.Timeout })
// result := getTimeout(myConfig) // Returns option.Some(myConfig.Timeout)
//
//go:inline
func Asks[R, A any](r Reader[R, A]) ReaderIOOption[R, A] {
return fromreader.Asks(FromReader[R, A])(r)
}
// MonadChainOptionK chains a ReaderIOOption with a function that returns an Option.
// This is useful for integrating functions that return Option directly.
//
// Example:
//
// parseAge := func(s string) option.Option[int] { ... }
// result := readeroption.MonadChainOptionK(
// readeroption.Of[Config]("25"),
// parseAge,
// )
//
//go:inline
func MonadChainOptionK[R, A, B any](ma ReaderIOOption[R, A], f O.Kleisli[A, B]) ReaderIOOption[R, B] {
return fromoption.MonadChainOptionK(
MonadChain[R, A, B],
FromOption[R, B],
ma,
f,
)
}
// ChainOptionK returns a function that chains a ReaderIOOption with a function returning an Option.
// This is the curried version of MonadChainOptionK.
//
// Example:
//
// parseAge := func(s string) option.Option[int] { ... }
// result := F.Pipe1(
// readeroption.Of[Config]("25"),
// readeroption.ChainOptionK[Config](parseAge),
// )
//
//go:inline
func ChainOptionK[R, A, B any](f O.Kleisli[A, B]) Operator[R, A, B] {
return fromoption.ChainOptionK(
Chain[R, A, B],
FromOption[R, B],
f,
)
}
// Flatten removes one level of nesting from a ReaderIOOption.
// Converts ReaderIOOption[R, ReaderIOOption[R, A]] to ReaderIOOption[R, A].
//
// Example:
//
// nested := readeroption.Of[Config](readeroption.Of[Config](42))
// flattened := readeroption.Flatten(nested)
//
//go:inline
func Flatten[R, A any](mma ReaderIOOption[R, ReaderIOOption[R, A]]) ReaderIOOption[R, A] {
return MonadChain(mma, function.Identity[ReaderIOOption[R, A]])
}
// Local changes the value of the local context during the execution of the action `ma` (similar to `Contravariant`'s
// `contramap`).
//
// This allows you to transform the environment before passing it to a computation.
//
// Example:
//
// type GlobalConfig struct { DB DBConfig }
// type DBConfig struct { Host string }
//
// // A computation that needs DBConfig
// query := func(cfg DBConfig) option.Option[User] { ... }
//
// // Transform GlobalConfig to DBConfig
// result := readeroption.Local(func(g GlobalConfig) DBConfig { return g.DB })(
// readeroption.Asks(query),
// )
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderIOOption[R1, A]) ReaderIOOption[R2, A] {
return reader.Local[IOOption[A]](f)
}
// Read applies a context to a reader to obtain its value.
// This executes the ReaderIOOption computation with the given environment.
//
// Example:
//
// ro := readeroption.Of[Config](42)
// result := readeroption.Read[int](myConfig)(ro) // Returns option.Some(42)
//
//go:inline
func Read[A, R any](e R) func(ReaderIOOption[R, A]) IOOption[A] {
return reader.Read[IOOption[A]](e)
}
// ReadOption executes a ReaderIOOption with an optional environment.
// If the environment is None, the result is None.
// If the environment is Some(e), the ReaderIOOption is executed with e.
//
// This is useful when the environment itself might not be available.
//
// Example:
//
// ro := readeroption.Of[Config](42)
// result1 := readeroption.ReadOption[int](option.Some(myConfig))(ro) // Returns option.Some(42)
// result2 := readeroption.ReadOption[int](option.None[Config]())(ro) // Returns option.None[int]()
//
//go:inline
// TOGGLE
// func ReadOption[A, R any](e Option[R]) func(ReaderIOOption[R, A]) IOOption[A] {
// return function.Flow2(
// optiont.Chain,
// Read[A](e),
// )
// }
// MonadFlap applies a value to a function wrapped in a ReaderIOOption.
// This is the reverse of MonadAp.
//
//go:inline
func MonadFlap[R, A, B any](fab ReaderIOOption[R, func(A) B], a A) ReaderIOOption[R, B] {
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
}
// Flap returns a function that applies a value to a function wrapped in a ReaderIOOption.
// This 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)
}
// MonadAlt provides an alternative ReaderIOOption if the first one returns None.
// If fa returns Some(a), that value is returned; otherwise, the alternative computation is executed.
// This is useful for providing fallback behavior.
//
// Example:
//
// primary := findUserInCache(123)
// fallback := findUserInDB(123)
// result := readeroption.MonadAlt(primary, fallback)
//
//go:inline
func MonadAlt[R, A any](first ReaderIOOption[R, A], second Lazy[ReaderIOOption[R, A]]) ReaderIOOption[R, A] {
return optiont.MonadAlt(
readerio.Of[R, Option[A]],
readerio.MonadChain[R, Option[A], Option[A]],
first,
second,
)
}
// Alt returns a function that provides an alternative ReaderIOOption if the first one returns None.
// This is the curried version of MonadAlt, useful for composition with F.Pipe.
//
// Example:
//
// result := F.Pipe1(
// findUserInCache(123),
// readeroption.Alt(findUserInDB(123)),
// )
//
//go:inline
func Alt[R, A any](second Lazy[ReaderIOOption[R, A]]) Operator[R, A, A] {
return optiont.Alt(
readerio.Of[R, Option[A]],
readerio.Chain[R, Option[A], Option[A]],
second,
)
}

View File

@@ -0,0 +1,462 @@
// 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 readeriooption
import (
"context"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
RIO "github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
)
func TestOf(t *testing.T) {
ro := Of[context.Context](42)
result := ro(context.Background())()
assert.Equal(t, O.Of(42), result)
}
func TestSome(t *testing.T) {
ro := Some[context.Context](42)
result := ro(context.Background())()
assert.Equal(t, O.Of(42), result)
}
func TestNone(t *testing.T) {
ro := None[context.Context, int]()
result := ro(context.Background())()
assert.Equal(t, O.None[int](), result)
}
func TestFromOption_Some(t *testing.T) {
opt := O.Of(42)
ro := FromOption[context.Context](opt)
result := ro(context.Background())()
assert.Equal(t, O.Of(42), result)
}
func TestFromOption_None(t *testing.T) {
opt := O.None[int]()
ro := FromOption[context.Context](opt)
result := ro(context.Background())()
assert.Equal(t, O.None[int](), result)
}
func TestFromReader(t *testing.T) {
type Config struct {
Value int
}
r := func(cfg Config) int {
return cfg.Value * 2
}
ro := FromReader[Config](r)
cfg := Config{Value: 21}
result := ro(cfg)()
assert.Equal(t, O.Of(42), result)
}
func TestSomeReader(t *testing.T) {
type Config struct {
Value int
}
r := func(cfg Config) int {
return cfg.Value * 2
}
ro := SomeReader[Config](r)
cfg := Config{Value: 21}
result := ro(cfg)()
assert.Equal(t, O.Of(42), result)
}
func TestMonadMap_Some(t *testing.T) {
ro := Of[context.Context](21)
result := MonadMap(ro, func(x int) int { return x * 2 })
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestMonadMap_None(t *testing.T) {
ro := None[context.Context, int]()
result := MonadMap(ro, func(x int) int { return x * 2 })
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestMap_Some(t *testing.T) {
result := F.Pipe1(
Of[context.Context](21),
Map[context.Context](func(x int) int { return x * 2 }),
)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestMap_None(t *testing.T) {
result := F.Pipe1(
None[context.Context, int](),
Map[context.Context](func(x int) int { return x * 2 }),
)
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestMonadChain_BothSome(t *testing.T) {
ro1 := Of[context.Context](21)
ro2 := func(x int) ReaderIOOption[context.Context, int] {
return Of[context.Context](x * 2)
}
result := MonadChain(ro1, ro2)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestMonadChain_FirstNone(t *testing.T) {
ro1 := None[context.Context, int]()
ro2 := func(x int) ReaderIOOption[context.Context, int] {
return Of[context.Context](x * 2)
}
result := MonadChain(ro1, ro2)
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestMonadChain_SecondNone(t *testing.T) {
ro1 := Of[context.Context](21)
ro2 := func(x int) ReaderIOOption[context.Context, int] {
return None[context.Context, int]()
}
result := MonadChain(ro1, ro2)
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestChain(t *testing.T) {
result := F.Pipe1(
Of[context.Context](21),
Chain(func(x int) ReaderIOOption[context.Context, int] {
return Of[context.Context](x * 2)
}),
)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestMonadAp_BothSome(t *testing.T) {
fab := Of[context.Context](func(x int) int { return x * 2 })
fa := Of[context.Context](21)
result := MonadAp(fab, fa)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestMonadAp_FunctionNone(t *testing.T) {
fab := None[context.Context, func(int) int]()
fa := Of[context.Context](21)
result := MonadAp(fab, fa)
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestMonadAp_ValueNone(t *testing.T) {
fab := Of[context.Context](func(x int) int { return x * 2 })
fa := None[context.Context, int]()
result := MonadAp(fab, fa)
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestAp(t *testing.T) {
fa := Of[context.Context](21)
result := F.Pipe1(
Of[context.Context](func(x int) int { return x * 2 }),
Ap[int](fa),
)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestFromPredicate_True(t *testing.T) {
isPositive := FromPredicate[context.Context](func(x int) bool { return x > 0 })
result := F.Pipe1(
Of[context.Context](42),
Chain(isPositive),
)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestFromPredicate_False(t *testing.T) {
isPositive := FromPredicate[context.Context](func(x int) bool { return x > 0 })
result := F.Pipe1(
Of[context.Context](-42),
Chain(isPositive),
)
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestAsk(t *testing.T) {
type Config struct {
Value int
}
ro := Ask[Config]()
cfg := Config{Value: 42}
result := ro(cfg)()
assert.Equal(t, O.Of(cfg), result)
}
func TestAsks(t *testing.T) {
type Config struct {
Value int
}
ro := Asks(func(cfg Config) int {
return cfg.Value * 2
})
cfg := Config{Value: 21}
result := ro(cfg)()
assert.Equal(t, O.Of(42), result)
}
func TestMonadChainOptionK_Some(t *testing.T) {
parsePositive := func(x int) O.Option[int] {
if x > 0 {
return O.Of(x)
}
return O.None[int]()
}
result := MonadChainOptionK(
Of[context.Context](42),
parsePositive,
)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestMonadChainOptionK_None(t *testing.T) {
parsePositive := func(x int) O.Option[int] {
if x > 0 {
return O.Of(x)
}
return O.None[int]()
}
result := MonadChainOptionK(
Of[context.Context](-42),
parsePositive,
)
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestChainOptionK(t *testing.T) {
parsePositive := func(x int) O.Option[int] {
if x > 0 {
return O.Of(x)
}
return O.None[int]()
}
result := F.Pipe1(
Of[context.Context](42),
ChainOptionK[context.Context](parsePositive),
)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestFlatten(t *testing.T) {
nested := Of[context.Context](Of[context.Context](42))
result := Flatten(nested)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestLocal(t *testing.T) {
type GlobalConfig struct {
Factor int
}
type LocalConfig struct {
Multiplier int
}
// Computation that needs LocalConfig
computation := func(cfg LocalConfig) IOOption[int] {
return func() O.Option[int] {
return O.Of(10 * cfg.Multiplier)
}
}
// Adapt to work with GlobalConfig
adapted := Local[int](func(g GlobalConfig) LocalConfig {
return LocalConfig{Multiplier: g.Factor}
})(computation)
globalCfg := GlobalConfig{Factor: 5}
result := adapted(globalCfg)()
assert.Equal(t, O.Of(50), result)
}
func TestRead(t *testing.T) {
type Config struct {
Value int
}
ro := func(cfg Config) IOOption[int] {
return func() O.Option[int] {
return O.Of(cfg.Value * 2)
}
}
cfg := Config{Value: 21}
result := Read[int](cfg)(ro)()
assert.Equal(t, O.Of(42), result)
}
func TestMonadFlap(t *testing.T) {
fab := Of[context.Context](func(x int) int { return x * 2 })
result := MonadFlap(fab, 21)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestFlap(t *testing.T) {
result := F.Pipe1(
Of[context.Context](func(x int) int { return x * 2 }),
Flap[context.Context, int](21),
)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestMonadAlt_FirstSome(t *testing.T) {
first := Of[context.Context](42)
second := func() ReaderIOOption[context.Context, int] {
return Of[context.Context](100)
}
result := MonadAlt(first, second)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestMonadAlt_FirstNone(t *testing.T) {
first := None[context.Context, int]()
second := func() ReaderIOOption[context.Context, int] {
return Of[context.Context](100)
}
result := MonadAlt(first, second)
assert.Equal(t, O.Of(100), result(context.Background())())
}
func TestMonadAlt_BothNone(t *testing.T) {
first := None[context.Context, int]()
second := func() ReaderIOOption[context.Context, int] {
return None[context.Context, int]()
}
result := MonadAlt(first, second)
assert.Equal(t, O.None[int](), result(context.Background())())
}
func TestAlt(t *testing.T) {
result := F.Pipe1(
None[context.Context, int](),
Alt(func() ReaderIOOption[context.Context, int] {
return Of[context.Context](42)
}),
)
assert.Equal(t, O.Of(42), result(context.Background())())
}
func TestGetOrElse_Some(t *testing.T) {
ro := Of[context.Context](42)
result := MonadFold(ro, RIO.Of[context.Context](100), func(x int) RIO.ReaderIO[context.Context, int] {
return RIO.Of[context.Context](x)
})(context.Background())()
assert.Equal(t, 42, result)
}
func TestGetOrElse_None(t *testing.T) {
ro := None[context.Context, int]()
result := MonadFold(ro, RIO.Of[context.Context](100), func(x int) RIO.ReaderIO[context.Context, int] {
return RIO.Of[context.Context](x)
})(context.Background())()
assert.Equal(t, 100, result)
}
func TestMonadFold_Some(t *testing.T) {
ro := Of[context.Context](42)
result := MonadFold(
ro,
RIO.Of[context.Context]("none"),
func(x int) RIO.ReaderIO[context.Context, string] {
return RIO.Of[context.Context]("value: " + fmt.Sprintf("%d", x))
},
)(context.Background())()
assert.Equal(t, "value: 42", result)
}
func TestMonadFold_None(t *testing.T) {
ro := None[context.Context, int]()
result := MonadFold(
ro,
RIO.Of[context.Context]("none"),
func(x int) RIO.ReaderIO[context.Context, string] {
return RIO.Of[context.Context]("value: " + fmt.Sprintf("%d", x))
},
)(context.Background())()
assert.Equal(t, "none", result)
}
func TestComplexChain(t *testing.T) {
// Test a complex chain of operations
type Config struct {
Factor int
}
result := F.Pipe3(
Of[Config](10),
Map[Config](func(x int) int { return x * 2 }), // 20
Chain(func(x int) ReaderIOOption[Config, int] {
return Asks(func(cfg Config) int {
return x * cfg.Factor
})
}),
Chain(func(x int) ReaderIOOption[Config, int] {
if x > 50 {
return Of[Config](x)
}
return None[Config, int]()
}),
)
cfg := Config{Factor: 5}
assert.Equal(t, O.Of(100), result(cfg)())
cfg2 := Config{Factor: 2}
assert.Equal(t, O.None[int](), result(cfg2)())
}

44
v2/readeriooption/rec.go Normal file
View File

@@ -0,0 +1,44 @@
// 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 readeriooption
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tailrec"
)
//go:inline
func TailRec[R, A, B any](f Kleisli[R, A, tailrec.Trampoline[A, B]]) Kleisli[R, A, B] {
return func(a A) ReaderIOOption[R, B] {
initialReader := f(a)
return func(r R) IOOption[B] {
initialB := initialReader(r)
return func() Option[B] {
current := initialB()
for {
rec, ok := option.Unwrap(current)
if !ok {
return option.None[B]()
}
if rec.Landed {
return option.Of(rec.Land)
}
current = f(rec.Bounce)(r)()
}
}
}
}
}

View File

@@ -0,0 +1,133 @@
// 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 readeriooption
import (
"github.com/IBM/fp-go/v2/internal/apply"
T "github.com/IBM/fp-go/v2/tuple"
)
// SequenceT functions convert multiple ReaderIOOption values into a single ReaderIOOption containing a tuple.
// If any input is None, the entire result is None.
// Otherwise, returns Some containing a tuple of all the unwrapped values.
//
// These functions are useful for combining multiple independent ReaderIOOption computations
// where you need to preserve the individual types of each result.
// SequenceT1 converts a single ReaderIOOption into a ReaderIOOption of a 1-tuple.
// This is mainly useful for consistency with the other SequenceT functions.
//
// Example:
//
// type Config struct { ... }
//
// user := readeroption.Of[Config](User{Name: "Alice"})
// result := readeroption.SequenceT1(user)
// // result(config) returns option.Some(tuple.MakeTuple1(User{Name: "Alice"}))
func SequenceT1[R, A any](a ReaderIOOption[R, A]) ReaderIOOption[R, T.Tuple1[A]] {
return apply.SequenceT1(
Map,
a,
)
}
// SequenceT2 combines two ReaderIOOption values into a ReaderIOOption of a 2-tuple.
// If either input is None, the result is None.
//
// Example:
//
// type Config struct { ... }
//
// user := readeroption.Of[Config](User{Name: "Alice"})
// count := readeroption.Of[Config](42)
//
// result := readeroption.SequenceT2(user, count)
// // result(config) returns option.Some(tuple.MakeTuple2(User{Name: "Alice"}, 42))
//
// noneUser := readeroption.None[Config, User]()
// result2 := readeroption.SequenceT2(noneUser, count)
// // result2(config) returns option.None[tuple.Tuple2[User, int]]()
func SequenceT2[R, A, B any](
a ReaderIOOption[R, A],
b ReaderIOOption[R, B],
) ReaderIOOption[R, T.Tuple2[A, B]] {
return apply.SequenceT2(
Map,
Ap,
a,
b,
)
}
// SequenceT3 combines three ReaderIOOption values into a ReaderIOOption of a 3-tuple.
// If any input is None, the result is None.
//
// Example:
//
// type Config struct { ... }
//
// user := readeroption.Of[Config](User{Name: "Alice"})
// count := readeroption.Of[Config](42)
// active := readeroption.Of[Config](true)
//
// result := readeroption.SequenceT3(user, count, active)
// // result(config) returns option.Some(tuple.MakeTuple3(User{Name: "Alice"}, 42, true))
func SequenceT3[R, A, B, C any](
a ReaderIOOption[R, A],
b ReaderIOOption[R, B],
c ReaderIOOption[R, C],
) ReaderIOOption[R, T.Tuple3[A, B, C]] {
return apply.SequenceT3(
Map,
Ap,
Ap,
a,
b,
c,
)
}
// SequenceT4 combines four ReaderIOOption values into a ReaderIOOption of a 4-tuple.
// If any input is None, the result is None.
//
// Example:
//
// type Config struct { ... }
//
// user := readeroption.Of[Config](User{Name: "Alice"})
// count := readeroption.Of[Config](42)
// active := readeroption.Of[Config](true)
// score := readeroption.Of[Config](95.5)
//
// result := readeroption.SequenceT4(user, count, active, score)
// // result(config) returns option.Some(tuple.MakeTuple4(User{Name: "Alice"}, 42, true, 95.5))
func SequenceT4[R, A, B, C, D any](
a ReaderIOOption[R, A],
b ReaderIOOption[R, B],
c ReaderIOOption[R, C],
d ReaderIOOption[R, D],
) ReaderIOOption[R, T.Tuple4[A, B, C, D]] {
return apply.SequenceT4(
Map,
Ap,
Ap,
Ap,
a,
b,
c,
d,
)
}

View File

@@ -0,0 +1,91 @@
// 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 readeriooption
import (
"testing"
O "github.com/IBM/fp-go/v2/option"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
type MyContext string
const defaultContext MyContext = "default"
func TestSequenceT1(t *testing.T) {
t1 := Of[MyContext]("s1")
e1 := None[MyContext, string]()
res1 := SequenceT1(t1)
assert.Equal(t, O.Of(T.MakeTuple1("s1")), res1(defaultContext)())
res2 := SequenceT1(e1)
assert.Equal(t, O.None[T.Tuple1[string]](), res2(defaultContext)())
}
func TestSequenceT2(t *testing.T) {
t1 := Of[MyContext]("s1")
e1 := None[MyContext, string]()
t2 := Of[MyContext](2)
e2 := None[MyContext, int]()
res1 := SequenceT2(t1, t2)
assert.Equal(t, O.Of(T.MakeTuple2("s1", 2)), res1(defaultContext)())
res2 := SequenceT2(e1, t2)
assert.Equal(t, O.None[T.Tuple2[string, int]](), res2(defaultContext)())
res3 := SequenceT2(t1, e2)
assert.Equal(t, O.None[T.Tuple2[string, int]](), res3(defaultContext)())
}
func TestSequenceT3(t *testing.T) {
t1 := Of[MyContext]("s1")
e1 := None[MyContext, string]()
t2 := Of[MyContext](2)
e2 := None[MyContext, int]()
t3 := Of[MyContext](true)
e3 := None[MyContext, bool]()
res1 := SequenceT3(t1, t2, t3)
assert.Equal(t, O.Of(T.MakeTuple3("s1", 2, true)), res1(defaultContext)())
res2 := SequenceT3(e1, t2, t3)
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), res2(defaultContext)())
res3 := SequenceT3(t1, e2, t3)
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), res3(defaultContext)())
res4 := SequenceT3(t1, t2, e3)
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), 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, O.Of(T.MakeTuple4("s1", 2, true, 1.0)), res(defaultContext)())
}

128
v2/readeriooption/types.go Normal file
View File

@@ -0,0 +1,128 @@
// 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 readeriooption provides a monad transformer that combines the Reader and Option monads.
//
// # Fantasy Land Specification
//
// This is a monad transformer combining:
// - Reader monad: https://github.com/fantasyland/fantasy-land
// - Maybe (Option) monad: https://github.com/fantasyland/fantasy-land#maybe
//
// Implemented Fantasy Land algebras:
// - Functor: https://github.com/fantasyland/fantasy-land#functor
// - Apply: https://github.com/fantasyland/fantasy-land#apply
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
// - Chain: https://github.com/fantasyland/fantasy-land#chain
// - Monad: https://github.com/fantasyland/fantasy-land#monad
// - Alt: https://github.com/fantasyland/fantasy-land#alt
//
// ReaderIOOption[R, A] represents a computation that:
// - Depends on a shared environment of type R (Reader monad)
// - May fail to produce a value of type A (Option monad)
//
// This is useful for computations that need access to configuration, context, or dependencies
// while also being able to represent the absence of a value without using errors.
//
// The ReaderIOOption monad is defined as: Reader[R, Option[A]]
//
// Key operations:
// - Of: Wraps a value in a ReaderIOOption
// - None: Creates a ReaderIOOption representing no value
// - Map: Transforms the value inside a ReaderIOOption
// - Chain: Sequences ReaderIOOption computations
// - Ask/Asks: Accesses the environment
//
// Example:
//
// type Config struct {
// DatabaseURL string
// Timeout int
// }
//
// // A computation that may or may not find a user
// func findUser(id int) readeriooption.ReaderIOOption[Config, User] {
// return readeriooption.Asks(func(cfg Config) option.Option[User] {
// // Use cfg.DatabaseURL to query database
// // Return Some(user) if found, None() if not found
// })
// }
//
// // Chain multiple operations
// result := F.Pipe2(
// findUser(123),
// readeriooption.Chain(func(user User) readeriooption.ReaderIOOption[Config, Profile] {
// return loadProfile(user.ProfileID)
// }),
// readeriooption.Map(func(profile Profile) string {
// return profile.DisplayName
// }),
// )
//
// // Execute with config
// config := Config{DatabaseURL: "localhost:5432", Timeout: 30}
// displayName := result(config) // Returns Option[string]
package readeriooption
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
type (
// Lazy represents a deferred computation that produces a value of type A.
Lazy[A any] = lazy.Lazy[A]
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
// IOOption represents an IO computation that may produce a value of type A.
// It combines IO effects with the Option monad for optional values.
IOOption[A any] = iooption.IOOption[A]
// Either represents a value of one of two possible types (a disjoint union).
// An instance of Either is either Left (representing an error) or Right (representing a success).
Either[E, A any] = either.Either[E, A]
// Reader represents a computation that depends on an environment R and produces a value A.
Reader[R, A any] = reader.Reader[R, A]
// ReaderIO represents a computation that depends on an environment R and performs IO to produce a value A.
// It combines the Reader monad (for dependency injection) with IO effects.
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// ReaderIOOption represents a computation that depends on an environment R and may produce a value A.
// It combines the Reader monad (for dependency injection) with IO effects and the Option monad (for optional values).
// This is the main type of this package, defined as Reader[R, IOOption[A]].
ReaderIOOption[R, A any] = Reader[R, IOOption[A]]
// Kleisli represents a function that takes a value A and returns a ReaderIOOption[R, B].
// This is the type of functions used with Chain/Bind operations, enabling monadic composition.
Kleisli[R, A, B any] = Reader[A, ReaderIOOption[R, B]]
// Operator represents a function that transforms one ReaderIOOption into another.
// It takes a ReaderIOOption[R, A] and produces a ReaderIOOption[R, B].
// This is commonly used for lifting functions into the ReaderIOOption context.
Operator[R, A, B any] = Reader[ReaderIOOption[R, A], ReaderIOOption[R, B]]
)

File diff suppressed because it is too large Load Diff