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

Compare commits

...

1 Commits

Author SHA1 Message Date
Dr. Carsten Leue
168a6e1072 fix: add Eitherize to Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 12:55:03 +01:00
5 changed files with 1435 additions and 2 deletions

View File

@@ -0,0 +1,213 @@
// 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 readerreaderioresult
import (
"context"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioresult"
)
// Eitherize converts a function that returns a value and error into a ReaderReaderIOResult.
//
// This function takes a function that accepts an outer context R and context.Context,
// returning a value T and an error, and converts it into a ReaderReaderIOResult[R, T].
// The error is automatically converted into the Left case of the Result, while successful
// values become the Right case.
//
// This is particularly useful for integrating standard Go error-handling patterns into
// the functional programming style of ReaderReaderIOResult. It is especially helpful
// for adapting interface member functions that accept a context. When you have an
// interface method with signature (receiver, context.Context) (T, error), you can
// use Eitherize to convert it into a ReaderReaderIOResult where the receiver becomes
// the outer reader context R.
//
// # Type Parameters
//
// - R: The outer reader context type (e.g., application configuration)
// - T: The success value type
//
// # Parameters
//
// - f: A function that takes R and context.Context and returns (T, error)
//
// # Returns
//
// - ReaderReaderIOResult[R, T]: A computation that depends on R and context.Context,
// performs IO, and produces a Result[T]
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // A function using standard Go error handling
// func fetchUser(cfg AppConfig, ctx context.Context) (*User, error) {
// // Implementation that may return an error
// return &User{ID: 1, Name: "Alice"}, nil
// }
//
// // Convert to ReaderReaderIOResult
// fetchUserRR := Eitherize(fetchUser)
//
// // Use in functional composition
// result := F.Pipe1(
// fetchUserRR,
// Map[AppConfig](func(u *User) string { return u.Name }),
// )
//
// // Execute with config and context
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
// outcome := result(cfg)(context.Background())()
//
// # Adapting Interface Methods
//
// Eitherize is particularly useful for adapting interface member functions:
//
// type UserRepository interface {
// GetUser(ctx context.Context, id int) (*User, error)
// }
//
// type UserRepo struct {
// db *sql.DB
// }
//
// func (r *UserRepo) GetUser(ctx context.Context, id int) (*User, error) {
// // Implementation
// return &User{ID: id}, nil
// }
//
// // Adapt the method by binding the first parameter (receiver)
// repo := &UserRepo{db: db}
// getUserRR := Eitherize(func(id int, ctx context.Context) (*User, error) {
// return repo.GetUser(ctx, id)
// })
//
// // Now getUserRR has type: ReaderReaderIOResult[int, *User]
// // The receiver (repo) is captured in the closure
// // The id becomes the outer reader context R
//
// # See Also
//
// - Eitherize1: For functions that take an additional parameter
// - ioresult.Eitherize2: The underlying conversion function
func Eitherize[R, T any](f func(R, context.Context) (T, error)) ReaderReaderIOResult[R, T] {
return F.Pipe1(
ioresult.Eitherize2(f),
F.Curry2,
)
}
// Eitherize1 converts a function that takes an additional parameter and returns a value
// and error into a Kleisli arrow.
//
// This function takes a function that accepts an outer context R, context.Context, and
// an additional parameter A, returning a value T and an error, and converts it into a
// Kleisli arrow (A -> ReaderReaderIOResult[R, T]). The error is automatically converted
// into the Left case of the Result, while successful values become the Right case.
//
// This is useful for creating composable operations that depend on both contexts and
// an input value, following standard Go error-handling patterns. It is especially helpful
// for adapting interface member functions that accept a context and additional parameters.
// When you have an interface method with signature (receiver, context.Context, A) (T, error),
// you can use Eitherize1 to convert it into a Kleisli arrow where the receiver becomes
// the outer reader context R and A becomes the input parameter.
//
// # Type Parameters
//
// - R: The outer reader context type (e.g., application configuration)
// - A: The input parameter type
// - T: The success value type
//
// # Parameters
//
// - f: A function that takes R, context.Context, and A, returning (T, error)
//
// # Returns
//
// - Kleisli[R, A, T]: A function from A to ReaderReaderIOResult[R, T]
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // A function using standard Go error handling
// func fetchUserByID(cfg AppConfig, ctx context.Context, id int) (*User, error) {
// // Implementation that may return an error
// return &User{ID: id, Name: "Alice"}, nil
// }
//
// // Convert to Kleisli arrow
// fetchUserKleisli := Eitherize1(fetchUserByID)
//
// // Use in functional composition with Chain
// pipeline := F.Pipe1(
// Of[AppConfig](123),
// Chain[AppConfig](fetchUserKleisli),
// )
//
// // Execute with config and context
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
// outcome := pipeline(cfg)(context.Background())()
//
// # Adapting Interface Methods
//
// Eitherize1 is particularly useful for adapting interface member functions with parameters:
//
// type UserRepository interface {
// GetUserByID(ctx context.Context, id int) (*User, error)
// UpdateUser(ctx context.Context, user *User) error
// }
//
// type UserRepo struct {
// db *sql.DB
// }
//
// func (r *UserRepo) GetUserByID(ctx context.Context, id int) (*User, error) {
// // Implementation
// return &User{ID: id}, nil
// }
//
// // Adapt the method - receiver becomes R, id becomes A
// repo := &UserRepo{db: db}
// getUserKleisli := Eitherize1(func(r *UserRepo, ctx context.Context, id int) (*User, error) {
// return r.GetUserByID(ctx, id)
// })
//
// // Now getUserKleisli has type: Kleisli[*UserRepo, int, *User]
// // Which is: func(int) ReaderReaderIOResult[*UserRepo, *User]
// // Use it in composition:
// pipeline := F.Pipe1(
// Of[*UserRepo](123),
// Chain[*UserRepo](getUserKleisli),
// )
// result := pipeline(repo)(context.Background())()
//
// # See Also
//
// - Eitherize: For functions without an additional parameter
// - Chain: For composing Kleisli arrows
// - ioresult.Eitherize3: The underlying conversion function
func Eitherize1[R, A, T any](f func(R, context.Context, A) (T, error)) Kleisli[R, A, T] {
return F.Flow2(
F.Bind3of3(ioresult.Eitherize3(f)),
F.Curry2,
)
}

View File

@@ -0,0 +1,507 @@
// 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 readerreaderioresult
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type TestConfig struct {
Prefix string
MaxLen int
}
var testConfig = TestConfig{
Prefix: "test",
MaxLen: 100,
}
// TestEitherize_Success tests successful conversion with Eitherize
func TestEitherize_Success(t *testing.T) {
t.Run("converts successful function to ReaderReaderIOResult", func(t *testing.T) {
// Arrange
successFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return cfg.Prefix + "-success", nil
}
rr := Eitherize(successFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("test-success"), outcome)
})
t.Run("preserves context values", func(t *testing.T) {
// Arrange
type ctxKey string
key := ctxKey("testKey")
expectedValue := "contextValue"
contextFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
value := ctx.Value(key)
if value == nil {
return "", errors.New("context value not found")
}
return value.(string), nil
}
rr := Eitherize(contextFunc)
ctx := context.WithValue(context.Background(), key, expectedValue)
// Act
outcome := rr(testConfig)(ctx)()
// Assert
assert.Equal(t, result.Of(expectedValue), outcome)
})
t.Run("works with different types", func(t *testing.T) {
// Arrange
intFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return cfg.MaxLen, nil
}
rr := Eitherize(intFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(100), outcome)
})
}
// TestEitherize_Failure tests error handling with Eitherize
func TestEitherize_Failure(t *testing.T) {
t.Run("converts error to Left", func(t *testing.T) {
// Arrange
expectedErr := errors.New("operation failed")
failFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return "", expectedErr
}
rr := Eitherize(failFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
assert.Equal(t, result.Left[string](expectedErr), outcome)
})
t.Run("preserves error message", func(t *testing.T) {
// Arrange
expectedErr := fmt.Errorf("validation error: field is required")
failFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return 0, expectedErr
}
rr := Eitherize(failFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
leftValue := result.MonadFold(outcome,
F.Identity[error],
func(int) error { return nil },
)
assert.Equal(t, expectedErr, leftValue)
})
}
// TestEitherize_EdgeCases tests edge cases for Eitherize
func TestEitherize_EdgeCases(t *testing.T) {
t.Run("handles nil context", func(t *testing.T) {
// Arrange
nilCtxFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
if ctx == nil {
return "nil-context", nil
}
return "non-nil-context", nil
}
rr := Eitherize(nilCtxFunc)
// Act
outcome := rr(testConfig)(nil)()
// Assert
assert.Equal(t, result.Of("nil-context"), outcome)
})
t.Run("handles zero value config", func(t *testing.T) {
// Arrange
zeroFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return cfg.Prefix, nil
}
rr := Eitherize(zeroFunc)
// Act
outcome := rr(TestConfig{})(context.Background())()
// Assert
assert.Equal(t, result.Of(""), outcome)
})
t.Run("handles pointer types", func(t *testing.T) {
// Arrange
type User struct {
Name string
}
ptrFunc := func(cfg TestConfig, ctx context.Context) (*User, error) {
return &User{Name: "Alice"}, nil
}
rr := Eitherize(ptrFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsRight(outcome))
user := result.MonadFold(outcome,
func(error) *User { return nil },
F.Identity[*User],
)
assert.NotNil(t, user)
assert.Equal(t, "Alice", user.Name)
})
}
// TestEitherize_Integration tests integration with other operations
func TestEitherize_Integration(t *testing.T) {
t.Run("composes with Map", func(t *testing.T) {
// Arrange
baseFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return 42, nil
}
rr := Eitherize(baseFunc)
// Act
pipeline := F.Pipe1(
rr,
Map[TestConfig](func(n int) string { return strconv.Itoa(n) }),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("composes with Chain", func(t *testing.T) {
// Arrange
firstFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return 10, nil
}
secondFunc := func(n int) ReaderReaderIOResult[TestConfig, string] {
return Of[TestConfig](fmt.Sprintf("value: %d", n))
}
// Act
pipeline := F.Pipe1(
Eitherize(firstFunc),
Chain[TestConfig](secondFunc),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("value: 10"), outcome)
})
}
// TestEitherize1_Success tests successful conversion with Eitherize1
func TestEitherize1_Success(t *testing.T) {
t.Run("converts successful function to Kleisli", func(t *testing.T) {
// Arrange
addFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
return n + cfg.MaxLen, nil
}
kleisli := Eitherize1(addFunc)
// Act
outcome := kleisli(10)(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(110), outcome)
})
t.Run("works with string input", func(t *testing.T) {
// Arrange
concatFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
return cfg.Prefix + "-" + s, nil
}
kleisli := Eitherize1(concatFunc)
// Act
outcome := kleisli("input")(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("test-input"), outcome)
})
t.Run("preserves context in Kleisli", func(t *testing.T) {
// Arrange
type ctxKey string
key := ctxKey("multiplier")
multiplyFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
multiplier := ctx.Value(key)
if multiplier == nil {
return n, nil
}
return n * multiplier.(int), nil
}
kleisli := Eitherize1(multiplyFunc)
ctx := context.WithValue(context.Background(), key, 3)
// Act
outcome := kleisli(5)(testConfig)(ctx)()
// Assert
assert.Equal(t, result.Of(15), outcome)
})
}
// TestEitherize1_Failure tests error handling with Eitherize1
func TestEitherize1_Failure(t *testing.T) {
t.Run("converts error to Left in Kleisli", func(t *testing.T) {
// Arrange
expectedErr := errors.New("division by zero")
divideFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
if n == 0 {
return 0, expectedErr
}
return 100 / n, nil
}
kleisli := Eitherize1(divideFunc)
// Act
outcome := kleisli(0)(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
assert.Equal(t, result.Left[int](expectedErr), outcome)
})
t.Run("preserves error context", func(t *testing.T) {
// Arrange
validateFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
if len(s) > cfg.MaxLen {
return "", fmt.Errorf("string too long: %d > %d", len(s), cfg.MaxLen)
}
return s, nil
}
kleisli := Eitherize1(validateFunc)
longString := string(make([]byte, 200))
// Act
outcome := kleisli(longString)(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
leftValue := result.MonadFold(outcome,
F.Identity[error],
func(string) error { return nil },
)
assert.Contains(t, leftValue.Error(), "string too long")
})
}
// TestEitherize1_EdgeCases tests edge cases for Eitherize1
func TestEitherize1_EdgeCases(t *testing.T) {
t.Run("handles zero value input", func(t *testing.T) {
// Arrange
zeroFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
return n, nil
}
kleisli := Eitherize1(zeroFunc)
// Act
outcome := kleisli(0)(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(0), outcome)
})
t.Run("handles pointer input", func(t *testing.T) {
// Arrange
type Input struct {
Value int
}
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
if in == nil {
return 0, errors.New("nil input")
}
return in.Value, nil
}
kleisli := Eitherize1(ptrFunc)
// Act
outcome := kleisli(&Input{Value: 42})(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(42), outcome)
})
t.Run("handles nil pointer input", func(t *testing.T) {
// Arrange
type Input struct {
Value int
}
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
if in == nil {
return 0, errors.New("nil input")
}
return in.Value, nil
}
kleisli := Eitherize1(ptrFunc)
// Act
outcome := kleisli((*Input)(nil))(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
})
}
// TestEitherize1_Integration tests integration with other operations
func TestEitherize1_Integration(t *testing.T) {
t.Run("composes with Chain", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
doubleFunc := func(n int) ReaderReaderIOResult[TestConfig, int] {
return Of[TestConfig](n * 2)
}
parseKleisli := Eitherize1(parseFunc)
// Act
pipeline := F.Pipe2(
Of[TestConfig]("42"),
Chain[TestConfig](parseKleisli),
Chain[TestConfig](doubleFunc),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(84), outcome)
})
t.Run("handles error in chain", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
parseKleisli := Eitherize1(parseFunc)
// Act
pipeline := F.Pipe1(
Of[TestConfig]("not-a-number"),
Chain[TestConfig](parseKleisli),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
})
t.Run("composes multiple Kleisli arrows", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
formatFunc := func(cfg TestConfig, ctx context.Context, n int) (string, error) {
return fmt.Sprintf("%s-%d", cfg.Prefix, n), nil
}
parseKleisli := Eitherize1(parseFunc)
formatKleisli := Eitherize1(formatFunc)
// Act
pipeline := F.Pipe2(
Of[TestConfig]("123"),
Chain[TestConfig](parseKleisli),
Chain[TestConfig](formatKleisli),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("test-123"), outcome)
})
}
// TestEitherize_TypeSafety tests type safety across different scenarios
func TestEitherize_TypeSafety(t *testing.T) {
t.Run("Eitherize with complex types", func(t *testing.T) {
// Arrange
type ComplexResult struct {
Data map[string]int
Count int
}
complexFunc := func(cfg TestConfig, ctx context.Context) (ComplexResult, error) {
return ComplexResult{
Data: map[string]int{"key": 42},
Count: 1,
}, nil
}
rr := Eitherize(complexFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsRight(outcome))
value := result.MonadFold(outcome,
func(error) ComplexResult { return ComplexResult{} },
F.Identity[ComplexResult],
)
assert.Equal(t, 42, value.Data["key"])
assert.Equal(t, 1, value.Count)
})
t.Run("Eitherize1 with different input and output types", func(t *testing.T) {
// Arrange
type Input struct {
ID int
}
type Output struct {
Name string
}
convertFunc := func(cfg TestConfig, ctx context.Context, in Input) (Output, error) {
return Output{Name: fmt.Sprintf("%s-%d", cfg.Prefix, in.ID)}, nil
}
kleisli := Eitherize1(convertFunc)
// Act
outcome := kleisli(Input{ID: 99})(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(Output{Name: "test-99"}), outcome)
})
}

208
v2/effect/eitherize.go Normal file
View File

@@ -0,0 +1,208 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"context"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
)
// Eitherize converts a function that returns a value and error into an Effect.
//
// This function takes a function that accepts a context C and context.Context,
// returning a value T and an error, and converts it into an Effect[C, T].
// The error is automatically converted into a failure, while successful
// values become successes.
//
// This is particularly useful for integrating standard Go error-handling patterns into
// the effect system. It is especially helpful for adapting interface member functions
// that accept a context. When you have an interface method with signature
// (receiver, context.Context) (T, error), you can use Eitherize to convert it into
// an Effect where the receiver becomes the context C.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - T: The success value type
//
// # Parameters
//
// - f: A function that takes C and context.Context and returns (T, error)
//
// # Returns
//
// - Effect[C, T]: An effect that depends on C, performs IO, and produces T
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // A function using standard Go error handling
// func fetchUser(cfg AppConfig, ctx context.Context) (*User, error) {
// // Implementation that may return an error
// return &User{ID: 1, Name: "Alice"}, nil
// }
//
// // Convert to Effect
// fetchUserEffect := effect.Eitherize(fetchUser)
//
// // Use in functional composition
// pipeline := F.Pipe1(
// fetchUserEffect,
// effect.Map[AppConfig](func(u *User) string { return u.Name }),
// )
//
// // Execute with config
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
// result, err := effect.RunSync(effect.Provide[*User](cfg)(pipeline))(context.Background())
//
// # Adapting Interface Methods
//
// Eitherize is particularly useful for adapting interface member functions:
//
// type UserRepository interface {
// GetUser(ctx context.Context, id int) (*User, error)
// }
//
// type UserRepo struct {
// db *sql.DB
// }
//
// func (r *UserRepo) GetUser(ctx context.Context, id int) (*User, error) {
// // Implementation
// return &User{ID: id}, nil
// }
//
// // Adapt the method by binding the first parameter (receiver)
// repo := &UserRepo{db: db}
// getUserEffect := effect.Eitherize(func(id int, ctx context.Context) (*User, error) {
// return repo.GetUser(ctx, id)
// })
//
// // Now getUserEffect has type: Effect[int, *User]
// // The receiver (repo) is captured in the closure
// // The id becomes the context C
//
// # See Also
//
// - Eitherize1: For functions that take an additional parameter
// - readerreaderioresult.Eitherize: The underlying implementation
//
//go:inline
func Eitherize[C, T any](f func(C, context.Context) (T, error)) Effect[C, T] {
return readerreaderioresult.Eitherize(f)
}
// Eitherize1 converts a function that takes an additional parameter and returns a value
// and error into a Kleisli arrow.
//
// This function takes a function that accepts a context C, context.Context, and
// an additional parameter A, returning a value T and an error, and converts it into a
// Kleisli arrow (A -> Effect[C, T]). The error is automatically converted into a failure,
// while successful values become successes.
//
// This is useful for creating composable operations that depend on context and
// an input value, following standard Go error-handling patterns. It is especially helpful
// for adapting interface member functions that accept a context and additional parameters.
// When you have an interface method with signature (receiver, context.Context, A) (T, error),
// you can use Eitherize1 to convert it into a Kleisli arrow where the receiver becomes
// the context C and A becomes the input parameter.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input parameter type
// - T: The success value type
//
// # Parameters
//
// - f: A function that takes C, context.Context, and A, returning (T, error)
//
// # Returns
//
// - Kleisli[C, A, T]: A function from A to Effect[C, T]
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // A function using standard Go error handling
// func fetchUserByID(cfg AppConfig, ctx context.Context, id int) (*User, error) {
// // Implementation that may return an error
// return &User{ID: id, Name: "Alice"}, nil
// }
//
// // Convert to Kleisli arrow
// fetchUserKleisli := effect.Eitherize1(fetchUserByID)
//
// // Use in functional composition with Chain
// pipeline := F.Pipe1(
// effect.Succeed[AppConfig](123),
// effect.Chain[AppConfig](fetchUserKleisli),
// )
//
// // Execute with config
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
// result, err := effect.RunSync(effect.Provide[*User](cfg)(pipeline))(context.Background())
//
// # Adapting Interface Methods
//
// Eitherize1 is particularly useful for adapting interface member functions with parameters:
//
// type UserRepository interface {
// GetUserByID(ctx context.Context, id int) (*User, error)
// UpdateUser(ctx context.Context, user *User) error
// }
//
// type UserRepo struct {
// db *sql.DB
// }
//
// func (r *UserRepo) GetUserByID(ctx context.Context, id int) (*User, error) {
// // Implementation
// return &User{ID: id}, nil
// }
//
// // Adapt the method - receiver becomes C, id becomes A
// repo := &UserRepo{db: db}
// getUserKleisli := effect.Eitherize1(func(r *UserRepo, ctx context.Context, id int) (*User, error) {
// return r.GetUserByID(ctx, id)
// })
//
// // Now getUserKleisli has type: Kleisli[*UserRepo, int, *User]
// // Which is: func(int) Effect[*UserRepo, *User]
// // Use it in composition:
// pipeline := F.Pipe1(
// effect.Succeed[*UserRepo](123),
// effect.Chain[*UserRepo](getUserKleisli),
// )
// result, err := effect.RunSync(effect.Provide[*User](repo)(pipeline))(context.Background())
//
// # See Also
//
// - Eitherize: For functions without an additional parameter
// - Chain: For composing Kleisli arrows
// - readerreaderioresult.Eitherize1: The underlying implementation
//
//go:inline
func Eitherize1[C, A, T any](f func(C, context.Context, A) (T, error)) Kleisli[C, A, T] {
return readerreaderioresult.Eitherize1(f)
}

507
v2/effect/eitherize_test.go Normal file
View File

@@ -0,0 +1,507 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestEitherize_Success tests successful conversion with Eitherize
func TestEitherize_Success(t *testing.T) {
t.Run("converts successful function to Effect", func(t *testing.T) {
// Arrange
successFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return cfg.Prefix + "-success", nil
}
eff := Eitherize(successFunc)
// Act
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "LOG-success", result)
})
t.Run("preserves context values", func(t *testing.T) {
// Arrange
type ctxKey string
key := ctxKey("testKey")
expectedValue := "contextValue"
contextFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
value := ctx.Value(key)
if value == nil {
return "", errors.New("context value not found")
}
return value.(string), nil
}
eff := Eitherize(contextFunc)
// Act
ioResult := Provide[string](testConfig)(eff)
readerResult := RunSync(ioResult)
ctx := context.WithValue(context.Background(), key, expectedValue)
result, err := readerResult(ctx)
// Assert
assert.NoError(t, err)
assert.Equal(t, expectedValue, result)
})
t.Run("works with different types", func(t *testing.T) {
// Arrange
intFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return cfg.Multiplier, nil
}
eff := Eitherize(intFunc)
// Act
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 3, result)
})
}
// TestEitherize_Failure tests error handling with Eitherize
func TestEitherize_Failure(t *testing.T) {
t.Run("converts error to failure", func(t *testing.T) {
// Arrange
expectedErr := errors.New("operation failed")
failFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return "", expectedErr
}
eff := Eitherize(failFunc)
// Act
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("preserves error message", func(t *testing.T) {
// Arrange
expectedErr := fmt.Errorf("validation error: field is required")
failFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return 0, expectedErr
}
eff := Eitherize(failFunc)
// Act
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
// TestEitherize_EdgeCases tests edge cases for Eitherize
func TestEitherize_EdgeCases(t *testing.T) {
t.Run("handles nil context", func(t *testing.T) {
// Arrange
nilCtxFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
if ctx == nil {
return "nil-context", nil
}
return "non-nil-context", nil
}
eff := Eitherize(nilCtxFunc)
// Act
ioResult := Provide[string](testConfig)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(nil)
// Assert
assert.NoError(t, err)
assert.Equal(t, "nil-context", result)
})
t.Run("handles zero value config", func(t *testing.T) {
// Arrange
zeroFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return cfg.Prefix, nil
}
eff := Eitherize(zeroFunc)
// Act
result, err := runEffect(eff, TestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, "", result)
})
t.Run("handles pointer types", func(t *testing.T) {
// Arrange
type User struct {
Name string
}
ptrFunc := func(cfg TestConfig, ctx context.Context) (*User, error) {
return &User{Name: cfg.Prefix}, nil
}
eff := Eitherize(ptrFunc)
// Act
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "LOG", result.Name)
})
}
// TestEitherize_Integration tests integration with other operations
func TestEitherize_Integration(t *testing.T) {
t.Run("composes with Map", func(t *testing.T) {
// Arrange
baseFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return cfg.Multiplier, nil
}
eff := Eitherize(baseFunc)
// Act
pipeline := F.Pipe1(
eff,
Map[TestConfig](func(n int) string { return strconv.Itoa(n) }),
)
result, err := runEffect(pipeline, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "3", result)
})
t.Run("composes with Chain", func(t *testing.T) {
// Arrange
firstFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return cfg.Multiplier, nil
}
secondFunc := func(n int) Effect[TestConfig, string] {
return Succeed[TestConfig](fmt.Sprintf("value: %d", n))
}
// Act
pipeline := F.Pipe1(
Eitherize(firstFunc),
Chain[TestConfig](secondFunc),
)
result, err := runEffect(pipeline, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "value: 3", result)
})
}
// TestEitherize1_Success tests successful conversion with Eitherize1
func TestEitherize1_Success(t *testing.T) {
t.Run("converts successful function to Kleisli", func(t *testing.T) {
// Arrange
multiplyFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
return n * cfg.Multiplier, nil
}
kleisli := Eitherize1(multiplyFunc)
// Act
eff := kleisli(10)
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 30, result)
})
t.Run("works with string input", func(t *testing.T) {
// Arrange
concatFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
return cfg.Prefix + "-" + s, nil
}
kleisli := Eitherize1(concatFunc)
// Act
eff := kleisli("input")
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "LOG-input", result)
})
t.Run("preserves context in Kleisli", func(t *testing.T) {
// Arrange
type ctxKey string
key := ctxKey("factor")
scaleFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
factor := ctx.Value(key)
if factor == nil {
return n * cfg.Multiplier, nil
}
return n * factor.(int), nil
}
kleisli := Eitherize1(scaleFunc)
// Act
eff := kleisli(5)
ioResult := Provide[int](testConfig)(eff)
readerResult := RunSync(ioResult)
ctx := context.WithValue(context.Background(), key, 7)
result, err := readerResult(ctx)
// Assert
assert.NoError(t, err)
assert.Equal(t, 35, result)
})
}
// TestEitherize1_Failure tests error handling with Eitherize1
func TestEitherize1_Failure(t *testing.T) {
t.Run("converts error to failure in Kleisli", func(t *testing.T) {
// Arrange
expectedErr := errors.New("division by zero")
divideFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
if n == 0 {
return 0, expectedErr
}
return 100 / n, nil
}
kleisli := Eitherize1(divideFunc)
// Act
eff := kleisli(0)
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("preserves error context", func(t *testing.T) {
// Arrange
validateFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
if len(s) > 10 {
return "", fmt.Errorf("string too long: %d > 10", len(s))
}
return s, nil
}
kleisli := Eitherize1(validateFunc)
// Act
eff := kleisli("this-string-is-too-long")
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "string too long")
})
}
// TestEitherize1_EdgeCases tests edge cases for Eitherize1
func TestEitherize1_EdgeCases(t *testing.T) {
t.Run("handles zero value input", func(t *testing.T) {
// Arrange
zeroFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
return n, nil
}
kleisli := Eitherize1(zeroFunc)
// Act
eff := kleisli(0)
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
t.Run("handles pointer input", func(t *testing.T) {
// Arrange
type Input struct {
Value int
}
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
if in == nil {
return 0, errors.New("nil input")
}
return in.Value * cfg.Multiplier, nil
}
kleisli := Eitherize1(ptrFunc)
// Act
eff := kleisli(&Input{Value: 7})
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 21, result)
})
t.Run("handles nil pointer input", func(t *testing.T) {
// Arrange
type Input struct {
Value int
}
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
if in == nil {
return 0, errors.New("nil input")
}
return in.Value, nil
}
kleisli := Eitherize1(ptrFunc)
// Act
eff := kleisli((*Input)(nil))
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "nil input")
})
}
// TestEitherize1_Integration tests integration with other operations
func TestEitherize1_Integration(t *testing.T) {
t.Run("composes with Chain", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
doubleFunc := func(n int) Effect[TestConfig, int] {
return Succeed[TestConfig](n * 2)
}
parseKleisli := Eitherize1(parseFunc)
// Act
pipeline := F.Pipe2(
Succeed[TestConfig]("42"),
Chain[TestConfig](parseKleisli),
Chain[TestConfig](doubleFunc),
)
result, err := runEffect(pipeline, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 84, result)
})
t.Run("handles error in chain", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
parseKleisli := Eitherize1(parseFunc)
// Act
pipeline := F.Pipe1(
Succeed[TestConfig]("not-a-number"),
Chain[TestConfig](parseKleisli),
)
_, err := runEffect(pipeline, testConfig)
// Assert
assert.Error(t, err)
})
t.Run("composes multiple Kleisli arrows", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
formatFunc := func(cfg TestConfig, ctx context.Context, n int) (string, error) {
return fmt.Sprintf("%s-%d", cfg.Prefix, n), nil
}
parseKleisli := Eitherize1(parseFunc)
formatKleisli := Eitherize1(formatFunc)
// Act
pipeline := F.Pipe2(
Succeed[TestConfig]("123"),
Chain[TestConfig](parseKleisli),
Chain[TestConfig](formatKleisli),
)
result, err := runEffect(pipeline, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "LOG-123", result)
})
}
// TestEitherize_TypeSafety tests type safety across different scenarios
func TestEitherize_TypeSafety(t *testing.T) {
t.Run("Eitherize with complex types", func(t *testing.T) {
// Arrange
type ComplexResult struct {
Data map[string]int
Count int
}
complexFunc := func(cfg TestConfig, ctx context.Context) (ComplexResult, error) {
return ComplexResult{
Data: map[string]int{cfg.Prefix: cfg.Multiplier},
Count: cfg.Multiplier,
}, nil
}
eff := Eitherize(complexFunc)
// Act
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 3, result.Data["LOG"])
assert.Equal(t, 3, result.Count)
})
t.Run("Eitherize1 with different input and output types", func(t *testing.T) {
// Arrange
type Input struct {
ID int
}
type Output struct {
Name string
}
convertFunc := func(cfg TestConfig, ctx context.Context, in Input) (Output, error) {
return Output{Name: fmt.Sprintf("%s-%d", cfg.Prefix, in.ID)}, nil
}
kleisli := Eitherize1(convertFunc)
// Act
eff := kleisli(Input{ID: 99})
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "LOG-99", result.Name)
})
}

View File

@@ -288,5 +288,3 @@ func BenchmarkVoidMonoid_Empty(b *testing.B) {
_ = m.Empty()
}
}
// Made with Bob