1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-29 10:36:04 +02:00

Compare commits

..

5 Commits

Author SHA1 Message Date
Dr. Carsten Leue
a7aa7e3560 fix: better DI example
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 22:45:17 +01:00
Dr. Carsten Leue
ff2a4299b2 fix: add some useful lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 17:39:34 +01:00
Dr. Carsten Leue
edd66d63e6 fix: more codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 14:51:35 +01:00
Dr. Carsten Leue
909aec8eba fix: better sequence iter
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-26 10:41:25 +01:00
Obed Tetteh
da0344f9bd feat(iterator): add Last function with Option return type (#155)
- Add Last function to retrieve the final element from an iterator,
  returning Some(element) for non-empty sequences and None for empty ones.
- Includes tests covering simple types and  complex types
- Add documentation including example code
2026-01-26 09:04:51 +01:00
60 changed files with 12290 additions and 68 deletions

View File

@@ -0,0 +1,168 @@
package readerreaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
"github.com/IBM/fp-go/v2/result"
)
// Local modifies the outer environment before passing it to a computation.
// Useful for providing different configurations to sub-computations.
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.Local[context.Context, error, A](f)
}
// LocalIOK transforms the outer environment of a ReaderReaderIOResult using an IO-based Kleisli arrow.
// It allows you to modify the outer environment through an effectful computation before
// passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources,
// but these effects cannot fail (or failures are not relevant).
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOK[context.Context, error, A](f)
}
// LocalIOEitherK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
// It allows you to modify the outer environment through an effectful computation that can fail before
// passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation itself requires IO effects that can fail,
// such as reading from a file that might not exist, making a network call that might timeout,
// or parsing data that might be invalid.
//
// The transformation happens in two stages:
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOEitherK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOEitherK[context.Context, A](f)
}
// LocalIOResultK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
// This is a type-safe alias for LocalIOEitherK specialized for Result types (which use error as the error type).
//
// It allows you to modify the outer environment through an effectful computation that can fail before
// passing it to the ReaderReaderIOResult.
//
// The transformation happens in two stages:
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOEitherK[context.Context, A](f)
}
//go:inline
func LocalResultK[A, R1, R2 any](f result.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalEitherK[context.Context, A](f)
}
// LocalReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
// It allows you to modify the outer environment through a computation that depends on the inner context
// and can perform IO effects that may fail.
//
// This is useful when the outer environment transformation requires access to the inner context (e.g., context.Context)
// and may perform IO operations that can fail, such as database queries, API calls, or file operations.
//
// The transformation happens in three stages:
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalReaderIOEitherK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderIOEitherK[A](f)
}
// LocalReaderIOResultK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
// This is a type-safe alias for LocalReaderIOEitherK specialized for Result types (which use error as the error type).
//
// It allows you to modify the outer environment through a computation that depends on the inner context
// and can perform IO effects that may fail.
//
// The transformation happens in three stages:
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderIOEitherK[A](f)
}
//go:inline
func LocalReaderReaderIOEitherK[A, R1, R2 any](f Kleisli[R2, R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderReaderIOEitherK[A](f)
}

View File

@@ -0,0 +1,428 @@
// 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 readerreaderioresult
import (
"context"
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type SimpleConfig struct {
Port int
}
type DetailedConfig struct {
Host string
Port int
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
ctx := context.Background()
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) io.IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOK
adapted := LocalIOK[string](loadConfig)(useConfig)
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) io.IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Processed: %d", n))
}
}
}
adapted := LocalIOK[string](loadData)(processData)
res := adapted("test")(ctx)()
assert.Equal(t, result.Of("Processed: 40"), res)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
loadConfig := func(path string) io.IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
}
adapted := LocalIOK[string](loadConfig)(failingOperation)
res := adapted("config.json")(ctx)()
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOEitherK tests LocalIOEitherK functionality
func TestLocalIOEitherK(t *testing.T) {
ctx := context.Background()
t.Run("basic IOResult transformation", func(t *testing.T) {
// IOResult effect that loads config from a path (can fail)
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
return result.Left[SimpleConfig](errors.New("file not found"))
}
}
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
res := adapted("missing.json")(ctx)()
// Error from loadConfig should propagate
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOResultK tests LocalIOResultK functionality
func TestLocalIOResultK(t *testing.T) {
ctx := context.Background()
t.Run("basic IOResult transformation", func(t *testing.T) {
// IOResult effect that loads config from a path (can fail)
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOResultK
adapted := LocalIOResultK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("compose multiple LocalIOResultK", func(t *testing.T) {
// First transformation: string -> int (can fail)
parseID := func(s string) ioresult.IOResult[int] {
return func() result.Result[int] {
if s == "" {
return result.Left[int](errors.New("empty string"))
}
return result.Of(len(s) * 10)
}
}
// Second transformation: int -> SimpleConfig (can fail)
loadConfig := func(id int) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if id < 0 {
return result.Left[SimpleConfig](errors.New("invalid ID"))
}
return result.Of(SimpleConfig{Port: 8000 + id})
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose transformations
step1 := LocalIOResultK[string](loadConfig)(formatConfig)
step2 := LocalIOResultK[string](parseID)(step1)
// Success case
res := step2("test")(ctx)()
assert.Equal(t, result.Of("Port: 8040"), res)
// Failure in first transformation
resErr1 := step2("")(ctx)()
assert.True(t, result.IsLeft(resErr1))
})
}
// TestLocalReaderIOEitherK tests LocalReaderIOEitherK functionality
func TestLocalReaderIOEitherK(t *testing.T) {
ctx := context.Background()
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
// ReaderIOResult effect that loads config from a path (can fail, uses context)
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
// Could use context here for cancellation, logging, etc.
return result.Of(SimpleConfig{Port: 8080})
}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalReaderIOEitherK
adapted := LocalReaderIOEitherK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("context propagation", func(t *testing.T) {
type ctxKey string
const key ctxKey = "test-key"
// ReaderIOResult that reads from context
loadFromContext := func(path string) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
if val := ctx.Value(key); val != nil {
return result.Of(val.(string))
}
return result.Left[string](errors.New("key not found in context"))
}
}
}
// ReaderReaderIOResult that uses the loaded value
useValue := func(val string) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of("Loaded: " + val)
}
}
}
adapted := LocalReaderIOEitherK[string](loadFromContext)(useValue)
// With context value
ctxWithValue := context.WithValue(ctx, key, "test-value")
res := adapted("ignored")(ctxWithValue)()
assert.Equal(t, result.Of("Loaded: test-value"), res)
// Without context value
resErr := adapted("ignored")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
}
// TestLocalReaderIOResultK tests LocalReaderIOResultK functionality
func TestLocalReaderIOResultK(t *testing.T) {
ctx := context.Background()
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
// ReaderIOResult effect that loads config from a path (can fail, uses context)
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalReaderIOResultK
adapted := LocalReaderIOResultK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("real-world: load and validate config with context", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Read file with context (can fail, uses context for cancellation)
readFile := func(cf ConfigFile) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
// Check context cancellation
select {
case <-ctx.Done():
return result.Left[string](ctx.Err())
default:
}
if cf.Path == "" {
return result.Left[string](errors.New("empty path"))
}
return result.Of(`{"port":9000}`)
}
}
}
// Parse config with context (can fail)
parseConfig := func(content string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if content == "" {
return result.Left[SimpleConfig](errors.New("empty content"))
}
return result.Of(SimpleConfig{Port: 9000})
}
}
}
// Use the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Using port: %d", cfg.Port))
}
}
}
// Compose the pipeline
step1 := LocalReaderIOResultK[string](parseConfig)(useConfig)
step2 := LocalReaderIOResultK[string](readFile)(step1)
// Success case
res := step2(ConfigFile{Path: "app.json"})(ctx)()
assert.Equal(t, result.Of("Using port: 9000"), res)
// Failure case
resErr := step2(ConfigFile{Path: ""})(ctx)()
assert.True(t, result.IsLeft(resErr))
})
}

View File

@@ -37,6 +37,7 @@ import (
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readeroption"
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
"github.com/IBM/fp-go/v2/result"
)
// FromReaderOption converts a ReaderOption to a ReaderReaderIOResult.
@@ -170,6 +171,15 @@ func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B]
)
}
//go:inline
func ChainResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return fromeither.ChainEitherK(
Chain[R, A, B],
FromEither[R, B],
f,
)
}
// MonadChainFirstEitherK chains a computation that returns an Either but preserves the original value.
// Useful for validation or side effects that may fail.
// This is the monadic version that takes the computation as the first parameter.
@@ -837,14 +847,6 @@ func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
return RRIOE.MapLeft[R, context.Context, A](f)
}
// Local modifies the outer environment before passing it to a computation.
// Useful for providing different configurations to sub-computations.
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.Local[context.Context, error, A](f)
}
// Read provides a specific outer environment value to a computation.
// Converts ReaderReaderIOResult[R, A] to ReaderIOResult[context.Context, A].
//
@@ -892,3 +894,8 @@ func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
return reader.Map[R](RIOE.Delay[A](delay))
}
//go:inline
func Defer[R, A any](fa Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
return RRIOE.Defer(fa)
}

View File

@@ -0,0 +1,9 @@
package readerreaderioresult
import (
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
)
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
return RRIOE.TraverseArray(f)
}

View File

@@ -30,8 +30,8 @@ import (
type (
// InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier
InjectableFactory = func(Dependency) IOResult[any]
ProviderFactory = func(InjectableFactory) IOResult[any]
InjectableFactory = ReaderIOResult[Dependency, any]
ProviderFactory = ReaderIOResult[InjectableFactory, any]
paramIndex = map[int]int
paramValue = map[int]any

View File

@@ -4,6 +4,7 @@ import (
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/record"
)
@@ -12,4 +13,5 @@ type (
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Entry[K comparable, V any] = record.Entry[K, V]
ReaderIOResult[R, T any] = readerioresult.ReaderIOResult[R, T]
)

264
v2/effect/bind.go Normal file
View File

@@ -0,0 +1,264 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func Do[C, S any](
empty S,
) Effect[C, S] {
return readerreaderioresult.Of[C](empty)
}
//go:inline
func Bind[C, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.Bind(setter, f)
}
//go:inline
func Let[C, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[C, S1, S2] {
return readerreaderioresult.Let[C](setter, f)
}
//go:inline
func LetTo[C, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[C, S1, S2] {
return readerreaderioresult.LetTo[C](setter, b)
}
//go:inline
func BindTo[C, S1, T any](
setter func(T) S1,
) Operator[C, T, S1] {
return readerreaderioresult.BindTo[C](setter)
}
//go:inline
func ApS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Effect[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApS[C](setter, fa)
}
//go:inline
func ApSL[C, S, T any](
lens Lens[S, T],
fa Effect[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApSL[C](lens, fa)
}
//go:inline
func BindL[C, S, T any](
lens Lens[S, T],
f func(T) Effect[C, T],
) Operator[C, S, S] {
return readerreaderioresult.BindL[C](lens, f)
}
//go:inline
func LetL[C, S, T any](
lens Lens[S, T],
f func(T) T,
) Operator[C, S, S] {
return readerreaderioresult.LetL[C](lens, f)
}
//go:inline
func LetToL[C, S, T any](
lens Lens[S, T],
b T,
) Operator[C, S, S] {
return readerreaderioresult.LetToL[C](lens, b)
}
//go:inline
func BindIOEitherK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f ioeither.Kleisli[error, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOEitherK[C](setter, f)
}
//go:inline
func BindIOResultK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f ioresult.Kleisli[S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOResultK[C](setter, f)
}
//go:inline
func BindIOK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f io.Kleisli[S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOK[C](setter, f)
}
//go:inline
func BindReaderK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindReaderK[C](setter, f)
}
//go:inline
func BindReaderIOK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f readerio.Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindReaderIOK[C](setter, f)
}
//go:inline
func BindEitherK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f either.Kleisli[error, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindEitherK[C](setter, f)
}
//go:inline
func BindIOEitherKL[C, S, T any](
lens Lens[S, T],
f ioeither.Kleisli[error, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindIOEitherKL[C](lens, f)
}
//go:inline
func BindIOKL[C, S, T any](
lens Lens[S, T],
f io.Kleisli[T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindIOKL[C](lens, f)
}
//go:inline
func BindReaderKL[C, S, T any](
lens Lens[S, T],
f reader.Kleisli[C, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindReaderKL[C](lens, f)
}
//go:inline
func BindReaderIOKL[C, S, T any](
lens Lens[S, T],
f readerio.Kleisli[C, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindReaderIOKL[C](lens, f)
}
//go:inline
func ApIOEitherS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa IOEither[error, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApIOEitherS[C](setter, fa)
}
//go:inline
func ApIOS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa IO[T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApIOS[C](setter, fa)
}
//go:inline
func ApReaderS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApReaderS[C](setter, fa)
}
//go:inline
func ApReaderIOS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIO[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApReaderIOS[C](setter, fa)
}
//go:inline
func ApEitherS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Either[error, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApEitherS[C](setter, fa)
}
//go:inline
func ApIOEitherSL[C, S, T any](
lens Lens[S, T],
fa IOEither[error, T],
) Operator[C, S, S] {
return readerreaderioresult.ApIOEitherSL[C](lens, fa)
}
//go:inline
func ApIOSL[C, S, T any](
lens Lens[S, T],
fa IO[T],
) Operator[C, S, S] {
return readerreaderioresult.ApIOSL[C](lens, fa)
}
//go:inline
func ApReaderSL[C, S, T any](
lens Lens[S, T],
fa Reader[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApReaderSL[C](lens, fa)
}
//go:inline
func ApReaderIOSL[C, S, T any](
lens Lens[S, T],
fa ReaderIO[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApReaderIOSL[C](lens, fa)
}
//go:inline
func ApEitherSL[C, S, T any](
lens Lens[S, T],
fa Either[error, T],
) Operator[C, S, S] {
return readerreaderioresult.ApEitherSL[C](lens, fa)
}

768
v2/effect/bind_test.go Normal file
View File

@@ -0,0 +1,768 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
)
type BindState struct {
Name string
Age int
Email string
}
func TestDo(t *testing.T) {
t.Run("creates effect with initial state", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 30}
eff := Do[TestContext](initial)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, initial, result)
})
t.Run("creates effect with empty struct", func(t *testing.T) {
type Empty struct{}
eff := Do[TestContext](Empty{})
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, Empty{}, result)
})
}
func TestBind(t *testing.T) {
t.Run("binds effect result to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := Bind[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("chains multiple binds", func(t *testing.T) {
initial := BindState{}
eff := Bind[TestContext](
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
func(s BindState) Effect[TestContext, string] {
return Of[TestContext, string]("alice@example.com")
},
)(Bind[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](30)
},
)(Bind[TestContext](
func(name string) func(BindState) BindState {
return func(s BindState) BindState {
s.Name = name
return s
}
},
func(s BindState) Effect[TestContext, string] {
return Of[TestContext, string]("Alice")
},
)(Do[TestContext](initial))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
assert.Equal(t, "alice@example.com", result.Email)
})
t.Run("propagates errors", func(t *testing.T) {
expectedErr := errors.New("bind error")
initial := BindState{Name: "Alice"}
eff := Bind[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLet(t *testing.T) {
t.Run("computes value and binds to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := Let[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) int {
return len(s.Name) * 10
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 50, result.Age) // len("Alice") * 10
})
t.Run("chains with Bind", func(t *testing.T) {
initial := BindState{Name: "Bob"}
eff := Let[TestContext](
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
func(s BindState) string {
return s.Name + "@example.com"
},
)(Bind[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](25)
},
)(Do[TestContext](initial)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Bob", result.Name)
assert.Equal(t, 25, result.Age)
assert.Equal(t, "Bob@example.com", result.Email)
})
}
func TestLetTo(t *testing.T) {
t.Run("binds constant value to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := LetTo[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
42,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 42, result.Age)
})
t.Run("chains multiple LetTo", func(t *testing.T) {
initial := BindState{}
eff := LetTo[TestContext](
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
"test@example.com",
)(LetTo[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
30,
)(LetTo[TestContext](
func(name string) func(BindState) BindState {
return func(s BindState) BindState {
s.Name = name
return s
}
},
"Alice",
)(Do[TestContext](initial))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
assert.Equal(t, "test@example.com", result.Email)
})
}
func TestBindTo(t *testing.T) {
t.Run("wraps value in state", func(t *testing.T) {
type SimpleState struct {
Value int
}
eff := BindTo[TestContext](func(v int) SimpleState {
return SimpleState{Value: v}
})(Of[TestContext, int](42))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result.Value)
})
t.Run("starts a bind chain", func(t *testing.T) {
type State struct {
X int
Y string
}
eff := Let[TestContext](
func(y string) func(State) State {
return func(s State) State {
s.Y = y
return s
}
},
func(s State) string {
return "computed"
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext, int](10)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
assert.Equal(t, "computed", result.Y)
})
}
func TestApS(t *testing.T) {
t.Run("applies effect and binds result to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
ageEffect := Of[TestContext, int](30)
eff := ApS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ageEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates errors from applied effect", func(t *testing.T) {
expectedErr := errors.New("aps error")
initial := BindState{Name: "Alice"}
ageEffect := Fail[TestContext, int](expectedErr)
eff := ApS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ageEffect,
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestBindIOK(t *testing.T) {
t.Run("binds IO operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) io.IO[int] {
return func() int {
return 30
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindIOEitherK(t *testing.T) {
t.Run("binds successful IOEither to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioeither.IOEither[error, int] {
return ioeither.Of[error, int](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates IOEither error", func(t *testing.T) {
expectedErr := errors.New("ioeither error")
initial := BindState{Name: "Alice"}
eff := BindIOEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioeither.IOEither[error, int] {
return ioeither.Left[int, error](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestBindIOResultK(t *testing.T) {
t.Run("binds successful IOResult to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOResultK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioresult.IOResult[int] {
return ioresult.Of[int](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindReaderK(t *testing.T) {
t.Run("binds Reader operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindReaderK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) reader.Reader[TestContext, int] {
return func(ctx TestContext) int {
return 30
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindReaderIOK(t *testing.T) {
t.Run("binds ReaderIO operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindReaderIOK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) readerio.ReaderIO[TestContext, int] {
return func(ctx TestContext) io.IO[int] {
return func() int {
return 30
}
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindEitherK(t *testing.T) {
t.Run("binds successful Either to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) either.Either[error, int] {
return either.Of[error, int](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates Either error", func(t *testing.T) {
expectedErr := errors.New("either error")
initial := BindState{Name: "Alice"}
eff := BindEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) either.Either[error, int] {
return either.Left[int, error](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLensOperations(t *testing.T) {
// Create lenses for BindState
nameLens := lens.MakeLens(
func(s BindState) string { return s.Name },
func(s BindState, name string) BindState {
s.Name = name
return s
},
)
ageLens := lens.MakeLens(
func(s BindState) int { return s.Age },
func(s BindState, age int) BindState {
s.Age = age
return s
},
)
t.Run("ApSL applies effect using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
ageEffect := Of[TestContext, int](30)
eff := ApSL[TestContext](ageLens, ageEffect)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("BindL binds effect using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := BindL[TestContext](
ageLens,
func(age int) Effect[TestContext, int] {
return Of[TestContext, int](age + 5)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("LetL computes value using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := LetL[TestContext](
ageLens,
func(age int) int {
return age * 2
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 50, result.Age)
})
t.Run("LetToL sets constant using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := LetToL[TestContext](ageLens, 100)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 100, result.Age)
})
t.Run("chains lens operations", func(t *testing.T) {
initial := BindState{}
eff := LetToL[TestContext](
ageLens,
30,
)(LetToL[TestContext](
nameLens,
"Bob",
)(Do[TestContext](initial)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Bob", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestApOperations(t *testing.T) {
t.Run("ApIOS applies IO effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
ioEffect := func() int { return 30 }
eff := ApIOS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ioEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
t.Run("ApReaderS applies Reader effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
readerEffect := func(ctx TestContext) int { return 30 }
eff := ApReaderS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
readerEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
t.Run("ApEitherS applies Either effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eitherEffect := either.Of[error, int](30)
eff := ApEitherS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
eitherEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
}
func TestComplexBindChain(t *testing.T) {
t.Run("builds complex state with multiple operations", func(t *testing.T) {
type ComplexState struct {
Name string
Age int
Email string
IsAdmin bool
Score int
}
eff := LetTo[TestContext](
func(score int) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Score = score
return s
}
},
100,
)(Let[TestContext](
func(isAdmin bool) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.IsAdmin = isAdmin
return s
}
},
func(s ComplexState) bool {
return s.Age >= 18
},
)(Let[TestContext](
func(email string) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Email = email
return s
}
},
func(s ComplexState) string {
return s.Name + "@example.com"
},
)(Bind[TestContext](
func(age int) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Age = age
return s
}
},
func(s ComplexState) Effect[TestContext, int] {
return Of[TestContext, int](25)
},
)(BindTo[TestContext](func(name string) ComplexState {
return ComplexState{Name: name}
})(Of[TestContext, string]("Alice"))))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 25, result.Age)
assert.Equal(t, "Alice@example.com", result.Email)
assert.True(t, result.IsAdmin)
assert.Equal(t, 100, result.Score)
})
}

110
v2/effect/dependencies.go Normal file
View File

@@ -0,0 +1,110 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
)
//go:inline
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
//go:inline
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
//go:inline
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOK[A](f)
}
//go:inline
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOResultK[A](f)
}
//go:inline
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalResultK[A](f)
}
//go:inline
func LocalThunkK[A, C1, C2 any](f readerioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderIOResultK[A](f)
}
// LocalEffectK transforms the context of an Effect using an Effect-returning function.
// This is the most powerful context transformation function, allowing the transformation
// itself to be effectful (can fail, perform I/O, and access the outer context).
//
// LocalEffectK takes a Kleisli arrow that:
// - Accepts the outer context C2
// - Returns an Effect that produces the inner context C1
// - Can fail with an error during context transformation
// - Can perform I/O operations during transformation
//
// This is useful when:
// - Context transformation requires I/O (e.g., loading config from a file)
// - Context transformation can fail (e.g., validating or parsing context)
// - Context transformation needs to access the outer context
//
// Type Parameters:
// - A: The value type produced by the effect
// - C1: The inner context type (required by the original effect)
// - C2: The outer context type (provided to the transformed effect)
//
// Parameters:
// - f: A Kleisli arrow (C2 -> Effect[C2, C1]) that transforms C2 to C1 effectfully
//
// Returns:
// - A function that transforms Effect[C1, A] to Effect[C2, A]
//
// Example:
//
// type DatabaseConfig struct {
// ConnectionString string
// }
//
// type AppConfig struct {
// ConfigPath string
// }
//
// // Effect that needs DatabaseConfig
// dbEffect := effect.Of[DatabaseConfig, string]("query result")
//
// // Transform AppConfig to DatabaseConfig effectfully
// // (e.g., load config from file, which can fail)
// loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
// return effect.Chain[AppConfig](func(_ AppConfig) Effect[AppConfig, DatabaseConfig] {
// // Simulate loading config from file (can fail)
// return effect.Of[AppConfig, DatabaseConfig](DatabaseConfig{
// ConnectionString: "loaded from " + app.ConfigPath,
// })
// })(effect.Of[AppConfig, AppConfig](app))
// }
//
// // Apply the transformation
// transform := effect.LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
// appEffect := transform(dbEffect)
//
// // Run with AppConfig
// ioResult := effect.Provide(AppConfig{ConfigPath: "/etc/app.conf"})(appEffect)
// readerResult := effect.RunSync(ioResult)
// result, err := readerResult(context.Background())
//
// Comparison with other Local functions:
// - Local/Contramap: Pure context transformation (C2 -> C1)
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
// - LocalReaderIOResultK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
//
//go:inline
func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderReaderIOEitherK[A](f)
}

View File

@@ -0,0 +1,620 @@
// 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"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/stretchr/testify/assert"
)
type OuterContext struct {
Value string
Number int
}
type InnerContext struct {
Value string
}
func TestLocal(t *testing.T) {
t.Run("transforms context for inner effect", func(t *testing.T) {
// Create an effect that uses InnerContext
innerEffect := Of[InnerContext, string]("result")
// Transform OuterContext to InnerContext
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
// Apply Local to transform the context
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("allows accessing outer context fields", func(t *testing.T) {
// Create an effect that reads from InnerContext
innerEffect := Chain[InnerContext](func(_ string) Effect[InnerContext, string] {
return Of[InnerContext, string]("inner value")
})(Of[InnerContext, string]("start"))
// Transform context
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value + " transformed"}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
Value: "original",
Number: 100,
})(outerEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner value", result)
})
t.Run("propagates errors from inner effect", func(t *testing.T) {
expectedErr := assert.AnError
innerEffect := Fail[InnerContext, string](expectedErr)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple Local transformations", func(t *testing.T) {
type Level1 struct {
A string
}
type Level2 struct {
B string
}
type Level3 struct {
C string
}
// Effect at deepest level
level3Effect := Of[Level3, string]("deep result")
// Transform Level2 -> Level3
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
return Level3{C: l2.B + "-c"}
})
// Transform Level1 -> Level2
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
return Level2{B: l1.A + "-b"}
})
// Compose transformations
level2Effect := local23(level3Effect)
level1Effect := local12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "deep result", result)
})
t.Run("works with complex context transformations", func(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
Database string
}
type AppConfig struct {
DB DatabaseConfig
APIKey string
Timeout int
}
// Effect that needs only DatabaseConfig
dbEffect := Of[DatabaseConfig, string]("connected")
// Extract DB config from AppConfig
accessor := func(app AppConfig) DatabaseConfig {
return app.DB
}
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
appEffect := kleisli(dbEffect)
// Run with full AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
DB: DatabaseConfig{
Host: "localhost",
Port: 5432,
Database: "mydb",
},
APIKey: "secret",
Timeout: 30,
})(appEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "connected", result)
})
}
func TestContramap(t *testing.T) {
t.Run("is equivalent to Local", func(t *testing.T) {
innerEffect := Of[InnerContext, int](42)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
// Test Local
localKleisli := Local[OuterContext, InnerContext, int](accessor)
localEffect := localKleisli(innerEffect)
// Test Contramap
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
contramapEffect := contramapKleisli(innerEffect)
outerCtx := OuterContext{Value: "test", Number: 100}
// Run both
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
localReader := RunSync[int](localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapReader := RunSync[int](contramapIO)
contramapResult, contramapErr := contramapReader(context.Background())
assert.NoError(t, localErr)
assert.NoError(t, contramapErr)
assert.Equal(t, localResult, contramapResult)
})
t.Run("transforms context correctly", func(t *testing.T) {
innerEffect := Of[InnerContext, string]("success")
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value + " modified"}
}
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
Value: "original",
Number: 50,
})(outerEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "success", result)
})
t.Run("handles errors from inner effect", func(t *testing.T) {
expectedErr := assert.AnError
innerEffect := Fail[InnerContext, int](expectedErr)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, int](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[int](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLocalAndContramapInteroperability(t *testing.T) {
t.Run("can be used interchangeably", func(t *testing.T) {
type Config1 struct {
Value string
}
type Config2 struct {
Data string
}
type Config3 struct {
Info string
}
// Effect at deepest level
effect3 := Of[Config3, string]("result")
// Use Local for first transformation
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
return Config3{Info: c2.Data}
})
// Use Contramap for second transformation
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
return Config2{Data: c1.Value}
})
// Compose them
effect2 := local23(effect3)
effect1 := contramap12(effect2)
// Run
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
}
func TestLocalEffectK(t *testing.T) {
t.Run("transforms context using effectful function", func(t *testing.T) {
type DatabaseConfig struct {
ConnectionString string
}
type AppConfig struct {
ConfigPath string
}
// Effect that needs DatabaseConfig
dbEffect := Of[DatabaseConfig, string]("query result")
// Transform AppConfig to DatabaseConfig effectfully
loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
ConnectionString: "loaded from " + app.ConfigPath,
})
}
// Apply the transformation
transform := LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "query result", result)
})
t.Run("propagates errors from context transformation", func(t *testing.T) {
type InnerCtx struct {
Value string
}
type OuterCtx struct {
Path string
}
innerEffect := Of[InnerCtx, string]("success")
expectedErr := assert.AnError
// Context transformation that fails
failingTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Fail[OuterCtx, InnerCtx](expectedErr)
}
transform := LocalEffectK[string, InnerCtx, OuterCtx](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates errors from inner effect", func(t *testing.T) {
type InnerCtx struct {
Value string
}
type OuterCtx struct {
Path string
}
expectedErr := assert.AnError
innerEffect := Fail[InnerCtx, string](expectedErr)
// Successful context transformation
transform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx, InnerCtx](InnerCtx{Value: outer.Path})
}
transformK := LocalEffectK[string, InnerCtx, OuterCtx](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("allows effectful context transformation with IO operations", func(t *testing.T) {
type Config struct {
Data string
}
type AppContext struct {
ConfigFile string
}
// Effect that uses Config
configEffect := Chain[Config](func(cfg Config) Effect[Config, string] {
return Of[Config, string]("processed: " + cfg.Data)
})(readerreaderioresult.Ask[Config]())
// Effectful transformation that simulates loading config
loadConfigEffect := func(app AppContext) Effect[AppContext, Config] {
// Simulate IO operation (e.g., reading file)
return Of[AppContext, Config](Config{
Data: "loaded from " + app.ConfigFile,
})
}
transform := LocalEffectK[string, Config, AppContext](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "processed: loaded from config.json", result)
})
t.Run("chains multiple LocalEffectK transformations", func(t *testing.T) {
type Level1 struct {
A string
}
type Level2 struct {
B string
}
type Level3 struct {
C string
}
// Effect at deepest level
level3Effect := Of[Level3, string]("deep result")
// Transform Level2 -> Level3 effectfully
transform23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2, Level3](Level3{C: l2.B + "-c"})
})
// Transform Level1 -> Level2 effectfully
transform12 := LocalEffectK[string, Level2, Level1](func(l1 Level1) Effect[Level1, Level2] {
return Of[Level1, Level2](Level2{B: l1.A + "-b"})
})
// Compose transformations
level2Effect := transform23(level3Effect)
level1Effect := transform12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "deep result", result)
})
t.Run("accesses outer context during transformation", func(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
}
type AppConfig struct {
Environment string
DBHost string
DBPort int
}
// Effect that needs DatabaseConfig
dbEffect := Chain[DatabaseConfig](func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
return Of[DatabaseConfig, string](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
})(readerreaderioresult.Ask[DatabaseConfig]())
// Transform using outer context
transformWithContext := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
// Access outer context to build inner context
prefix := ""
if app.Environment == "prod" {
prefix = "prod-"
}
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
Host: prefix + app.DBHost,
Port: app.DBPort,
})
}
transform := LocalEffectK[string, DatabaseConfig, AppConfig](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
Environment: "prod",
DBHost: "localhost",
DBPort: 5432,
})(appEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Contains(t, result, "prod-localhost")
})
t.Run("validates context during transformation", func(t *testing.T) {
type ValidatedConfig struct {
APIKey string
}
type RawConfig struct {
APIKey string
}
innerEffect := Of[ValidatedConfig, string]("success")
// Validation that can fail
validateConfig := func(raw RawConfig) Effect[RawConfig, ValidatedConfig] {
if raw.APIKey == "" {
return Fail[RawConfig, ValidatedConfig](assert.AnError)
}
return Of[RawConfig, ValidatedConfig](ValidatedConfig{
APIKey: raw.APIKey,
})
}
transform := LocalEffectK[string, ValidatedConfig, RawConfig](validateConfig)
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync[string](ioResult2)
result, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "success", result)
})
t.Run("composes with other Local functions", func(t *testing.T) {
type Level1 struct {
Value string
}
type Level2 struct {
Data string
}
type Level3 struct {
Info string
}
// Effect at deepest level
effect3 := Of[Level3, string]("result")
// Use LocalEffectK for first transformation (effectful)
localEffectK23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2, Level3](Level3{Info: l2.Data})
})
// Use Local for second transformation (pure)
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
// Compose them
effect2 := localEffectK23(effect3)
effect1 := local12(effect2)
// Run
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("handles complex nested effects in transformation", func(t *testing.T) {
type InnerCtx struct {
Value int
}
type OuterCtx struct {
Multiplier int
}
// Effect that uses InnerCtx
innerEffect := Chain[InnerCtx](func(ctx InnerCtx) Effect[InnerCtx, int] {
return Of[InnerCtx, int](ctx.Value * 2)
})(readerreaderioresult.Ask[InnerCtx]())
// Complex transformation with nested effects
complexTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx, InnerCtx](InnerCtx{
Value: outer.Multiplier * 10,
})
}
transform := LocalEffectK[int, InnerCtx, OuterCtx](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync[int](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 60, result) // 3 * 10 * 2
})
}

222
v2/effect/doc.go Normal file
View File

@@ -0,0 +1,222 @@
// 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 provides a functional effect system for managing side effects in Go.
# Overview
The effect package is a high-level abstraction for composing effectful computations
that may fail, require dependencies (context), and perform I/O operations. It is built
on top of ReaderReaderIOResult, providing a clean API for dependency injection and
error handling.
# Naming Conventions
The naming conventions in this package are modeled after effect-ts (https://effect.website/),
a popular TypeScript library for functional effect systems. This alignment helps developers
familiar with effect-ts to quickly understand and use this Go implementation.
# Core Type
The central type is Effect[C, A], which represents:
- C: The context/dependency type required by the effect
- A: The success value type produced by the effect
An Effect can:
- Succeed with a value of type A
- Fail with an error
- Require a context of type C
- Perform I/O operations
# Basic Operations
Creating Effects:
// Create a successful effect
effect.Succeed[MyContext, string]("hello")
// Create a failed effect
effect.Fail[MyContext, string](errors.New("failed"))
// Lift a pure value into an effect
effect.Of[MyContext, int](42)
Transforming Effects:
// Map over the success value
effect.Map[MyContext](func(x int) string {
return strconv.Itoa(x)
})
// Chain effects together (flatMap)
effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
return effect.Succeed[MyContext, string](strconv.Itoa(x))
})
// Tap into an effect without changing its value
effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
return effect.Succeed[MyContext, any](fmt.Println(x))
})
# Dependency Injection
Effects can access their required context:
// Transform the context before passing it to an effect
effect.Local[OuterCtx, InnerCtx](func(outer OuterCtx) InnerCtx {
return outer.Inner
})
// Provide a context to run an effect
effect.Provide[MyContext, string](myContext)
# Do Notation
The package provides "do notation" for composing effects in a sequential, imperative style:
type State struct {
X int
Y string
}
result := effect.Do[MyContext](State{}).
Bind(func(y string) func(State) State {
return func(s State) State {
s.Y = y
return s
}
}, fetchString).
Let(func(x int) func(State) State {
return func(s State) State {
s.X = x
return s
}
}, func(s State) int {
return len(s.Y)
})
# Bind Operations
The package provides various bind operations for integrating with other effect types:
- BindIOK: Bind an IO operation
- BindIOEitherK: Bind an IOEither operation
- BindIOResultK: Bind an IOResult operation
- BindReaderK: Bind a Reader operation
- BindReaderIOK: Bind a ReaderIO operation
- BindEitherK: Bind an Either operation
Each bind operation has a corresponding "L" variant for working with lenses:
- BindL, BindIOKL, BindReaderKL, etc.
# Applicative Operations
Apply effects in parallel:
// Apply a function effect to a value effect
effect.Ap[string, MyContext](valueEffect)(functionEffect)
// Apply effects to build up a structure
effect.ApS[MyContext](setter, effect1)
# Traversal
Traverse collections with effects:
// Map an array with an effectful function
effect.TraverseArray[MyContext](func(x int) Effect[MyContext, string] {
return effect.Succeed[MyContext, string](strconv.Itoa(x))
})
# Retry Logic
Retry effects with configurable policies:
effect.Retrying[MyContext, string](
retryPolicy,
func(status retry.RetryStatus) Effect[MyContext, string] {
return fetchData()
},
func(result Result[string]) bool {
return result.IsLeft() // retry on error
},
)
# Monoids
Combine effects using monoid operations:
// Combine effects using applicative semantics
effect.ApplicativeMonoid[MyContext](stringMonoid)
// Combine effects using alternative semantics (first success)
effect.AlternativeMonoid[MyContext](stringMonoid)
# Running Effects
To execute an effect:
// Provide the context
ioResult := effect.Provide[MyContext, string](myContext)(myEffect)
// Run synchronously
readerResult := effect.RunSync(ioResult)
// Execute with a context.Context
value, err := readerResult(ctx)
# Integration with Other Packages
The effect package integrates seamlessly with other fp-go packages:
- either: For error handling
- io: For I/O operations
- reader: For dependency injection
- result: For result types
- retry: For retry logic
- monoid: For combining effects
# Example
type Config struct {
APIKey string
BaseURL string
}
func fetchUser(id int) Effect[Config, User] {
return effect.Chain[Config](func(cfg Config) Effect[Config, User] {
// Use cfg.APIKey and cfg.BaseURL
return effect.Succeed[Config, User](User{ID: id})
})(effect.Of[Config, Config](Config{}))
}
func main() {
cfg := Config{APIKey: "key", BaseURL: "https://api.example.com"}
userEffect := fetchUser(42)
// Run the effect
ioResult := effect.Provide(cfg)(userEffect)
readerResult := effect.RunSync(ioResult)
user, err := readerResult(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", user)
}
*/
package effect
//go:generate go run ../main.go lens --dir . --filename gen_lens.go --include-test-files

51
v2/effect/effect.go Normal file
View File

@@ -0,0 +1,51 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
func Succeed[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
func Fail[C, A any](err error) Effect[C, A] {
return readerreaderioresult.Left[C, A](err)
}
func Of[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
return readerreaderioresult.Map[C](f)
}
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.Chain(f)
}
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
return readerreaderioresult.Ap[B](fa)
}
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
return readerreaderioresult.Defer(fa)
}
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
return readerreaderioresult.Tap(f)
}
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
return function.Ternary(pred, onTrue, onFalse)
}
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainResultK[C](f)
}
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
return readerreaderioresult.Read[A](c)
}

506
v2/effect/effect_test.go Normal file
View File

@@ -0,0 +1,506 @@
// 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"
"testing"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type TestContext struct {
Value string
}
func runEffect[A any](eff Effect[TestContext, A], ctx TestContext) (A, error) {
ioResult := Provide[TestContext, A](ctx)(eff)
readerResult := RunSync[A](ioResult)
return readerResult(context.Background())
}
func TestSucceed(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Succeed[TestContext, int](42)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("creates successful effect with string", func(t *testing.T) {
eff := Succeed[TestContext, string]("hello")
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "hello", result)
})
t.Run("creates successful effect with struct", func(t *testing.T) {
type User struct {
Name string
Age int
}
user := User{Name: "Alice", Age: 30}
eff := Succeed[TestContext, User](user)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, user, result)
})
}
func TestFail(t *testing.T) {
t.Run("creates failed effect with error", func(t *testing.T) {
expectedErr := errors.New("test error")
eff := Fail[TestContext, int](expectedErr)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("creates failed effect with custom error", func(t *testing.T) {
expectedErr := fmt.Errorf("custom error: %s", "details")
eff := Fail[TestContext, string](expectedErr)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestOf(t *testing.T) {
t.Run("lifts value into effect", func(t *testing.T) {
eff := Of[TestContext, int](100)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 100, result)
})
t.Run("is equivalent to Succeed", func(t *testing.T) {
value := "test value"
eff1 := Of[TestContext, string](value)
eff2 := Succeed[TestContext, string](value)
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
result2, err2 := runEffect(eff2, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, result1, result2)
})
}
func TestMap(t *testing.T) {
t.Run("maps over successful effect", func(t *testing.T) {
eff := Of[TestContext, int](10)
mapped := Map[TestContext](func(x int) int {
return x * 2
})(eff)
result, err := runEffect(mapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
t.Run("maps to different type", func(t *testing.T) {
eff := Of[TestContext, int](42)
mapped := Map[TestContext](func(x int) string {
return fmt.Sprintf("value: %d", x)
})(eff)
result, err := runEffect(mapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", result)
})
t.Run("preserves error in failed effect", func(t *testing.T) {
expectedErr := errors.New("original error")
eff := Fail[TestContext, int](expectedErr)
mapped := Map[TestContext](func(x int) int {
return x * 2
})(eff)
_, err := runEffect(mapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple maps", func(t *testing.T) {
eff := Of[TestContext, int](5)
result := Map[TestContext](func(x int) int {
return x + 1
})(Map[TestContext](func(x int) int {
return x * 2
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 11, value) // (5 * 2) + 1
})
}
func TestChain(t *testing.T) {
t.Run("chains successful effects", func(t *testing.T) {
eff := Of[TestContext, int](10)
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
t.Run("chains to different type", func(t *testing.T) {
eff := Of[TestContext, int](42)
chained := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](fmt.Sprintf("number: %d", x))
})(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "number: 42", result)
})
t.Run("propagates first error", func(t *testing.T) {
expectedErr := errors.New("first error")
eff := Fail[TestContext, int](expectedErr)
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedErr := errors.New("second error")
eff := Of[TestContext, int](10)
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
})(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple operations", func(t *testing.T) {
eff := Of[TestContext, int](5)
result := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x + 10)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, value) // (5 * 2) + 10
})
}
func TestAp(t *testing.T) {
t.Run("applies function effect to value effect", func(t *testing.T) {
fn := Of[TestContext, func(int) int](func(x int) int {
return x * 2
})
value := Of[TestContext, int](21)
result := Ap[int](value)(fn)
val, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
t.Run("applies function to different type", func(t *testing.T) {
fn := Of[TestContext, func(int) string](func(x int) string {
return fmt.Sprintf("value: %d", x)
})
value := Of[TestContext, int](42)
result := Ap[string](value)(fn)
val, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", val)
})
t.Run("propagates error from function effect", func(t *testing.T) {
expectedErr := errors.New("function error")
fn := Fail[TestContext, func(int) int](expectedErr)
value := Of[TestContext, int](42)
result := Ap[int](value)(fn)
_, err := runEffect(result, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates error from value effect", func(t *testing.T) {
expectedErr := errors.New("value error")
fn := Of[TestContext, func(int) int](func(x int) int {
return x * 2
})
value := Fail[TestContext, int](expectedErr)
result := Ap[int](value)(fn)
_, err := runEffect(result, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestSuspend(t *testing.T) {
t.Run("suspends effect computation", func(t *testing.T) {
callCount := 0
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
callCount++
return Of[TestContext, int](42)
})
// Effect not executed yet
assert.Equal(t, 0, callCount)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 1, callCount)
})
t.Run("suspends failing effect", func(t *testing.T) {
expectedErr := errors.New("suspended error")
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
})
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("allows lazy evaluation", func(t *testing.T) {
var value int
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
return Of[TestContext, int](value)
})
value = 10
result1, err1 := runEffect(eff, TestContext{Value: "test"})
value = 20
result2, err2 := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, 10, result1)
assert.Equal(t, 20, result2)
})
}
func TestTap(t *testing.T) {
t.Run("executes side effect without changing value", func(t *testing.T) {
sideEffectValue := 0
eff := Of[TestContext, int](42)
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
sideEffectValue = x * 2
return Of[TestContext, any](nil)
})(eff)
result, err := runEffect(tapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 84, sideEffectValue)
})
t.Run("propagates original error", func(t *testing.T) {
expectedErr := errors.New("original error")
eff := Fail[TestContext, int](expectedErr)
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
return Of[TestContext, any](nil)
})(eff)
_, err := runEffect(tapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates tap error", func(t *testing.T) {
expectedErr := errors.New("tap error")
eff := Of[TestContext, int](42)
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
return Fail[TestContext, any](expectedErr)
})(eff)
_, err := runEffect(tapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple taps", func(t *testing.T) {
values := []int{}
eff := Of[TestContext, int](10)
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
values = append(values, x+2)
return Of[TestContext, any](nil)
})(Tap[TestContext](func(x int) Effect[TestContext, any] {
values = append(values, x+1)
return Of[TestContext, any](nil)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 10, value)
assert.Equal(t, []int{11, 12}, values)
})
}
func TestTernary(t *testing.T) {
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
kleisli := Ternary[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
},
)
result, err := runEffect(kleisli(15), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "greater", result)
})
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
kleisli := Ternary[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
},
)
result, err := runEffect(kleisli(5), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "less or equal", result)
})
t.Run("handles errors in onTrue branch", func(t *testing.T) {
expectedErr := errors.New("true branch error")
kleisli := Ternary[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Fail[TestContext, string](expectedErr)
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
},
)
_, err := runEffect(kleisli(15), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("handles errors in onFalse branch", func(t *testing.T) {
expectedErr := errors.New("false branch error")
kleisli := Ternary[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
},
func(x int) Effect[TestContext, string] {
return Fail[TestContext, string](expectedErr)
},
)
_, err := runEffect(kleisli(5), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestEffectComposition(t *testing.T) {
t.Run("composes Map and Chain", func(t *testing.T) {
eff := Of[TestContext, int](5)
result := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](fmt.Sprintf("result: %d", x))
})(Map[TestContext](func(x int) int {
return x * 2
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "result: 10", value)
})
t.Run("composes Chain and Tap", func(t *testing.T) {
sideEffect := 0
eff := Of[TestContext, int](10)
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
sideEffect = x
return Of[TestContext, any](nil)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, value)
assert.Equal(t, 20, sideEffect)
})
}
func TestEffectWithResult(t *testing.T) {
t.Run("converts result to effect", func(t *testing.T) {
res := result.Of[int](42)
// This demonstrates integration with result package
assert.True(t, result.IsRight(res))
})
}

118
v2/effect/gen_lens_test.go Normal file
View File

@@ -0,0 +1,118 @@
package effect
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-27 22:19:41.6840253 +0100 CET m=+0.008579701
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"
__option "github.com/IBM/fp-go/v2/option"
__prism "github.com/IBM/fp-go/v2/optics/prism"
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
)
// ComplexServiceLenses provides lenses for accessing fields of ComplexService
type ComplexServiceLenses struct {
// mandatory fields
service1 __lens.Lens[ComplexService, Service1]
service2 __lens.Lens[ComplexService, Service2]
// optional fields
service1O __lens_option.LensO[ComplexService, Service1]
service2O __lens_option.LensO[ComplexService, Service2]
}
// ComplexServiceRefLenses provides lenses for accessing fields of ComplexService via a reference to ComplexService
type ComplexServiceRefLenses struct {
// mandatory fields
service1 __lens.Lens[*ComplexService, Service1]
service2 __lens.Lens[*ComplexService, Service2]
// optional fields
service1O __lens_option.LensO[*ComplexService, Service1]
service2O __lens_option.LensO[*ComplexService, Service2]
// prisms
service1P __prism.Prism[*ComplexService, Service1]
service2P __prism.Prism[*ComplexService, Service2]
}
// ComplexServicePrisms provides prisms for accessing fields of ComplexService
type ComplexServicePrisms struct {
service1 __prism.Prism[ComplexService, Service1]
service2 __prism.Prism[ComplexService, Service2]
}
// MakeComplexServiceLenses creates a new ComplexServiceLenses with lenses for all fields
func MakeComplexServiceLenses() ComplexServiceLenses {
// mandatory lenses
lensservice1 := __lens.MakeLensWithName(
func(s ComplexService) Service1 { return s.service1 },
func(s ComplexService, v Service1) ComplexService { s.service1 = v; return s },
"ComplexService.service1",
)
lensservice2 := __lens.MakeLensWithName(
func(s ComplexService) Service2 { return s.service2 },
func(s ComplexService, v Service2) ComplexService { s.service2 = v; return s },
"ComplexService.service2",
)
// optional lenses
lensservice1O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
lensservice2O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
return ComplexServiceLenses{
// mandatory lenses
service1: lensservice1,
service2: lensservice2,
// optional lenses
service1O: lensservice1O,
service2O: lensservice2O,
}
}
// MakeComplexServiceRefLenses creates a new ComplexServiceRefLenses with lenses for all fields
func MakeComplexServiceRefLenses() ComplexServiceRefLenses {
// mandatory lenses
lensservice1 := __lens.MakeLensStrictWithName(
func(s *ComplexService) Service1 { return s.service1 },
func(s *ComplexService, v Service1) *ComplexService { s.service1 = v; return s },
"(*ComplexService).service1",
)
lensservice2 := __lens.MakeLensStrictWithName(
func(s *ComplexService) Service2 { return s.service2 },
func(s *ComplexService, v Service2) *ComplexService { s.service2 = v; return s },
"(*ComplexService).service2",
)
// optional lenses
lensservice1O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
lensservice2O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
return ComplexServiceRefLenses{
// mandatory lenses
service1: lensservice1,
service2: lensservice2,
// optional lenses
service1O: lensservice1O,
service2O: lensservice2O,
}
}
// MakeComplexServicePrisms creates a new ComplexServicePrisms with prisms for all fields
func MakeComplexServicePrisms() ComplexServicePrisms {
_fromNonZeroservice1 := __option.FromNonZero[Service1]()
_prismservice1 := __prism.MakePrismWithName(
func(s ComplexService) __option.Option[Service1] { return _fromNonZeroservice1(s.service1) },
func(v Service1) ComplexService {
return ComplexService{ service1: v }
},
"ComplexService.service1",
)
_fromNonZeroservice2 := __option.FromNonZero[Service2]()
_prismservice2 := __prism.MakePrismWithName(
func(s ComplexService) __option.Option[Service2] { return _fromNonZeroservice2(s.service2) },
func(v Service2) ComplexService {
return ComplexService{ service2: v }
},
"ComplexService.service2",
)
return ComplexServicePrisms {
service1: _prismservice1,
service2: _prismservice2,
}
}

207
v2/effect/injection_test.go Normal file
View File

@@ -0,0 +1,207 @@
// Package effect demonstrates dependency injection using the Effect pattern.
//
// This test file shows how to build a type-safe dependency injection system where:
// - An InjectionContainer can resolve services by ID (InjectionToken)
// - Services are generic effects that depend on the container
// - Lookup methods convert from untyped container to typed dependencies
// - Handler functions depend type-safely on specific service interfaces
package effect
import (
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
type (
// InjectionToken is a unique identifier for services in the container
InjectionToken string
// InjectionContainer is an Effect that resolves services by their token.
// It takes an InjectionToken and returns a Thunk that produces any type.
// This allows the container to store and retrieve services of different types.
InjectionContainer = Effect[InjectionToken, any]
// Service is a generic Effect that depends on the InjectionContainer.
// It represents a computation that needs access to the dependency injection
// container to resolve its dependencies before producing a string result.
Service[T any] = Effect[InjectionContainer, T]
// Service1 is an example service interface that can be resolved from the container
Service1 interface {
GetService1() string
}
// Service2 is another example service interface
Service2 interface {
GetService2() string
}
// impl1 is a concrete implementation of Service1
impl1 struct{}
// impl2 is a concrete implementation of Service2
impl2 struct{}
)
// ComplexService demonstrates a more complex dependency injection scenario
// where a service depends on multiple other services. This struct aggregates
// Service1 and Service2, showing how to compose dependencies.
// The fp-go:Lens directive generates lens functions for type-safe field access.
//
// fp-go:Lens
type ComplexService struct {
service1 Service1
service2 Service2
}
func (_ *impl1) GetService1() string {
return "service1"
}
func (_ *impl2) GetService2() string {
return "service2"
}
const (
// service1 is the injection token for Service1
service1 = InjectionToken("service1")
// service2 is the injection token for Service2
service2 = InjectionToken("service2")
)
var (
// complexServiceLenses provides type-safe accessors for ComplexService fields,
// generated by the fp-go:Lens directive. These lenses are used in applicative
// composition to build the ComplexService from individual dependencies.
complexServiceLenses = MakeComplexServiceLenses()
)
// makeSampleInjectionContainer creates an InjectionContainer that can resolve services by ID.
// The container maps InjectionTokens to their corresponding service implementations.
// It returns an error if a requested service is not available.
func makeSampleInjectionContainer() InjectionContainer {
return func(token InjectionToken) Thunk[any] {
switch token {
case service1:
return readerioresult.Of(any(&impl1{}))
case service2:
return readerioresult.Of(any(&impl2{}))
default:
return readerioresult.Left[any](errors.New("dependency not available"))
}
}
}
// handleService1 is an Effect that depends type-safely on Service1.
// It demonstrates how to write handlers that work with specific service interfaces
// rather than the untyped container, providing compile-time type safety.
func handleService1() Effect[Service1, string] {
return func(ctx Service1) ReaderIOResult[string] {
return readerioresult.Of(fmt.Sprintf("Service1: %s", ctx.GetService1()))
}
}
// handleComplexService is an Effect that depends on ComplexService, which itself
// aggregates multiple service dependencies (Service1 and Service2).
// This demonstrates how to work with composite dependencies in a type-safe manner.
func handleComplexService() Effect[ComplexService, string] {
return func(ctx ComplexService) ReaderIOResult[string] {
return readerioresult.Of(fmt.Sprintf("ComplexService: %s x %s", ctx.service1.GetService1(), ctx.service2.GetService2()))
}
}
// lookupService1 is a lookup method that converts from an untyped InjectionContainer
// to a typed Service1 dependency. It performs two steps:
// 1. Read[any](service1) - retrieves the service from the container by token
// 2. ChainResultK(result.InstanceOf[Service1]) - safely casts from any to Service1
// This conversion provides type safety when moving from the untyped container to typed handlers.
var lookupService1 = F.Flow2(
Read[any](service1),
readerioresult.ChainResultK(result.InstanceOf[Service1]),
)
// lookupService2 is a lookup method for Service2, following the same pattern as lookupService1.
// It retrieves Service2 from the container and safely casts it to the correct type.
var lookupService2 = F.Flow2(
Read[any](service2),
readerioresult.ChainResultK(result.InstanceOf[Service2]),
)
// lookupComplexService demonstrates applicative composition for complex dependency injection.
// It builds a ComplexService by composing multiple service lookups:
// 1. Do[InjectionContainer](ComplexService{}) - starts with an empty ComplexService in the Effect context
// 2. ApSL(complexServiceLenses.service1, lookupService1) - looks up Service1 and sets it using the lens
// 3. ApSL(complexServiceLenses.service2, lookupService2) - looks up Service2 and sets it using the lens
//
// This applicative style allows parallel composition of independent dependencies,
// building the complete ComplexService from its constituent parts in a type-safe way.
var lookupComplexService = F.Pipe2(
Do[InjectionContainer](ComplexService{}),
ApSL(complexServiceLenses.service1, lookupService1),
ApSL(complexServiceLenses.service2, lookupService2),
)
// handleResult is a curried function that combines results from two services.
// It demonstrates how to compose the outputs of multiple effects into a final result.
// The curried form allows it to be used with applicative composition (ApS).
func handleResult(s1 string) func(string) string {
return func(s2 string) string {
return fmt.Sprintf("Final Result: %s : %s", s1, s2)
}
}
// TestDependencyLookup demonstrates both simple and complex dependency injection patterns:
//
// Simple Pattern (handle1):
// 1. Create an InjectionContainer with registered services
// 2. Define a handler (handleService1) that depends on a single typed service interface
// 3. Use a lookup method (lookupService1) to resolve the dependency from the container
// 4. Compose the handler with the lookup using LocalThunkK to inject the dependency
//
// Complex Pattern (handleComplex):
// 1. Define a handler (handleComplexService) that depends on a composite service (ComplexService)
// 2. Use applicative composition (lookupComplexService) to build the composite from multiple lookups
// 3. Each sub-dependency is resolved independently and combined using lenses
// 4. LocalThunkK injects the complete composite dependency into the handler
//
// Service Composition:
// - ApS combines the results of handle1 and handleComplex using handleResult
// - This demonstrates how to compose multiple independent effects that share the same container
// - The final result aggregates outputs from both simple and complex dependency patterns
func TestDependencyLookup(t *testing.T) {
// Create the dependency injection container
container := makeSampleInjectionContainer()
// Simple dependency injection: single service lookup
// LocalThunkK transforms the handler to work with the container
handle1 := F.Pipe1(
handleService1(),
LocalThunkK[string](lookupService1),
)
// Complex dependency injection: composite service with multiple dependencies
// lookupComplexService uses applicative composition to build ComplexService
handleComplex := F.Pipe1(
handleComplexService(),
LocalThunkK[string](lookupComplexService),
)
// Compose both services using applicative style
// ApS applies handleResult to combine outputs from handle1 and handleComplex
result := F.Pipe1(
handle1,
ApS(handleResult, handleComplex),
)
// Execute: provide container, then context, then run the IO operation
res := result(container)(t.Context())()
fmt.Println(res)
}

14
v2/effect/monoid.go Normal file
View File

@@ -0,0 +1,14 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/monoid"
)
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.ApplicativeMonoid[C](m)
}
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.AlternativeMonoid[C](m)
}

350
v2/effect/monoid_test.go Normal file
View File

@@ -0,0 +1,350 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/monoid"
"github.com/stretchr/testify/assert"
)
func TestApplicativeMonoid(t *testing.T) {
t.Run("combines successful effects with string monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("Hello")
eff2 := Of[TestContext, string](" ")
eff3 := Of[TestContext, string]("World")
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Hello World", result)
})
t.Run("combines successful effects with int monoid", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
eff1 := Of[TestContext, int](10)
eff2 := Of[TestContext, int](20)
eff3 := Of[TestContext, int](30)
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 60, result)
})
t.Run("returns empty value for empty monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"empty",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "empty", result)
})
t.Run("propagates first error", func(t *testing.T) {
expectedErr := errors.New("first error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
eff1 := Fail[TestContext, string](expectedErr)
eff2 := Of[TestContext, string]("World")
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedErr := errors.New("second error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("Hello")
eff2 := Fail[TestContext, string](expectedErr)
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("combines multiple effects", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a * b },
1,
)
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
effects := []Effect[TestContext, int]{
Of[TestContext, int](2),
Of[TestContext, int](3),
Of[TestContext, int](4),
Of[TestContext, int](5),
}
combined := effectMonoid.Empty()
for _, eff := range effects {
combined = effectMonoid.Concat(combined, eff)
}
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 120, result) // 1 * 2 * 3 * 4 * 5
})
t.Run("works with custom types", func(t *testing.T) {
type Counter struct {
Count int
}
counterMonoid := monoid.MakeMonoid(
func(a, b Counter) Counter {
return Counter{Count: a.Count + b.Count}
},
Counter{Count: 0},
)
effectMonoid := ApplicativeMonoid[TestContext, Counter](counterMonoid)
eff1 := Of[TestContext, Counter](Counter{Count: 5})
eff2 := Of[TestContext, Counter](Counter{Count: 10})
eff3 := Of[TestContext, Counter](Counter{Count: 15})
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Count)
})
}
func TestAlternativeMonoid(t *testing.T) {
t.Run("combines successful effects with monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("First")
eff2 := Of[TestContext, string]("Second")
combined := effectMonoid.Concat(eff1, eff2)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "FirstSecond", result) // Alternative still combines when both succeed
})
t.Run("tries second effect if first fails", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first failed"))
eff2 := Of[TestContext, string]("Second")
combined := effectMonoid.Concat(eff1, eff2)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Second", result)
})
t.Run("returns error if all effects fail", func(t *testing.T) {
expectedErr := errors.New("second error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first error"))
eff2 := Fail[TestContext, string](expectedErr)
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("returns empty value for empty monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"default",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "default", result)
})
t.Run("chains multiple alternatives", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
effectMonoid := AlternativeMonoid[TestContext, int](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Fail[TestContext, int](errors.New("error 2"))
eff3 := Of[TestContext, int](42)
eff4 := Of[TestContext, int](100)
combined := effectMonoid.Concat(
effectMonoid.Concat(eff1, eff2),
effectMonoid.Concat(eff3, eff4),
)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 142, result) // Combines successful values: 42 + 100
})
t.Run("works with custom types", func(t *testing.T) {
type Result struct {
Value string
Code int
}
resultMonoid := monoid.MakeMonoid(
func(a, b Result) Result {
return Result{Value: a.Value + b.Value, Code: a.Code + b.Code}
},
Result{Value: "", Code: 0},
)
effectMonoid := AlternativeMonoid[TestContext, Result](resultMonoid)
eff1 := Fail[TestContext, Result](errors.New("failed"))
eff2 := Of[TestContext, Result](Result{Value: "success", Code: 200})
eff3 := Of[TestContext, Result](Result{Value: "backup", Code: 201})
combined := effectMonoid.Concat(effectMonoid.Concat(eff1, eff2), eff3)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "successbackup", result.Value) // Combines both successful values
assert.Equal(t, 401, result.Code) // 200 + 201
})
}
func TestMonoidComparison(t *testing.T) {
t.Run("ApplicativeMonoid vs AlternativeMonoid with all success", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + "," + b },
"",
)
applicativeMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
alternativeMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("A")
eff2 := Of[TestContext, string]("B")
// Applicative combines values
applicativeResult, err1 := runEffect(
applicativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
// Alternative takes first
alternativeResult, err2 := runEffect(
alternativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, "A,B", applicativeResult) // Combined with comma separator
assert.Equal(t, "A,B", alternativeResult) // Also combined (Alternative uses Alt semigroup)
})
t.Run("ApplicativeMonoid vs AlternativeMonoid with failures", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
applicativeMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
alternativeMonoid := AlternativeMonoid[TestContext, int](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Of[TestContext, int](42)
// Applicative fails on first error
_, err1 := runEffect(
applicativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
// Alternative tries second on first failure
result2, err2 := runEffect(
alternativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
assert.Error(t, err1)
assert.NoError(t, err2)
assert.Equal(t, 42, result2)
})
}

14
v2/effect/retry.go Normal file
View File

@@ -0,0 +1,14 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/retry"
)
func Retrying[C, A any](
policy retry.RetryPolicy,
action Kleisli[C, retry.RetryStatus, A],
check Predicate[Result[A]],
) Effect[C, A] {
return readerreaderioresult.Retrying(policy, action, check)
}

377
v2/effect/retry_test.go Normal file
View File

@@ -0,0 +1,377 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"errors"
"testing"
"time"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
func TestRetrying(t *testing.T) {
t.Run("succeeds on first attempt", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Of[TestContext, string]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "success", result)
assert.Equal(t, 1, attemptCount)
})
t.Run("retries on failure and eventually succeeds", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 3 {
return Fail[TestContext, string](errors.New("temporary error"))
}
return Of[TestContext, string]("success after retries")
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "success after retries", result)
assert.Equal(t, 3, attemptCount)
})
t.Run("exhausts retry limit", func(t *testing.T) {
attemptCount := 0
maxRetries := uint(3)
policy := retry.LimitRetries(maxRetries)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Fail[TestContext, string](errors.New("persistent error"))
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, int(maxRetries+1), attemptCount) // initial attempt + retries
})
t.Run("does not retry on success", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
return Of[TestContext, int](42)
},
func(res Result[int]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 1, attemptCount)
})
t.Run("uses custom retry predicate", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
return Of[TestContext, int](attemptCount * 10)
},
func(res Result[int]) bool {
// Retry if value is less than 30
if result.IsRight(res) {
val, _ := result.Unwrap(res)
return val < 30
}
return true
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result)
assert.Equal(t, 3, attemptCount)
})
t.Run("tracks retry status", func(t *testing.T) {
var statuses []retry.RetryStatus
policy := retry.LimitRetries(3)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
statuses = append(statuses, status)
if len(statuses) < 3 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("done")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "done", result)
assert.Len(t, statuses, 3)
// First attempt has iteration 0
assert.Equal(t, uint(0), statuses[0].IterNumber)
assert.Equal(t, uint(1), statuses[1].IterNumber)
assert.Equal(t, uint(2), statuses[2].IterNumber)
})
t.Run("works with exponential backoff", func(t *testing.T) {
attemptCount := 0
policy := retry.Monoid.Concat(
retry.LimitRetries(3),
retry.ExponentialBackoff(10*time.Millisecond),
)
startTime := time.Now()
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 3 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
elapsed := time.Since(startTime)
assert.NoError(t, err)
assert.Equal(t, "success", result)
assert.Equal(t, 3, attemptCount)
// Should have some delay due to backoff
assert.Greater(t, elapsed, 10*time.Millisecond)
})
t.Run("combines with other effect operations", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Map[TestContext](func(s string) string {
return "mapped: " + s
})(Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "mapped: success", result)
assert.Equal(t, 2, attemptCount)
})
t.Run("retries with different error types", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
errors := []error{
errors.New("error 1"),
errors.New("error 2"),
errors.New("error 3"),
}
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
if attemptCount < len(errors) {
err := errors[attemptCount]
attemptCount++
return Fail[TestContext, string](err)
}
attemptCount++
return Of[TestContext, string]("finally succeeded")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "finally succeeded", result)
assert.Equal(t, 4, attemptCount)
})
t.Run("no retry when predicate returns false", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Fail[TestContext, string](errors.New("error"))
},
func(res Result[string]) bool {
return false // never retry
},
)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, 1, attemptCount) // only initial attempt
})
t.Run("retries with context access", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
ctx := TestContext{Value: "retry-context"}
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success with context")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, ctx)
assert.NoError(t, err)
assert.Equal(t, "success with context", result)
assert.Equal(t, 2, attemptCount)
})
}
func TestRetryingWithComplexScenarios(t *testing.T) {
t.Run("retry with state accumulation", func(t *testing.T) {
type State struct {
Attempts []int
Value string
}
policy := retry.LimitRetries(4)
eff := Retrying[TestContext, State](
policy,
func(status retry.RetryStatus) Effect[TestContext, State] {
state := State{
Attempts: make([]int, status.IterNumber+1),
Value: "attempt",
}
for i := uint(0); i <= status.IterNumber; i++ {
state.Attempts[i] = int(i)
}
if status.IterNumber < 2 {
return Fail[TestContext, State](errors.New("retry"))
}
return Of[TestContext, State](state)
},
func(res Result[State]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "attempt", result.Value)
assert.Equal(t, []int{0, 1, 2}, result.Attempts)
})
t.Run("retry with chain operations", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("final: " + string(rune('0'+x)))
})(Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, int](errors.New("retry"))
}
return Of[TestContext, int](attemptCount)
},
func(res Result[int]) bool {
return result.IsLeft(res)
},
))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Contains(t, result, "final:")
})
}

19
v2/effect/run.go Normal file
View File

@@ -0,0 +1,19 @@
package effect
import (
"context"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
"github.com/IBM/fp-go/v2/result"
)
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
return readerreaderioresult.Read[A](c)
}
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
return func(ctx context.Context) (A, error) {
return result.Unwrap(fa(ctx)())
}
}

326
v2/effect/run_test.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 effect
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestProvide(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
ctx := TestContext{Value: "test-value"}
eff := Of[TestContext, string]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("provides context with specific values", func(t *testing.T) {
type Config struct {
Host string
Port int
}
cfg := Config{Host: "localhost", Port: 8080}
eff := Of[Config, string]("connected")
ioResult := Provide[Config, string](cfg)(eff)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "connected", result)
})
t.Run("propagates errors", func(t *testing.T) {
expectedErr := errors.New("provide error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("works with different context types", func(t *testing.T) {
type SimpleContext struct {
ID int
}
ctx := SimpleContext{ID: 42}
eff := Of[SimpleContext, int](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, result)
})
t.Run("provides context to chained effects", func(t *testing.T) {
ctx := TestContext{Value: "base"}
eff := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("result")
})(Of[TestContext, int](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("provides context to mapped effects", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Map[TestContext](func(x int) string {
return "mapped"
})(Of[TestContext, int](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "mapped", result)
})
}
func TestRunSync(t *testing.T) {
t.Run("runs effect synchronously", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, int](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("runs effect with context.Context", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, string]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
bgCtx := context.Background()
result, err := readerResult(bgCtx)
assert.NoError(t, err)
assert.Equal(t, "hello", result)
})
t.Run("propagates errors synchronously", func(t *testing.T) {
expectedErr := errors.New("sync error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, int](expectedErr)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("runs complex effect chains", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x + 10)
})(Of[TestContext, int](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 30, result) // (5 + 10) * 2
})
t.Run("handles multiple sequential runs", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, int](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
// Run multiple times
result1, err1 := readerResult(context.Background())
result2, err2 := readerResult(context.Background())
result3, err3 := readerResult(context.Background())
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
assert.Equal(t, 42, result1)
assert.Equal(t, 42, result2)
assert.Equal(t, 42, result3)
})
t.Run("works with different result types", func(t *testing.T) {
type User struct {
Name string
Age int
}
ctx := TestContext{Value: "test"}
user := User{Name: "Alice", Age: 30}
eff := Of[TestContext, User](user)
ioResult := Provide[TestContext, User](ctx)(eff)
readerResult := RunSync[User](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, user, result)
})
}
func TestProvideAndRunSyncIntegration(t *testing.T) {
t.Run("complete workflow with success", func(t *testing.T) {
type AppConfig struct {
APIKey string
Timeout int
}
cfg := AppConfig{APIKey: "secret", Timeout: 30}
// Create an effect that uses the config
eff := Of[AppConfig, string]("API call successful")
// Provide config and run
result, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "API call successful", result)
})
t.Run("complete workflow with error", func(t *testing.T) {
type AppConfig struct {
APIKey string
}
expectedErr := errors.New("API error")
cfg := AppConfig{APIKey: "secret"}
eff := Fail[AppConfig, string](expectedErr)
_, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("workflow with transformations", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Map[TestContext](func(x int) string {
return "final"
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(Of[TestContext, int](21)))
result, err := RunSync[string](Provide[TestContext, string](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "final", result)
})
t.Run("workflow with bind operations", func(t *testing.T) {
type State struct {
X int
Y int
}
ctx := TestContext{Value: "test"}
eff := Bind[TestContext](
func(y int) func(State) State {
return func(s State) State {
s.Y = y
return s
}
},
func(s State) Effect[TestContext, int] {
return Of[TestContext, int](s.X * 2)
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext, int](10)))
result, err := RunSync[State](Provide[TestContext, State](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
assert.Equal(t, 20, result.Y)
})
t.Run("workflow with context transformation", func(t *testing.T) {
type OuterCtx struct {
Value string
}
type InnerCtx struct {
Data string
}
outerCtx := OuterCtx{Value: "outer"}
innerEff := Of[InnerCtx, string]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync[string](Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner result", result)
})
t.Run("workflow with array traversal", func(t *testing.T) {
ctx := TestContext{Value: "test"}
input := []int{1, 2, 3, 4, 5}
eff := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(input)
result, err := RunSync[[]int](Provide[TestContext, []int](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
})
}

7
v2/effect/traverse.go Normal file
View File

@@ -0,0 +1,7 @@
package effect
import "github.com/IBM/fp-go/v2/context/readerreaderioresult"
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
return readerreaderioresult.TraverseArray(f)
}

266
v2/effect/traverse_test.go Normal file
View File

@@ -0,0 +1,266 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTraverseArray(t *testing.T) {
t.Run("traverses empty array", func(t *testing.T) {
input := []int{}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("traverses array with single element", func(t *testing.T) {
input := []int{42}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"42"}, result)
})
t.Run("traverses array with multiple elements", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, result)
})
t.Run("transforms to different type", func(t *testing.T) {
input := []string{"hello", "world", "test"}
kleisli := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
return Of[TestContext, int](len(s))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{5, 5, 4}, result)
})
t.Run("stops on first error", func(t *testing.T) {
expectedErr := errors.New("traverse error")
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
if x == 3 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("handles complex transformations", func(t *testing.T) {
type User struct {
ID int
Name string
}
input := []int{1, 2, 3}
kleisli := TraverseArray[TestContext](func(id int) Effect[TestContext, User] {
return Of[TestContext, User](User{
ID: id,
Name: fmt.Sprintf("User%d", id),
})
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Len(t, result, 3)
assert.Equal(t, 1, result[0].ID)
assert.Equal(t, "User1", result[0].Name)
assert.Equal(t, 2, result[1].ID)
assert.Equal(t, "User2", result[1].Name)
assert.Equal(t, 3, result[2].ID)
assert.Equal(t, "User3", result[2].Name)
})
t.Run("chains with other operations", func(t *testing.T) {
input := []int{1, 2, 3}
eff := Chain[TestContext](func(strings []string) Effect[TestContext, int] {
total := 0
for _, s := range strings {
val, _ := strconv.Atoi(s)
total += val
}
return Of[TestContext, int](total)
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x * 2))
})(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 12, result) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
})
t.Run("uses context in transformation", func(t *testing.T) {
input := []int{1, 2, 3}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Chain[TestContext](func(ctx TestContext) Effect[TestContext, string] {
return Of[TestContext, string](fmt.Sprintf("%s-%d", ctx.Value, x))
})(Of[TestContext, TestContext](TestContext{Value: "prefix"}))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"prefix-1", "prefix-2", "prefix-3"}, result)
})
t.Run("preserves order", func(t *testing.T) {
input := []int{5, 3, 8, 1, 9, 2}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 10)
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{50, 30, 80, 10, 90, 20}, result)
})
t.Run("handles large arrays", func(t *testing.T) {
size := 1000
input := make([]int, size)
for i := 0; i < size; i++ {
input[i] = i
}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Len(t, result, size)
assert.Equal(t, 0, result[0])
assert.Equal(t, 1998, result[999])
})
t.Run("composes multiple traversals", func(t *testing.T) {
input := []int{1, 2, 3}
// First traversal: int -> string
kleisli1 := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
// Second traversal: string -> int (length)
kleisli2 := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
return Of[TestContext, int](len(s))
})
eff := Chain[TestContext](kleisli2)(kleisli1(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{1, 1, 1}, result) // All single-digit numbers have length 1
})
t.Run("handles nil array", func(t *testing.T) {
var input []int
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Empty(t, result) // TraverseArray returns empty slice for nil input
})
t.Run("works with Map for post-processing", func(t *testing.T) {
input := []int{1, 2, 3}
eff := Map[TestContext](func(strings []string) string {
result := ""
for _, s := range strings {
result += s + ","
}
return result
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "1,2,3,", result)
})
t.Run("error in middle of array", func(t *testing.T) {
expectedErr := errors.New("middle error")
input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("error at end of array", func(t *testing.T) {
expectedErr := errors.New("end error")
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}

37
v2/effect/types.go Normal file
View File

@@ -0,0 +1,37 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
)
type (
Either[E, A any] = either.Either[E, A]
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
IO[A any] = io.IO[A]
IOEither[E, A any] = ioeither.IOEither[E, A]
Lazy[A any] = lazy.Lazy[A]
IOResult[A any] = ioresult.IOResult[A]
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
Monoid[A any] = monoid.Monoid[A]
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
Thunk[A any] = ReaderIOResult[A]
Predicate[A any] = predicate.Predicate[A]
Result[A any] = result.Result[A]
Lens[S, T any] = lens.Lens[S, T]
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
)

View File

@@ -19,6 +19,31 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// MonadSequenceSegment sequences a segment of an array of effects using a divide-and-conquer approach.
// It recursively splits the array segment in half, sequences each half, and concatenates the results.
//
// This function is optimized for performance by using a divide-and-conquer strategy that reduces
// the depth of nested function calls compared to a linear fold approach.
//
// Type parameters:
// - HKTB: The higher-kinded type containing values (e.g., Option[B], Either[E, B])
// - HKTRB: The higher-kinded type containing an array of values (e.g., Option[[]B], Either[E, []B])
//
// Parameters:
// - fof: Function to lift a single HKTB into HKTRB
// - empty: The empty/identity value for HKTRB
// - concat: Function to concatenate two HKTRB values
// - fbs: The array of effects to sequence
// - start: The starting index of the segment (inclusive)
// - end: The ending index of the segment (exclusive)
//
// Returns:
// - HKTRB: The sequenced result for the segment
//
// The function handles three cases:
// - Empty segment (end - start == 0): returns empty
// - Single element (end - start == 1): returns fof(fbs[start])
// - Multiple elements: recursively divides and conquers
func MonadSequenceSegment[HKTB, HKTRB any](
fof func(HKTB) HKTRB,
empty HKTRB,
@@ -41,6 +66,23 @@ func MonadSequenceSegment[HKTB, HKTRB any](
}
}
// SequenceSegment creates a function that sequences a segment of an array of effects.
// Unlike MonadSequenceSegment, this returns a curried function that can be reused.
//
// This function builds a computation tree at construction time, which can be more efficient
// when the same sequencing pattern needs to be applied multiple times to arrays of the same length.
//
// Type parameters:
// - HKTB: The higher-kinded type containing values
// - HKTRB: The higher-kinded type containing an array of values
//
// Parameters:
// - fof: Function to lift a single HKTB into HKTRB
// - empty: The empty/identity value for HKTRB
// - concat: Function to concatenate two HKTRB values
//
// Returns:
// - A function that takes an array of HKTB and returns HKTRB
func SequenceSegment[HKTB, HKTRB any](
fof func(HKTB) HKTRB,
empty HKTRB,
@@ -85,14 +127,39 @@ func SequenceSegment[HKTB, HKTRB any](
}
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverse maps each element of an array to an effect, then sequences the results.
// This is the monadic version that takes the array as a direct parameter.
//
// Traverse combines mapping and sequencing in one operation. It's useful when you want to
// transform each element of an array into an effect (like Option, Either, IO, etc.) and
// then collect all those effects into a single effect containing an array.
//
// We need to pass the members of the applicative explicitly, because golang does neither
// support higher kinded types nor template methods on structs or interfaces.
//
// Type parameters:
// - GA: The input array type (e.g., []A)
// - GB: The output array type (e.g., []B)
// - A: The input element type
// - B: The output element type
// - HKTB: HKT<B> - The effect containing B (e.g., Option[B])
// - HKTAB: HKT<func(B)GB> - Intermediate applicative type
// - HKTRB: HKT<GB> - The effect containing the result array (e.g., Option[[]B])
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - ta: The input array to traverse
// - f: The function to apply to each element, producing an effect
//
// Returns:
// - HKTRB: An effect containing the array of transformed values
//
// Example:
//
// If any element produces None, the entire result is None.
// If all elements produce Some, the result is Some containing all values.
func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -103,14 +170,20 @@ func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
return MonadTraverseReduce(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverseWithIndex is like MonadTraverse but the transformation function also receives the index.
// This is useful when the transformation depends on the element's position in the array.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - ta: The input array to traverse
// - f: The function to apply to each element with its index, producing an effect
//
// Returns:
// - HKTRB: An effect containing the array of transformed values
func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -121,6 +194,19 @@ func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
}
// Traverse creates a curried function that maps each element to an effect and sequences the results.
// This is the curried version of MonadTraverse, useful for partial application and composition.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - f: The function to apply to each element, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the transformed array
func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -133,6 +219,19 @@ func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
}
}
// TraverseWithIndex creates a curried function like Traverse but with index-aware transformation.
// This is the curried version of MonadTraverseWithIndex.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - f: The function to apply to each element with its index, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the transformed array
func TraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -231,6 +330,16 @@ func TraverseReduce[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
}
}
// TraverseReduceWithIndex creates a curried function for index-aware custom reduction during traversal.
// This is the curried version of MonadTraverseReduceWithIndex.
//
// Type parameters: Same as MonadTraverseReduce
//
// Parameters: Same as TraverseReduce, except:
// - transform: Function that takes index and element, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the accumulated value
func TraverseReduceWithIndex[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,

View File

@@ -1,10 +1,60 @@
// Copyright (c) 2024 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 iter provides functional programming utilities for working with Go 1.23+ iterators.
// It offers operations for reducing, mapping, concatenating, and transforming iterator sequences
// in a functional style, compatible with the range-over-func pattern.
package iter
import (
"slices"
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
)
func From[A any](as ...A) Seq[A] {
return slices.Values(as)
}
// MonadReduceWithIndex reduces an iterator sequence to a single value using a reducer function
// that receives the current index, accumulated value, and current element.
//
// The function iterates through all elements in the sequence, applying the reducer function
// at each step with the element's index. This is useful when the position of elements matters
// in the reduction logic.
//
// Parameters:
// - fa: The iterator sequence to reduce
// - f: The reducer function that takes (index, accumulator, element) and returns the new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - The final accumulated value after processing all elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(10)
// yield(20)
// yield(30)
// }
// // Sum with index multiplier: 0*10 + 1*20 + 2*30 = 80
// result := MonadReduceWithIndex(iter, func(i, acc, val int) int {
// return acc + i*val
// }, 0)
func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(int, B, A) B, initial B) B {
current := initial
var i int
@@ -15,6 +65,29 @@ func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(
return current
}
// MonadReduce reduces an iterator sequence to a single value using a reducer function.
//
// This is similar to MonadReduceWithIndex but without index tracking, making it more
// efficient when the position of elements is not needed in the reduction logic.
//
// Parameters:
// - fa: The iterator sequence to reduce
// - f: The reducer function that takes (accumulator, element) and returns the new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - The final accumulated value after processing all elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// sum := MonadReduce(iter, func(acc, val int) int {
// return acc + val
// }, 0) // Returns: 6
func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B, initial B) B {
current := initial
for a := range fa {
@@ -23,7 +96,30 @@ func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B,
return current
}
// Concat concatenates two sequences, yielding all elements from left followed by all elements from right.
// Concat concatenates two iterator sequences, yielding all elements from left followed by all elements from right.
//
// The resulting iterator will first yield all elements from the left sequence, then all elements
// from the right sequence. If the consumer stops early (yield returns false), iteration stops
// immediately without processing remaining elements.
//
// Parameters:
// - left: The first iterator sequence
// - right: The second iterator sequence
//
// Returns:
// - A new iterator that yields elements from both sequences in order
//
// Example:
//
// left := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// right := func(yield func(int) bool) {
// yield(3)
// yield(4)
// }
// combined := Concat(left, right) // Yields: 1, 2, 3, 4
func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
return func(yield func(T) bool) {
for t := range left {
@@ -39,28 +135,129 @@ func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
}
}
// Of creates an iterator sequence containing a single element.
//
// This is the unit/return operation for the iterator monad, lifting a single value
// into the iterator context.
//
// Parameters:
// - a: The element to wrap in an iterator
//
// Returns:
// - An iterator that yields exactly one element
//
// Example:
//
// iter := Of[func(yield func(int) bool)](42)
// // Yields: 42
func Of[GA ~func(yield func(A) bool), A any](a A) GA {
return func(yield func(A) bool) {
yield(a)
}
}
// MonadAppend appends a single element to the end of an iterator sequence.
//
// This creates a new iterator that yields all elements from the original sequence
// followed by the tail element.
//
// Parameters:
// - f: The original iterator sequence
// - tail: The element to append
//
// Returns:
// - A new iterator with the tail element appended
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := MonadAppend(iter, 3) // Yields: 1, 2, 3
func MonadAppend[GA ~func(yield func(A) bool), A any](f GA, tail A) GA {
return Concat(f, Of[GA](tail))
}
// Append returns a function that appends a single element to the end of an iterator sequence.
//
// This is the curried version of MonadAppend, useful for partial application and composition.
//
// Parameters:
// - tail: The element to append
//
// Returns:
// - A function that takes an iterator and returns a new iterator with the tail element appended
//
// Example:
//
// appendThree := Append[func(yield func(int) bool)](3)
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := appendThree(iter) // Yields: 1, 2, 3
func Append[GA ~func(yield func(A) bool), A any](tail A) func(GA) GA {
return F.Bind2nd(Concat[GA], Of[GA](tail))
}
// Prepend returns a function that prepends a single element to the beginning of an iterator sequence.
//
// This is the curried version for prepending, useful for partial application and composition.
//
// Parameters:
// - head: The element to prepend
//
// Returns:
// - A function that takes an iterator and returns a new iterator with the head element prepended
//
// Example:
//
// prependZero := Prepend[func(yield func(int) bool)](0)
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := prependZero(iter) // Yields: 0, 1, 2
func Prepend[GA ~func(yield func(A) bool), A any](head A) func(GA) GA {
return F.Bind1st(Concat[GA], Of[GA](head))
}
// Empty creates an empty iterator sequence that yields no elements.
//
// This is the identity element for the Concat operation and represents an empty collection
// in the iterator context.
//
// Returns:
// - An iterator that yields no elements
//
// Example:
//
// iter := Empty[func(yield func(int) bool), int]()
// // Yields nothing
func Empty[GA ~func(yield func(A) bool), A any]() GA {
return func(_ func(A) bool) {}
}
// ToArray collects all elements from an iterator sequence into a slice.
//
// This eagerly evaluates the entire iterator sequence and materializes all elements
// into memory as a slice.
//
// Parameters:
// - fa: The iterator sequence to collect
//
// Returns:
// - A slice containing all elements from the iterator
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// arr := ToArray[func(yield func(int) bool), []int](iter) // Returns: []int{1, 2, 3}
func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
bs := make(GB, 0)
for a := range fa {
@@ -69,6 +266,28 @@ func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
return bs
}
// MonadMapToArray maps each element of an iterator sequence through a function and collects the results into a slice.
//
// This combines mapping and collection into a single operation, eagerly evaluating the entire
// iterator sequence and materializing the transformed elements into memory.
//
// Parameters:
// - fa: The iterator sequence to map and collect
// - f: The mapping function to apply to each element
//
// Returns:
// - A slice containing the mapped elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// doubled := MonadMapToArray[func(yield func(int) bool), []int](iter, func(x int) int {
// return x * 2
// }) // Returns: []int{2, 4, 6}
func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(A) B) GB {
bs := make(GB, 0)
for a := range fa {
@@ -77,10 +296,54 @@ func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f f
return bs
}
// MapToArray returns a function that maps each element through a function and collects the results into a slice.
//
// This is the curried version of MonadMapToArray, useful for partial application and composition.
//
// Parameters:
// - f: The mapping function to apply to each element
//
// Returns:
// - A function that takes an iterator and returns a slice of mapped elements
//
// Example:
//
// double := MapToArray[func(yield func(int) bool), []int](func(x int) int {
// return x * 2
// })
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := double(iter) // Returns: []int{2, 4}
func MapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(A) B) func(GA) GB {
return F.Bind2nd(MonadMapToArray[GA, GB], f)
}
// MonadMapToArrayWithIndex maps each element of an iterator sequence through a function that receives
// the element's index, and collects the results into a slice.
//
// This is similar to MonadMapToArray but the mapping function also receives the zero-based index
// of each element, useful when the position matters in the transformation logic.
//
// Parameters:
// - fa: The iterator sequence to map and collect
// - f: The mapping function that takes (index, element) and returns the transformed element
//
// Returns:
// - A slice containing the mapped elements
//
// Example:
//
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// yield("c")
// }
// indexed := MonadMapToArrayWithIndex[func(yield func(string) bool), []string](iter,
// func(i int, s string) string {
// return fmt.Sprintf("%d:%s", i, s)
// }) // Returns: []string{"0:a", "1:b", "2:c"}
func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(int, A) B) GB {
bs := make(GB, 0)
var i int
@@ -91,10 +354,49 @@ func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f
return bs
}
// MapToArrayWithIndex returns a function that maps each element through an indexed function
// and collects the results into a slice.
//
// This is the curried version of MonadMapToArrayWithIndex, useful for partial application and composition.
//
// Parameters:
// - f: The mapping function that takes (index, element) and returns the transformed element
//
// Returns:
// - A function that takes an iterator and returns a slice of mapped elements
//
// Example:
//
// addIndex := MapToArrayWithIndex[func(yield func(string) bool), []string](
// func(i int, s string) string {
// return fmt.Sprintf("%d:%s", i, s)
// })
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// }
// result := addIndex(iter) // Returns: []string{"0:a", "1:b"}
func MapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
return F.Bind2nd(MonadMapToArrayWithIndex[GA, GB], f)
}
// Monoid returns a Monoid instance for iterator sequences.
//
// The monoid uses Concat as the binary operation and Empty as the identity element,
// allowing iterator sequences to be combined in an associative way with a neutral element.
// This enables generic operations that work with any monoid, such as folding a collection
// of iterators into a single iterator.
//
// Returns:
// - A Monoid instance with Concat and Empty operations
//
// Example:
//
// m := Monoid[func(yield func(int) bool), int]()
// iter1 := func(yield func(int) bool) { yield(1); yield(2) }
// iter2 := func(yield func(int) bool) { yield(3); yield(4) }
// combined := m.Concat(iter1, iter2) // Yields: 1, 2, 3, 4
// empty := m.Empty() // Yields nothing
func Monoid[GA ~func(yield func(A) bool), A any]() M.Monoid[GA] {
return M.MakeMonoid(Concat[GA], Empty[GA]())
}

View File

@@ -21,18 +21,50 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
)
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverse traverses an iterator sequence, applying an effectful function to each element
// and collecting the results in an applicative context.
//
// This is a fundamental operation in functional programming that allows you to "turn inside out"
// a structure containing effects. It maps each element through a function that produces an effect,
// then sequences all those effects together while preserving the iterator structure.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The output iterator type ~func(yield func(B) bool)
// - A: The input element type
// - B: The output element type
// - HKT_B: The higher-kinded type representing an effect containing B
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
// - HKT_GB: The higher-kinded type representing an effect containing GB (the result iterator)
//
// Parameters:
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
// - fof_gb: Lifts a GB value into the effect context (pure/of operation)
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
// - fap_gb: Applies an effectful function to an effectful value (ap operation)
// - ta: The input iterator sequence to traverse
// - f: The effectful function to apply to each element
//
// Returns:
// - An effect containing an iterator of transformed elements
//
// Note: We need to pass the applicative operations explicitly because Go doesn't support
// higher-kinded types or template methods on structs/interfaces.
//
// Example (conceptual with Option):
//
// // Traverse an iterator of strings, parsing each as an integer
// // If any parse fails, the whole result is None
// iter := func(yield func(string) bool) {
// yield("1")
// yield("2")
// yield("3")
// }
// result := MonadTraverse(..., iter, parseInt) // Some(iterator of [1,2,3]) or None
func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
fmap_b func(HKT_B, func(B) GB) HKT_GB,
fof_gb func(GB) HKT_GB,
fof_gb OfType[GB, HKT_GB],
fmap_gb func(HKT_GB, func(GB) func(GB) GB) HKT_GB_GB,
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
@@ -54,14 +86,43 @@ func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
}
// Traverse is the curried version of MonadTraverse, returning a function that traverses an iterator.
//
// This version uses type aliases for better readability and is more suitable for partial application
// and function composition. It returns a Kleisli arrow (a function from GA to HKT_GB).
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The output iterator type ~func(yield func(B) bool)
// - A: The input element type
// - B: The output element type
// - HKT_B: The higher-kinded type representing an effect containing B
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
// - HKT_GB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
// - fof_gb: Lifts a GB value into the effect context
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
// - fap_gb: Applies an effectful function to an effectful value
// - f: The effectful function to apply to each element (Kleisli arrow)
//
// Returns:
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// parseInts := Traverse[...](fmap, fof, fmap_gb, fap, parseInt)
// iter := func(yield func(string) bool) { yield("1"); yield("2") }
// result := parseInts(iter) // Effect containing iterator of integers
func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
fmap_b func(func(B) GB) func(HKT_B) HKT_GB,
fmap_b MapType[B, GB, HKT_B, HKT_GB],
fof_gb func(GB) HKT_GB,
fmap_gb func(func(GB) func(GB) GB) func(HKT_GB) HKT_GB_GB,
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
fof_gb OfType[GB, HKT_GB],
fmap_gb MapType[GB, Endomorphism[GB], HKT_GB, HKT_GB_GB],
fap_gb ApType[HKT_GB, HKT_GB, HKT_GB_GB],
f func(A) HKT_B) func(GA) HKT_GB {
f Kleisli[A, HKT_B]) Kleisli[GA, HKT_GB] {
fof := fmap_b(Of[GB])
empty := fof_gb(Empty[GB]())
@@ -69,18 +130,50 @@ func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B,
concat_gb := fmap_gb(cb)
concat := func(first, second HKT_GB) HKT_GB {
return fap_gb(concat_gb(first), second)
return fap_gb(second)(concat_gb(first))
}
return func(ma GA) HKT_GB {
// return INTA.SequenceSegment(fof, empty, concat)(MapToArray[GA, []HKT_B](f)(ma))
hktb := MonadMapToArray[GA, []HKT_B](ma, f)
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
}
return F.Flow2(
MapToArray[GA, []HKT_B](f),
INTA.SequenceSegment(fof, empty, concat),
)
}
// MonadSequence sequences an iterator of effects into an effect containing an iterator.
//
// This is a special case of traverse where the transformation function is the identity.
// It "flips" the nesting of the iterator and effect types, collecting all effects into
// a single effect containing an iterator of values.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(HKTA) bool)
// - HKTA: The higher-kinded type representing an effect containing A
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
//
// Parameters:
// - fof: Lifts an HKTA value into the HKTRA context
// - m: A monoid for combining HKTRA values
// - ta: The input iterator of effects to sequence
//
// Returns:
// - An effect containing an iterator of values
//
// Example (conceptual with Option):
//
// iter := func(yield func(Option[int]) bool) {
// yield(Some(1))
// yield(Some(2))
// yield(Some(3))
// }
// result := MonadSequence(..., iter) // Some(iterator of [1,2,3])
//
// iter2 := func(yield func(Option[int]) bool) {
// yield(Some(1))
// yield(None)
// }
// result2 := MonadSequence(..., iter2) // None
func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
fof func(HKTA) HKTRA,
fof OfType[HKTA, HKTRA],
m M.Monoid[HKTRA],
ta GA) HKTRA {
@@ -90,14 +183,37 @@ func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverseWithIndex traverses an iterator sequence with index tracking, applying an effectful
// function to each element along with its index.
//
// This is similar to MonadTraverse but the transformation function receives both the element's
// zero-based index and the element itself, useful when the position matters in the transformation.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - A: The input element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
//
// Parameters:
// - fof: Lifts an HKTB value into the HKTRB context
// - m: A monoid for combining HKTRB values
// - ta: The input iterator sequence to traverse
// - f: The effectful function that takes (index, element) and returns an effect
//
// Returns:
// - An effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// }
// // Add index prefix to each element
// result := MonadTraverseWithIndex(..., iter, func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// }) // Effect containing iterator of ["0:a", "1:b"]
func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
fof func(HKTB) HKTRB,
m M.Monoid[HKTRB],
@@ -110,8 +226,29 @@ func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
}
// Sequence is the curried version of MonadSequence, returning a function that sequences an iterator of effects.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(HKTA) bool)
// - HKTA: The higher-kinded type representing an effect containing A
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
//
// Parameters:
// - fof: Lifts an HKTA value into the HKTRA context
// - m: A monoid for combining HKTRA values
//
// Returns:
// - A function that takes an iterator of effects and returns an effect containing an iterator
//
// Example (conceptual):
//
// sequenceOptions := Sequence[...](fof, monoid)
// iter := func(yield func(Option[int]) bool) { yield(Some(1)); yield(Some(2)) }
// result := sequenceOptions(iter) // Some(iterator of [1,2])
func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
fof func(HKTA) HKTRA,
fof OfType[HKTA, HKTRA],
m M.Monoid[HKTRA]) func(GA) HKTRA {
return func(ma GA) HKTRA {
@@ -119,6 +256,32 @@ func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
}
}
// TraverseWithIndex is the curried version of MonadTraverseWithIndex, returning a function that
// traverses an iterator with index tracking.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - A: The input element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
//
// Parameters:
// - fof: Lifts an HKTB value into the HKTRB context
// - m: A monoid for combining HKTRB values
// - f: The effectful function that takes (index, element) and returns an effect
//
// Returns:
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// addIndexPrefix := TraverseWithIndex[...](fof, monoid, func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// })
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
// result := addIndexPrefix(iter) // Effect containing iterator of ["0:a", "1:b"]
func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
fof func(HKTB) HKTRB,
m M.Monoid[HKTRB],
@@ -130,6 +293,39 @@ func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
}
}
// MonadTraverseReduce combines traversal with reduction, applying an effectful transformation
// and accumulating results using a reducer function.
//
// This is a more efficient operation when you want to both transform elements through effects
// and reduce them to a single accumulated value, avoiding intermediate collections.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - ta: The input iterator sequence to traverse and reduce
// - transform: The effectful function to apply to each element
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - An effect containing the final accumulated value
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
// // Parse strings to ints and sum them
// result := MonadTraverseReduce(..., iter, parseInt, add, 0)
// // Returns: Some(6) or None if any parse fails
func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -152,6 +348,44 @@ func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HK
}, fof(initial))
}
// MonadTraverseReduceWithIndex combines indexed traversal with reduction, applying an effectful
// transformation that receives element indices and accumulating results using a reducer function.
//
// This is similar to MonadTraverseReduce but the transformation function also receives the
// zero-based index of each element, useful when position matters in the transformation logic.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - ta: The input iterator sequence to traverse and reduce
// - transform: The effectful function that takes (index, element) and returns an effect
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - An effect containing the final accumulated value
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) { yield("a"); yield("b"); yield("c") }
// // Create indexed strings and concatenate
// result := MonadTraverseReduceWithIndex(..., iter,
// func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// },
// func(acc, s string) string { return acc + "," + s },
// "")
// // Returns: Effect containing "0:a,1:b,2:c"
func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -174,6 +408,36 @@ func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB,
}, fof(initial))
}
// TraverseReduce is the curried version of MonadTraverseReduce, returning a function that
// traverses and reduces an iterator.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - transform: The effectful function to apply to each element
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - A function that takes an iterator and returns an effect containing the accumulated value
//
// Example (conceptual):
//
// sumParsedInts := TraverseReduce[...](fof, fmap, fap, parseInt, add, 0)
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
// result := sumParsedInts(iter) // Some(6) or None if any parse fails
func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -188,6 +452,41 @@ func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB a
}
}
// TraverseReduceWithIndex is the curried version of MonadTraverseReduceWithIndex, returning a
// function that traverses and reduces an iterator with index tracking.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - transform: The effectful function that takes (index, element) and returns an effect
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - A function that takes an iterator and returns an effect containing the accumulated value
//
// Example (conceptual):
//
// concatIndexed := TraverseReduceWithIndex[...](fof, fmap, fap,
// func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// },
// func(acc, s string) string { return acc + "," + s },
// "")
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
// result := concatIndexed(iter) // Effect containing "0:a,1:b"
func TraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,

View File

@@ -2,10 +2,23 @@ package iter
import (
I "iter"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
)
type (
// Seq represents Go's standard library iterator type for single values.
// It's an alias for iter.Seq[A] and provides interoperability with Go 1.23+ range-over-func.
Seq[A any] = I.Seq[A]
Endomorphism[A any] = endomorphism.Endomorphism[A]
OfType[A, HKT_A any] = pointed.OfType[A, HKT_A]
MapType[A, B, HKT_A, HKT_B any] = functor.MapType[A, B, HKT_A, HKT_B]
ApType[HKT_A, HKT_B, HKT_AB any] = apply.ApType[HKT_A, HKT_B, HKT_AB]
Kleisli[A, HKT_B any] = func(A) HKT_B
)

View File

@@ -61,18 +61,105 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
)
}
// TraverseIter applies an IO-returning function to each element of an iterator sequence
// and collects the results into an IO of an iterator sequence. Executes in parallel by default.
//
// This function is useful for processing lazy sequences where each element requires an IO operation.
// The resulting iterator is also lazy and will only execute IO operations when iterated.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: A function that takes an element of type A and returns an IO computation producing B
//
// Returns:
// - A function that takes an iterator sequence of A and returns an IO of an iterator sequence of B
//
// Example:
//
// // Fetch user data for each ID in a sequence
// fetchUser := func(id int) io.IO[User] {
// return func() User {
// // Simulate fetching user from database
// return User{ID: id, Name: fmt.Sprintf("User%d", id)}
// }
// }
//
// // Create an iterator of user IDs
// userIDs := func(yield func(int) bool) {
// for _, id := range []int{1, 2, 3, 4, 5} {
// if !yield(id) { return }
// }
// }
//
// // Traverse the iterator, fetching each user
// fetchUsers := io.TraverseIter(fetchUser)
// usersIO := fetchUsers(userIDs)
//
// // Execute the IO to get the iterator of users
// users := usersIO()
// for user := range users {
// fmt.Printf("User: %v\n", user)
// }
func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
return INTI.Traverse[Seq[A]](
Map[B],
Of[Seq[B]],
Map[Seq[B]],
MonadAp[Seq[B]],
Ap[Seq[B]],
f,
)
}
// SequenceIter converts an iterator sequence of IO computations into an IO of an iterator sequence of results.
// All computations are executed in parallel by default when the resulting IO is invoked.
//
// This is a special case of TraverseIter where the transformation function is the identity.
// It "flips" the nesting of the iterator and IO types, executing all IO operations and collecting
// their results into a lazy iterator.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: An iterator sequence where each element is an IO computation
//
// Returns:
// - An IO computation that, when executed, produces an iterator sequence of results
//
// Example:
//
// // Create an iterator of IO operations
// operations := func(yield func(io.IO[int]) bool) {
// yield(func() int { return 1 })
// yield(func() int { return 2 })
// yield(func() int { return 3 })
// }
//
// // Sequence the operations
// resultsIO := io.SequenceIter(operations)
//
// // Execute all IO operations and get the iterator of results
// results := resultsIO()
// for result := range results {
// fmt.Printf("Result: %d\n", result)
// }
//
// Note: The IO operations are executed when resultsIO() is called, not when iterating
// over the results. The resulting iterator is lazy but the computations have already
// been performed.
func SequenceIter[A any](as Seq[IO[A]]) IO[Seq[A]] {
return INTI.MonadSequence(
Map(INTI.Of[Seq[A]]),
ApplicativeMonoid(INTI.Monoid[Seq[A]]()),
as,
)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index.
// Executes in parallel by default.
//

View File

@@ -1,9 +1,12 @@
package io
import (
"fmt"
"slices"
"strings"
"testing"
A "github.com/IBM/fp-go/v2/array"
"github.com/stretchr/testify/assert"
)
@@ -36,3 +39,264 @@ func TestTraverseCustomSlice(t *testing.T) {
assert.Equal(t, res(), []string{"A", "B"})
}
func TestTraverseIter(t *testing.T) {
t.Run("transforms all elements successfully", func(t *testing.T) {
// Create an iterator of strings
input := slices.Values(A.From("hello", "world", "test"))
// Transform each string to uppercase
transform := func(s string) IO[string] {
return Of(strings.ToUpper(s))
}
// Traverse the iterator
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
// Execute the IO and collect results
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Equal(t, []string{"HELLO", "WORLD", "TEST"}, collected)
})
t.Run("works with empty iterator", func(t *testing.T) {
// Create an empty iterator
input := func(yield func(string) bool) {}
transform := func(s string) IO[string] {
return Of(strings.ToUpper(s))
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Empty(t, collected)
})
t.Run("works with single element", func(t *testing.T) {
input := func(yield func(int) bool) {
yield(42)
}
transform := func(n int) IO[int] {
return Of(n * 2)
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{84}, collected)
})
t.Run("preserves order of elements", func(t *testing.T) {
input := func(yield func(int) bool) {
for i := 1; i <= 5; i++ {
if !yield(i) {
return
}
}
}
transform := func(n int) IO[string] {
return Of(fmt.Sprintf("item-%d", n))
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
expected := []string{"item-1", "item-2", "item-3", "item-4", "item-5"}
assert.Equal(t, expected, collected)
})
t.Run("handles complex transformations", func(t *testing.T) {
type User struct {
ID int
Name string
}
input := func(yield func(int) bool) {
for _, id := range []int{1, 2, 3} {
if !yield(id) {
return
}
}
}
transform := func(id int) IO[User] {
return Of(User{ID: id, Name: fmt.Sprintf("User%d", id)})
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []User
for user := range result {
collected = append(collected, user)
}
expected := []User{
{ID: 1, Name: "User1"},
{ID: 2, Name: "User2"},
{ID: 3, Name: "User3"},
}
assert.Equal(t, expected, collected)
})
}
func TestSequenceIter(t *testing.T) {
t.Run("sequences multiple IO operations", func(t *testing.T) {
// Create an iterator of IO operations
input := slices.Values(A.From(Of(1), Of(2), Of(3)))
// Sequence the operations
resultIO := SequenceIter(input)
// Execute and collect results
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{1, 2, 3}, collected)
})
t.Run("works with empty iterator", func(t *testing.T) {
input := slices.Values(A.Empty[IO[string]]())
resultIO := SequenceIter(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Empty(t, collected)
})
t.Run("executes all IO operations", func(t *testing.T) {
// Track execution order
var executed []int
input := func(yield func(IO[int]) bool) {
yield(func() int {
executed = append(executed, 1)
return 10
})
yield(func() int {
executed = append(executed, 2)
return 20
})
yield(func() int {
executed = append(executed, 3)
return 30
})
}
resultIO := SequenceIter(input)
// Before execution, nothing should be executed
assert.Empty(t, executed)
// Execute the IO
result := resultIO()
// Collect results
var collected []int
for n := range result {
collected = append(collected, n)
}
// All operations should have been executed
assert.Equal(t, []int{1, 2, 3}, executed)
assert.Equal(t, []int{10, 20, 30}, collected)
})
t.Run("works with single IO operation", func(t *testing.T) {
input := func(yield func(IO[string]) bool) {
yield(Of("hello"))
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Equal(t, []string{"hello"}, collected)
})
t.Run("preserves order of results", func(t *testing.T) {
input := func(yield func(IO[int]) bool) {
for i := 5; i >= 1; i-- {
n := i // capture loop variable
yield(func() int { return n * 10 })
}
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{50, 40, 30, 20, 10}, collected)
})
t.Run("works with complex types", func(t *testing.T) {
type Result struct {
Value int
Label string
}
input := func(yield func(IO[Result]) bool) {
yield(Of(Result{Value: 1, Label: "first"}))
yield(Of(Result{Value: 2, Label: "second"}))
yield(Of(Result{Value: 3, Label: "third"}))
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []Result
for r := range result {
collected = append(collected, r)
}
expected := []Result{
{Value: 1, Label: "first"},
{Value: 2, Label: "second"},
{Value: 3, Label: "third"},
}
assert.Equal(t, expected, collected)
})
}

54
v2/iterator/iter/last.go Normal file
View File

@@ -0,0 +1,54 @@
package iter
import (
"github.com/IBM/fp-go/v2/option"
)
// Last returns the last element from an [Iterator] wrapped in an [Option].
//
// This function retrieves the last element from the iterator by consuming the entire
// sequence. If the iterator contains at least one element, it returns Some(element).
// If the iterator is empty, it returns None.
//
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
//
// Type Parameters:
// - U: The type of elements in the iterator
//
// Parameters:
// - it: The input iterator to get the last element from
//
// Returns:
// - Option[U]: Some(last element) if the iterator is non-empty, None otherwise
//
// Example with non-empty sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// last := iter.Last(seq)
// // Returns: Some(5)
//
// Example with empty sequence:
//
// seq := iter.Empty[int]()
// last := iter.Last(seq)
// // Returns: None
//
// Example with filtered sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// filtered := iter.Filter(func(x int) bool { return x < 4 })(seq)
// last := iter.Last(filtered)
// // Returns: Some(3)
func Last[U any](it Seq[U]) Option[U] {
var last U
found := false
for last = range it {
found = true
}
if !found {
return option.None[U]()
}
return option.Some(last)
}

View File

@@ -0,0 +1,305 @@
package iter
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestLast test getting the last element from a non-empty sequence
func TestLastSimple(t *testing.T) {
t.Run("returns last element from integer sequence", func(t *testing.T) {
seq := From(1, 2, 3)
last := Last(seq)
assert.Equal(t, O.Of(3), last)
})
t.Run("returns last element from string sequence", func(t *testing.T) {
seq := From("a", "b", "c")
last := Last(seq)
assert.Equal(t, O.Of("c"), last)
})
t.Run("returns last element from single element sequence", func(t *testing.T) {
seq := From(42)
last := Last(seq)
assert.Equal(t, O.Of(42), last)
})
t.Run("returns last element from large sequence", func(t *testing.T) {
seq := From(100, 200, 300, 400, 500)
last := Last(seq)
assert.Equal(t, O.Of(500), last)
})
}
// TestLastEmpty tests getting the last element from an empty sequence
func TestLastEmpty(t *testing.T) {
t.Run("returns None for empty integer sequence", func(t *testing.T) {
seq := Empty[int]()
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
t.Run("returns None for empty string sequence", func(t *testing.T) {
seq := Empty[string]()
last := Last(seq)
assert.Equal(t, O.None[string](), last)
})
t.Run("returns None for empty struct sequence", func(t *testing.T) {
type TestStruct struct {
Value int
}
seq := Empty[TestStruct]()
last := Last(seq)
assert.Equal(t, O.None[TestStruct](), last)
})
t.Run("returns None for empty sequence of functions", func(t *testing.T) {
type TestFunc func(int)
seq := Empty[TestFunc]()
last := Last(seq)
assert.Equal(t, O.None[TestFunc](), last)
})
}
// TestLastWithComplex tests Last with complex types
func TestLastWithComplex(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("returns last person", func(t *testing.T) {
seq := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
)
last := Last(seq)
expected := O.Of(Person{"Charlie", 35})
assert.Equal(t, expected, last)
})
t.Run("returns last pointer", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
seq := From(p1, p2)
last := Last(seq)
assert.Equal(t, O.Of(p2), last)
})
}
func TestLastWithFunctions(t *testing.T) {
t.Run("return function", func(t *testing.T) {
want := "last"
f1 := function.Constant("first")
f2 := function.Constant("last")
seq := From(f1, f2)
getLast := function.Flow2(
Last,
O.Map(funcReader),
)
assert.Equal(t, O.Of(want), getLast(seq))
})
}
func funcReader(f func() string) string {
return f()
}
// TestLastWithChan tests Last with channels
func TestLastWithChan(t *testing.T) {
t.Run("return function", func(t *testing.T) {
want := 30
seq := From(intChan(10),
intChan(20),
intChan(want))
getLast := function.Flow2(
Last,
O.Map(chanReader[int]),
)
assert.Equal(t, O.Of(want), getLast(seq))
})
}
func chanReader[T any](c <-chan T) T {
return <-c
}
func intChan(val int) <-chan int {
ch := make(chan int, 1)
ch <- val
close(ch)
return ch
}
// TestLastWithChainedOperations tests Last with multiple chained operations
func TestLastWithChainedOperations(t *testing.T) {
t.Run("chains filter, map, and last", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, N.MoreThan(5))
mapped := MonadMap(filtered, N.Mul(10))
result := Last(mapped)
assert.Equal(t, O.Of(100), result)
})
t.Run("chains map and filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
filtered := MonadFilter(mapped, N.MoreThan(5))
result := Last(filtered)
assert.Equal(t, O.Of(10), result)
})
}
// TestLastWithReplicate tests Last with replicated values
func TestLastWithReplicate(t *testing.T) {
t.Run("returns last from replicated sequence", func(t *testing.T) {
seq := Replicate(5, 42)
last := Last(seq)
assert.Equal(t, O.Of(42), last)
})
t.Run("returns None from zero replications", func(t *testing.T) {
seq := Replicate(0, 42)
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithMakeBy tests Last with MakeBy
func TestLastWithMakeBy(t *testing.T) {
t.Run("returns last generated element", func(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i * i })
last := Last(seq)
assert.Equal(t, O.Of(16), last)
})
t.Run("returns None for zero elements", func(t *testing.T) {
seq := MakeBy(0, F.Identity[int])
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithPrepend tests Last with Prepend
func TestLastWithPrepend(t *testing.T) {
t.Run("returns last element, not prepended", func(t *testing.T) {
seq := From(2, 3, 4)
prepended := Prepend(1)(seq)
last := Last(prepended)
assert.Equal(t, O.Of(4), last)
})
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
prepended := Prepend(42)(seq)
last := Last(prepended)
assert.Equal(t, O.Of(42), last)
})
}
// TestLastWithAppend tests Last with Append
func TestLastWithAppend(t *testing.T) {
t.Run("returns appended element", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
last := Last(appended)
assert.Equal(t, O.Of(4), last)
})
t.Run("returns appended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
appended := Append(42)(seq)
last := Last(appended)
assert.Equal(t, O.Of(42), last)
})
}
// TestLastWithChain tests Last with Chain (flatMap)
func TestLastWithChain(t *testing.T) {
t.Run("returns last from chained sequence", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
last := Last(chained)
assert.Equal(t, O.Of(30), last)
})
t.Run("returns None when chain produces empty", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return Empty[int]()
})
last := Last(chained)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithFlatten tests Last with Flatten
func TestLastWithFlatten(t *testing.T) {
t.Run("returns last from flattened sequence", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5))
flattened := Flatten(nested)
last := Last(flattened)
assert.Equal(t, O.Of(5), last)
})
t.Run("returns None from empty nested sequence", func(t *testing.T) {
nested := Empty[Seq[int]]()
flattened := Flatten(nested)
last := Last(flattened)
assert.Equal(t, O.None[int](), last)
})
}
// Example tests for documentation
func ExampleLast() {
seq := From(1, 2, 3, 4, 5)
last := Last(seq)
if value, ok := O.Unwrap(last); ok {
fmt.Printf("Last element: %d\n", value)
}
// Output: Last element: 5
}
func ExampleLast_empty() {
seq := Empty[int]()
last := Last(seq)
if _, ok := O.Unwrap(last); !ok {
fmt.Println("Sequence is empty")
}
// Output: Sequence is empty
}
func ExampleLast_functions() {
f1 := function.Constant("first")
f2 := function.Constant("middle")
f3 := function.Constant("last")
seq := From(f1, f2, f3)
last := Last(seq)
if fn, ok := O.Unwrap(last); ok {
result := fn()
fmt.Printf("Last function result: %s\n", result)
}
// Output: Last function result: last
}

340
v2/optics/codec/codecs.go Normal file
View File

@@ -0,0 +1,340 @@
// Copyright (c) 2024 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package codec provides pre-built codec implementations for common types.
// This package includes codecs for URL parsing, date/time formatting, and other
// standard data transformations that require bidirectional encoding/decoding.
//
// The codecs in this package follow functional programming principles and integrate
// with the validation framework to provide type-safe, composable transformations.
package codec
import (
"net/url"
"regexp"
"strconv"
"time"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/reader"
)
// validateFromParser creates a validation function from a parser that may fail.
// It wraps a parser function that returns (A, error) into a Validate[I, A] function
// that integrates with the validation framework.
//
// The returned validation function:
// - Calls the parser with the input value
// - On success: returns a successful validation containing the parsed value
// - On failure: returns a validation failure with the error message and cause
//
// Type Parameters:
// - A: The target type to parse into
// - I: The input type to parse from
//
// Parameters:
// - parser: A function that attempts to parse input I into type A, returning an error on failure
//
// Returns:
// - A Validate[I, A] function that can be used in codec construction
//
// Example:
//
// // Create a validator for parsing integers from strings
// intValidator := validateFromParser(strconv.Atoi)
// // Use in a codec
// intCodec := MakeType("Int", Is[int](), intValidator, strconv.Itoa)
func validateFromParser[A, I any](parser func(I) (A, error)) Validate[I, A] {
return func(i I) Decode[Context, A] {
// Attempt to parse the input value
a, err := parser(i)
if err != nil {
// On error, create a validation failure with the error details
return validation.FailureWithError[A](i, err.Error())(err)
}
// On success, wrap the parsed value in a successful validation
return reader.Of[Context](validation.Success(a))
}
}
// URL creates a bidirectional codec for URL parsing and formatting.
// This codec can parse strings into *url.URL and encode *url.URL back to strings.
//
// The codec:
// - Decodes: Parses a string using url.Parse, validating URL syntax
// - Encodes: Converts a *url.URL to its string representation using String()
// - Validates: Ensures the input string is a valid URL format
//
// Returns:
// - A Type[*url.URL, string, string] codec that handles URL transformations
//
// Example:
//
// urlCodec := URL()
//
// // Decode a string to URL
// validation := urlCodec.Decode("https://example.com/path?query=value")
// // validation is Right(*url.URL{...})
//
// // Encode a URL to string
// u, _ := url.Parse("https://example.com")
// str := urlCodec.Encode(u)
// // str is "https://example.com"
//
// // Invalid URL fails validation
// validation := urlCodec.Decode("not a valid url")
// // validation is Left(ValidationError{...})
func URL() Type[*url.URL, string, string] {
return MakeType(
"URL",
Is[*url.URL](),
validateFromParser(url.Parse),
(*url.URL).String,
)
}
// Date creates a bidirectional codec for date/time parsing and formatting with a specific layout.
// This codec uses Go's time.Parse and time.Format with the provided layout string.
//
// The codec:
// - Decodes: Parses a string into time.Time using the specified layout
// - Encodes: Formats a time.Time back to a string using the same layout
// - Validates: Ensures the input string matches the expected date/time format
//
// Parameters:
// - layout: The time layout string (e.g., "2006-01-02", time.RFC3339)
// See time package documentation for layout format details
//
// Returns:
// - A Type[time.Time, string, string] codec that handles date/time transformations
//
// Example:
//
// // Create a codec for ISO 8601 dates
// dateCodec := Date("2006-01-02")
//
// // Decode a string to time.Time
// validation := dateCodec.Decode("2024-03-15")
// // validation is Right(time.Time{...})
//
// // Encode a time.Time to string
// t := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
// str := dateCodec.Encode(t)
// // str is "2024-03-15"
//
// // Create a codec for RFC3339 timestamps
// timestampCodec := Date(time.RFC3339)
// validation := timestampCodec.Decode("2024-03-15T10:30:00Z")
//
// // Invalid format fails validation
// validation := dateCodec.Decode("15-03-2024")
// // validation is Left(ValidationError{...})
func Date(layout string) Type[time.Time, string, string] {
return MakeType(
"Date",
Is[time.Time](),
validateFromParser(func(s string) (time.Time, error) { return time.Parse(layout, s) }),
F.Bind2nd(time.Time.Format, layout),
)
}
// Regex creates a bidirectional codec for regex pattern matching with capture groups.
// This codec can match strings against a regular expression pattern and extract capture groups,
// then reconstruct the original string from the match data.
//
// The codec uses prism.Match which contains:
// - Before: Text before the match
// - Groups: Capture groups (index 0 is the full match, 1+ are numbered capture groups)
// - After: Text after the match
//
// The codec:
// - Decodes: Attempts to match the regex against the input string
// - Encodes: Reconstructs the original string from a Match structure
// - Validates: Ensures the string matches the regex pattern
//
// Parameters:
// - re: A compiled regular expression pattern
//
// Returns:
// - A Type[prism.Match, string, string] codec that handles regex matching
//
// Example:
//
// // Create a codec for matching numbers in text
// numberRegex := regexp.MustCompile(`\d+`)
// numberCodec := Regex(numberRegex)
//
// // Decode a string with a number
// validation := numberCodec.Decode("Price: 42 dollars")
// // validation is Right(Match{Before: "Price: ", Groups: []string{"42"}, After: " dollars"})
//
// // Encode a Match back to string
// match := prism.Match{Before: "Price: ", Groups: []string{"42"}, After: " dollars"}
// str := numberCodec.Encode(match)
// // str is "Price: 42 dollars"
//
// // Non-matching string fails validation
// validation := numberCodec.Decode("no numbers here")
// // validation is Left(ValidationError{...})
func Regex(re *regexp.Regexp) Type[prism.Match, string, string] {
return FromRefinement(prism.RegexMatcher(re))
}
// RegexNamed creates a bidirectional codec for regex pattern matching with named capture groups.
// This codec can match strings against a regular expression with named groups and extract them
// by name, then reconstruct the original string from the match data.
//
// The codec uses prism.NamedMatch which contains:
// - Before: Text before the match
// - Groups: Map of named capture groups (name -> matched text)
// - Full: The complete matched text
// - After: Text after the match
//
// The codec:
// - Decodes: Attempts to match the regex against the input string
// - Encodes: Reconstructs the original string from a NamedMatch structure
// - Validates: Ensures the string matches the regex pattern with named groups
//
// Parameters:
// - re: A compiled regular expression with named capture groups (e.g., `(?P<name>pattern)`)
//
// Returns:
// - A Type[prism.NamedMatch, string, string] codec that handles named regex matching
//
// Example:
//
// // Create a codec for matching email addresses with named groups
// emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
// emailCodec := RegexNamed(emailRegex)
//
// // Decode an email string
// validation := emailCodec.Decode("john@example.com")
// // validation is Right(NamedMatch{
// // Before: "",
// // Groups: map[string]string{"user": "john", "domain": "example.com"},
// // Full: "john@example.com",
// // After: ""
// // })
//
// // Encode a NamedMatch back to string
// match := prism.NamedMatch{
// Before: "",
// Groups: map[string]string{"user": "john", "domain": "example.com"},
// Full: "john@example.com",
// After: "",
// }
// str := emailCodec.Encode(match)
// // str is "john@example.com"
//
// // Non-matching string fails validation
// validation := emailCodec.Decode("not-an-email")
// // validation is Left(ValidationError{...})
func RegexNamed(re *regexp.Regexp) Type[prism.NamedMatch, string, string] {
return FromRefinement(prism.RegexNamedMatcher(re))
}
// IntFromString creates a bidirectional codec for parsing integers from strings.
// This codec converts string representations of integers to int values and vice versa.
//
// The codec:
// - Decodes: Parses a string to an int using strconv.Atoi
// - Encodes: Converts an int to its string representation using strconv.Itoa
// - Validates: Ensures the string contains a valid integer (base 10)
//
// The codec accepts integers in base 10 format, with optional leading sign (+/-).
// It does not accept hexadecimal, octal, or other number formats.
//
// Returns:
// - A Type[int, string, string] codec that handles int/string conversions
//
// Example:
//
// intCodec := IntFromString()
//
// // Decode a valid integer string
// validation := intCodec.Decode("42")
// // validation is Right(42)
//
// // Decode negative integer
// validation := intCodec.Decode("-123")
// // validation is Right(-123)
//
// // Encode an integer to string
// str := intCodec.Encode(42)
// // str is "42"
//
// // Invalid integer string fails validation
// validation := intCodec.Decode("not a number")
// // validation is Left(ValidationError{...})
//
// // Floating point fails validation
// validation := intCodec.Decode("3.14")
// // validation is Left(ValidationError{...})
func IntFromString() Type[int, string, string] {
return MakeType(
"IntFromString",
Is[int](),
validateFromParser(strconv.Atoi),
strconv.Itoa,
)
}
// Int64FromString creates a bidirectional codec for parsing 64-bit integers from strings.
// This codec converts string representations of integers to int64 values and vice versa.
//
// The codec:
// - Decodes: Parses a string to an int64 using strconv.ParseInt with base 10
// - Encodes: Converts an int64 to its string representation
// - Validates: Ensures the string contains a valid 64-bit integer (base 10)
//
// The codec accepts integers in base 10 format, with optional leading sign (+/-).
// It supports the full range of int64 values (-9223372036854775808 to 9223372036854775807).
//
// Returns:
// - A Type[int64, string, string] codec that handles int64/string conversions
//
// Example:
//
// int64Codec := Int64FromString()
//
// // Decode a valid integer string
// validation := int64Codec.Decode("9223372036854775807")
// // validation is Right(9223372036854775807)
//
// // Decode negative integer
// validation := int64Codec.Decode("-9223372036854775808")
// // validation is Right(-9223372036854775808)
//
// // Encode an int64 to string
// str := int64Codec.Encode(42)
// // str is "42"
//
// // Invalid integer string fails validation
// validation := int64Codec.Decode("not a number")
// // validation is Left(ValidationError{...})
//
// // Out of range value fails validation
// validation := int64Codec.Decode("9223372036854775808")
// // validation is Left(ValidationError{...})
func Int64FromString() Type[int64, string, string] {
return MakeType(
"Int64FromString",
Is[int64](),
validateFromParser(func(s string) (int64, error) { return strconv.ParseInt(s, 10, 64) }),
prism.ParseInt64().ReverseGet,
)
}

View File

@@ -0,0 +1,908 @@
// Copyright (c) 2024 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package codec
import (
"net/url"
"regexp"
"testing"
"time"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestURL(t *testing.T) {
urlCodec := URL()
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, *url.URL](nil))
t.Run("decodes valid HTTP URL", func(t *testing.T) {
result := urlCodec.Decode("https://example.com/path?query=value")
assert.True(t, either.IsRight(result), "should successfully decode valid URL")
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "https", parsedURL.Scheme)
assert.Equal(t, "example.com", parsedURL.Host)
assert.Equal(t, "/path", parsedURL.Path)
assert.Equal(t, "query=value", parsedURL.RawQuery)
})
t.Run("decodes valid HTTP URL without path", func(t *testing.T) {
result := urlCodec.Decode("https://example.com")
assert.True(t, either.IsRight(result))
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "https", parsedURL.Scheme)
assert.Equal(t, "example.com", parsedURL.Host)
})
t.Run("decodes URL with port", func(t *testing.T) {
result := urlCodec.Decode("http://localhost:8080/api")
assert.True(t, either.IsRight(result))
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "http", parsedURL.Scheme)
assert.Equal(t, "localhost:8080", parsedURL.Host)
assert.Equal(t, "/api", parsedURL.Path)
})
t.Run("decodes URL with fragment", func(t *testing.T) {
result := urlCodec.Decode("https://example.com/page#section")
assert.True(t, either.IsRight(result))
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "section", parsedURL.Fragment)
})
t.Run("decodes relative URL", func(t *testing.T) {
result := urlCodec.Decode("/path/to/resource")
assert.True(t, either.IsRight(result))
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "/path/to/resource", parsedURL.Path)
})
t.Run("fails to decode invalid URL", func(t *testing.T) {
result := urlCodec.Decode("not a valid url ://")
assert.True(t, either.IsLeft(result), "should fail to decode invalid URL")
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(*url.URL) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("fails to decode URL with invalid characters", func(t *testing.T) {
result := urlCodec.Decode("http://example.com/path with spaces")
// Note: url.Parse actually handles spaces, so let's test a truly invalid URL
result = urlCodec.Decode("ht!tp://invalid")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes URL to string", func(t *testing.T) {
parsedURL, err := url.Parse("https://example.com/path?query=value")
require.NoError(t, err)
encoded := urlCodec.Encode(parsedURL)
assert.Equal(t, "https://example.com/path?query=value", encoded)
})
t.Run("encodes URL with fragment", func(t *testing.T) {
parsedURL, err := url.Parse("https://example.com/page#section")
require.NoError(t, err)
encoded := urlCodec.Encode(parsedURL)
assert.Equal(t, "https://example.com/page#section", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "https://example.com/path?key=value&foo=bar#fragment"
// Decode
decodeResult := urlCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
parsedURL := getOrElseNull(decodeResult)
// Encode
encoded := urlCodec.Encode(parsedURL)
assert.Equal(t, original, encoded)
})
t.Run("codec has correct name", func(t *testing.T) {
assert.Equal(t, "URL", urlCodec.Name())
})
}
func TestDate(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, time.Time](time.Time{}))
t.Run("ISO 8601 date format", func(t *testing.T) {
dateCodec := Date("2006-01-02")
t.Run("decodes valid date", func(t *testing.T) {
result := dateCodec.Decode("2024-03-15")
assert.True(t, either.IsRight(result))
parsedDate := getOrElseNull(result)
assert.Equal(t, 2024, parsedDate.Year())
assert.Equal(t, time.March, parsedDate.Month())
assert.Equal(t, 15, parsedDate.Day())
})
t.Run("fails to decode invalid date format", func(t *testing.T) {
result := dateCodec.Decode("15-03-2024")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(time.Time) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("fails to decode invalid date", func(t *testing.T) {
result := dateCodec.Decode("2024-13-45")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode non-date string", func(t *testing.T) {
result := dateCodec.Decode("not a date")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes date to string", func(t *testing.T) {
date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
encoded := dateCodec.Encode(date)
assert.Equal(t, "2024-03-15", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "2024-12-25"
// Decode
decodeResult := dateCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
parsedDate := getOrElseNull(decodeResult)
// Encode
encoded := dateCodec.Encode(parsedDate)
assert.Equal(t, original, encoded)
})
})
t.Run("RFC3339 timestamp format", func(t *testing.T) {
timestampCodec := Date(time.RFC3339)
t.Run("decodes valid RFC3339 timestamp", func(t *testing.T) {
result := timestampCodec.Decode("2024-03-15T10:30:00Z")
assert.True(t, either.IsRight(result))
parsedTime := getOrElseNull(result)
assert.Equal(t, 2024, parsedTime.Year())
assert.Equal(t, time.March, parsedTime.Month())
assert.Equal(t, 15, parsedTime.Day())
assert.Equal(t, 10, parsedTime.Hour())
assert.Equal(t, 30, parsedTime.Minute())
assert.Equal(t, 0, parsedTime.Second())
})
t.Run("decodes RFC3339 with timezone offset", func(t *testing.T) {
result := timestampCodec.Decode("2024-03-15T10:30:00+01:00")
assert.True(t, either.IsRight(result))
parsedTime := getOrElseNull(result)
assert.Equal(t, 2024, parsedTime.Year())
assert.Equal(t, time.March, parsedTime.Month())
assert.Equal(t, 15, parsedTime.Day())
})
t.Run("fails to decode invalid RFC3339", func(t *testing.T) {
result := timestampCodec.Decode("2024-03-15 10:30:00")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes timestamp to RFC3339 string", func(t *testing.T) {
timestamp := time.Date(2024, 3, 15, 10, 30, 0, 0, time.UTC)
encoded := timestampCodec.Encode(timestamp)
assert.Equal(t, "2024-03-15T10:30:00Z", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "2024-12-25T15:45:30Z"
// Decode
decodeResult := timestampCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
parsedTime := getOrElseNull(decodeResult)
// Encode
encoded := timestampCodec.Encode(parsedTime)
assert.Equal(t, original, encoded)
})
})
t.Run("custom date format", func(t *testing.T) {
customCodec := Date("02/01/2006")
t.Run("decodes custom format", func(t *testing.T) {
result := customCodec.Decode("15/03/2024")
assert.True(t, either.IsRight(result))
parsedDate := getOrElseNull(result)
assert.Equal(t, 2024, parsedDate.Year())
assert.Equal(t, time.March, parsedDate.Month())
assert.Equal(t, 15, parsedDate.Day())
})
t.Run("encodes to custom format", func(t *testing.T) {
date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
encoded := customCodec.Encode(date)
assert.Equal(t, "15/03/2024", encoded)
})
})
t.Run("codec has correct name", func(t *testing.T) {
dateCodec := Date("2006-01-02")
assert.Equal(t, "Date", dateCodec.Name())
})
}
func TestValidateFromParser(t *testing.T) {
t.Run("successful parsing", func(t *testing.T) {
// Create a simple parser that always succeeds
parser := func(s string) (int, error) {
return 42, nil
}
validator := validateFromParser(parser)
decode := validator("test")
// Execute with empty context
result := decode(validation.Context{})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("failed parsing", func(t *testing.T) {
// Create a parser that always fails
parser := func(s string) (int, error) {
return 0, assert.AnError
}
validator := validateFromParser(parser)
decode := validator("test")
// Execute with empty context
result := decode(validation.Context{})
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
// Check that the error contains the input value
if len(errors) > 0 {
assert.Equal(t, "test", errors[0].Value)
}
})
t.Run("parser with context", func(t *testing.T) {
parser := func(s string) (string, error) {
if s == "" {
return "", assert.AnError
}
return s, nil
}
validator := validateFromParser(parser)
// Test with context
ctx := validation.Context{
{Key: "field", Type: "string"},
}
decode := validator("")
result := decode(ctx)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(string) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
// Verify context is preserved
if len(errors) > 0 {
assert.Equal(t, ctx, errors[0].Context)
}
})
}
func TestRegex(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](prism.Match{}))
t.Run("simple number pattern", func(t *testing.T) {
numberRegex := regexp.MustCompile(`\d+`)
regexCodec := Regex(numberRegex)
t.Run("decodes string with number", func(t *testing.T) {
result := regexCodec.Decode("Price: 42 dollars")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "Price: ", match.Before)
assert.Equal(t, []string{"42"}, match.Groups)
assert.Equal(t, " dollars", match.After)
})
t.Run("decodes number at start", func(t *testing.T) {
result := regexCodec.Decode("123 items")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, []string{"123"}, match.Groups)
assert.Equal(t, " items", match.After)
})
t.Run("decodes number at end", func(t *testing.T) {
result := regexCodec.Decode("Total: 999")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "Total: ", match.Before)
assert.Equal(t, []string{"999"}, match.Groups)
assert.Equal(t, "", match.After)
})
t.Run("fails to decode string without number", func(t *testing.T) {
result := regexCodec.Decode("no numbers here")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(prism.Match) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("encodes Match to string", func(t *testing.T) {
match := prism.Match{
Before: "Price: ",
Groups: []string{"42"},
After: " dollars",
}
encoded := regexCodec.Encode(match)
assert.Equal(t, "Price: 42 dollars", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "Count: 789 items"
// Decode
decodeResult := regexCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
match := getOrElseNull(decodeResult)
// Encode
encoded := regexCodec.Encode(match)
assert.Equal(t, original, encoded)
})
})
t.Run("pattern with capture groups", func(t *testing.T) {
// Pattern to match word followed by number
wordNumberRegex := regexp.MustCompile(`(\w+)(\d+)`)
regexCodec := Regex(wordNumberRegex)
t.Run("decodes with capture groups", func(t *testing.T) {
result := regexCodec.Decode("item42")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "", match.Before)
// Groups contains the full match and capture groups
require.NotEmpty(t, match.Groups)
assert.Equal(t, "item42", match.Groups[0])
// Verify we have capture groups
if len(match.Groups) > 1 {
assert.Contains(t, match.Groups[1], "item")
assert.Contains(t, match.Groups[len(match.Groups)-1], "2")
}
assert.Equal(t, "", match.After)
})
})
t.Run("codec name contains pattern info", func(t *testing.T) {
numberRegex := regexp.MustCompile(`\d+`)
regexCodec := Regex(numberRegex)
// The name is generated by FromRefinement and includes the pattern
assert.Contains(t, regexCodec.Name(), "FromRefinement")
})
}
func TestRegexNamed(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](prism.NamedMatch{}))
t.Run("email pattern with named groups", func(t *testing.T) {
emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
emailCodec := RegexNamed(emailRegex)
t.Run("decodes valid email", func(t *testing.T) {
result := emailCodec.Decode("john@example.com")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, "john@example.com", match.Full)
assert.Equal(t, "", match.After)
require.NotNil(t, match.Groups)
assert.Equal(t, "john", match.Groups["user"])
assert.Equal(t, "example.com", match.Groups["domain"])
})
t.Run("decodes email with surrounding text", func(t *testing.T) {
result := emailCodec.Decode("Contact: alice@test.org for info")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "Contact: ", match.Before)
assert.Equal(t, "alice@test.org", match.Full)
assert.Equal(t, " for info", match.After)
assert.Equal(t, "alice", match.Groups["user"])
assert.Equal(t, "test.org", match.Groups["domain"])
})
t.Run("fails to decode invalid email", func(t *testing.T) {
result := emailCodec.Decode("not-an-email")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(prism.NamedMatch) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("encodes NamedMatch to string", func(t *testing.T) {
match := prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "bob", "domain": "example.com"},
Full: "bob@example.com",
After: "",
}
encoded := emailCodec.Encode(match)
assert.Equal(t, "Email: bob@example.com", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "Contact: support@company.io"
// Decode
decodeResult := emailCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
match := getOrElseNull(decodeResult)
// Encode
encoded := emailCodec.Encode(match)
assert.Equal(t, original, encoded)
})
})
t.Run("phone pattern with named groups", func(t *testing.T) {
phoneRegex := regexp.MustCompile(`(?P<area>\d{3})-(?P<prefix>\d{3})-(?P<line>\d{4})`)
phoneCodec := RegexNamed(phoneRegex)
t.Run("decodes valid phone number", func(t *testing.T) {
result := phoneCodec.Decode("555-123-4567")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "555-123-4567", match.Full)
assert.Equal(t, "555", match.Groups["area"])
assert.Equal(t, "123", match.Groups["prefix"])
assert.Equal(t, "4567", match.Groups["line"])
})
t.Run("fails to decode invalid phone format", func(t *testing.T) {
result := phoneCodec.Decode("123-45-6789")
assert.True(t, either.IsLeft(result))
})
})
t.Run("codec name contains refinement info", func(t *testing.T) {
emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
emailCodec := RegexNamed(emailRegex)
// The name is generated by FromRefinement
assert.Contains(t, emailCodec.Name(), "FromRefinement")
})
}
func TestIntFromString(t *testing.T) {
intCodec := IntFromString()
t.Run("decodes positive integer", func(t *testing.T) {
result := intCodec.Decode("42")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("decodes negative integer", func(t *testing.T) {
result := intCodec.Decode("-123")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, -123, value)
})
t.Run("decodes zero", func(t *testing.T) {
result := intCodec.Decode("0")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("decodes integer with plus sign", func(t *testing.T) {
result := intCodec.Decode("+456")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 456, value)
})
t.Run("fails to decode floating point", func(t *testing.T) {
result := intCodec.Decode("3.14")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("fails to decode non-numeric string", func(t *testing.T) {
result := intCodec.Decode("not a number")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode empty string", func(t *testing.T) {
result := intCodec.Decode("")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode hexadecimal", func(t *testing.T) {
result := intCodec.Decode("0xFF")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode with whitespace", func(t *testing.T) {
result := intCodec.Decode(" 42 ")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes positive integer", func(t *testing.T) {
encoded := intCodec.Encode(42)
assert.Equal(t, "42", encoded)
})
t.Run("encodes negative integer", func(t *testing.T) {
encoded := intCodec.Encode(-123)
assert.Equal(t, "-123", encoded)
})
t.Run("encodes zero", func(t *testing.T) {
encoded := intCodec.Encode(0)
assert.Equal(t, "0", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "9876"
// Decode
decodeResult := intCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
value := either.MonadFold(decodeResult,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
// Encode
encoded := intCodec.Encode(value)
assert.Equal(t, original, encoded)
})
t.Run("codec has correct name", func(t *testing.T) {
assert.Equal(t, "IntFromString", intCodec.Name())
})
}
func TestInt64FromString(t *testing.T) {
int64Codec := Int64FromString()
t.Run("decodes positive int64", func(t *testing.T) {
result := int64Codec.Decode("9223372036854775807")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int64 { return 0 },
F.Identity[int64],
)
assert.Equal(t, int64(9223372036854775807), value)
})
t.Run("decodes negative int64", func(t *testing.T) {
result := int64Codec.Decode("-9223372036854775808")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int64 { return 0 },
F.Identity[int64],
)
assert.Equal(t, int64(-9223372036854775808), value)
})
t.Run("decodes zero", func(t *testing.T) {
result := int64Codec.Decode("0")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int64 { return -1 },
F.Identity[int64],
)
assert.Equal(t, int64(0), value)
})
t.Run("decodes small int64", func(t *testing.T) {
result := int64Codec.Decode("42")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int64 { return 0 },
F.Identity[int64],
)
assert.Equal(t, int64(42), value)
})
t.Run("fails to decode out of range positive", func(t *testing.T) {
result := int64Codec.Decode("9223372036854775808")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int64) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("fails to decode out of range negative", func(t *testing.T) {
result := int64Codec.Decode("-9223372036854775809")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode floating point", func(t *testing.T) {
result := int64Codec.Decode("3.14")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode non-numeric string", func(t *testing.T) {
result := int64Codec.Decode("not a number")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode empty string", func(t *testing.T) {
result := int64Codec.Decode("")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes positive int64", func(t *testing.T) {
encoded := int64Codec.Encode(9223372036854775807)
assert.Equal(t, "9223372036854775807", encoded)
})
t.Run("encodes negative int64", func(t *testing.T) {
encoded := int64Codec.Encode(-9223372036854775808)
assert.Equal(t, "-9223372036854775808", encoded)
})
t.Run("encodes zero", func(t *testing.T) {
encoded := int64Codec.Encode(0)
assert.Equal(t, "0", encoded)
})
t.Run("encodes small int64", func(t *testing.T) {
encoded := int64Codec.Encode(42)
assert.Equal(t, "42", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "1234567890123456"
// Decode
decodeResult := int64Codec.Decode(original)
require.True(t, either.IsRight(decodeResult))
value := either.MonadFold(decodeResult,
func(validation.Errors) int64 { return 0 },
F.Identity[int64],
)
// Encode
encoded := int64Codec.Encode(value)
assert.Equal(t, original, encoded)
})
t.Run("codec has correct name", func(t *testing.T) {
assert.Equal(t, "Int64FromString", int64Codec.Name())
})
}

264
v2/optics/lenses/doc.go Normal file
View File

@@ -0,0 +1,264 @@
// 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 lenses provides pre-built lens and prism implementations for common data structures.
//
// This package offers ready-to-use optics (lenses and prisms) for working with regex match
// structures and URL components in a functional programming style. Lenses enable immutable
// updates to nested data structures, while prisms provide safe optional access to fields.
//
// # Overview
//
// The package includes optics for:
// - Match structures: For working with regex match results (indexed capture groups)
// - NamedMatch structures: For working with regex matches with named capture groups
// - url.Userinfo: For working with URL authentication information
//
// Each structure has three variants of optics:
// - Value lenses: Work with value types (immutable updates)
// - Reference lenses: Work with pointer types (mutable updates)
// - Prisms: Provide optional access treating zero values as None
//
// # Lenses vs Prisms
//
// Lenses provide guaranteed access to a field within a structure:
// - Get: Extract a field value
// - Set: Update a field value (returns new structure for values, mutates for pointers)
//
// Prisms provide optional access to fields that may not be present:
// - GetOption: Try to extract a field, returning Option[T]
// - ReverseGet: Construct a structure from a field value
//
// # Match Structures
//
// Match represents a regex match with indexed capture groups:
//
// type Match struct {
// Before string // Text before the match
// Groups []string // Capture groups (index 0 is full match)
// After string // Text after the match
// }
//
// NamedMatch represents a regex match with named capture groups:
//
// type NamedMatch struct {
// Before string // Text before the match
// Groups map[string]string // Named capture groups
// Full string // Full matched text
// After string // Text after the match
// }
//
// # Usage Examples
//
// Working with Match (value-based):
//
// lenses := MakeMatchLenses()
// match := Match{
// Before: "Hello ",
// Groups: []string{"world", "world"},
// After: "!",
// }
//
// // Get a field
// before := lenses.Before.Get(match) // "Hello "
//
// // Update a field (returns new Match)
// updated := lenses.Before.Set(match, "Hi ")
// // updated.Before is "Hi ", original match unchanged
//
// // Use optional lens (treats empty string as None)
// emptyMatch := Match{Before: "", Groups: []string{}, After: ""}
// beforeOpt := lenses.BeforeO.GetOption(emptyMatch) // None
//
// Working with Match (reference-based):
//
// lenses := MakeMatchRefLenses()
// match := &Match{
// Before: "Hello ",
// Groups: []string{"world"},
// After: "!",
// }
//
// // Get a field
// before := lenses.Before.Get(match) // "Hello "
//
// // Update a field (mutates the pointer)
// lenses.Before.Set(match, "Hi ")
// // match.Before is now "Hi "
//
// // Use prism for optional access
// afterOpt := lenses.AfterP.GetOption(match) // Some("!")
//
// Working with NamedMatch:
//
// lenses := MakeNamedMatchLenses()
// match := NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{
// "user": "john",
// "domain": "example.com",
// },
// Full: "john@example.com",
// After: "",
// }
//
// // Get field values
// full := lenses.Full.Get(match) // "john@example.com"
// groups := lenses.Groups.Get(match) // map with user and domain
//
// // Update a field
// updated := lenses.Before.Set(match, "Contact: ")
//
// // Use optional lens
// afterOpt := lenses.AfterO.GetOption(match) // None (empty string)
//
// Working with url.Userinfo:
//
// lenses := MakeUserinfoRefLenses()
// userinfo := url.UserPassword("john", "secret123")
//
// // Get username
// username := lenses.Username.Get(userinfo) // "john"
//
// // Update password (returns new Userinfo)
// updated := lenses.Password.Set(userinfo, "newpass")
//
// // Use optional lens for password
// pwdOpt := lenses.PasswordO.GetOption(userinfo) // Some("secret123")
//
// // Handle userinfo without password
// userOnly := url.User("alice")
//
// Working with url.URL:
//
// lenses := MakeURLLenses()
// u := url.URL{
// Scheme: "https",
// Host: "example.com",
// Path: "/api/v1/users",
// }
//
// // Get field values
// scheme := lenses.Scheme.Get(u) // "https"
// host := lenses.Host.Get(u) // "example.com"
//
// // Update fields (returns new URL)
// updated := lenses.Path.Set("/api/v2/users")(u)
// // updated.Path is "/api/v2/users", original u unchanged
//
// // Use optional lens for query string
// queryOpt := lenses.RawQueryO.Get(u) // None (no query string)
//
// // Set query string
// withQuery := lenses.RawQuery.Set("page=1&limit=10")(u)
//
// Working with url.Error:
//
// lenses := MakeErrorLenses()
// urlErr := url.Error{
// Op: "Get",
// URL: "https://example.com",
// Err: errors.New("connection timeout"),
// }
//
// // Get field values
// op := lenses.Op.Get(urlErr) // "Get"
// urlStr := lenses.URL.Get(urlErr) // "https://example.com"
// err := lenses.Err.Get(urlErr) // error: "connection timeout"
//
// // Update fields (returns new Error)
// updated := lenses.Op.Set("Post")(urlErr)
// // updated.Op is "Post", original urlErr unchanged
// pwdOpt = lenses.PasswordO.GetOption(userOnly) // None
//
// # Composing Optics
//
// Lenses and prisms can be composed to access nested structures:
//
// // Compose lenses to access nested fields
// outerLens := MakeSomeLens()
// innerLens := MakeSomeOtherLens()
// composed := lens.Compose(outerLens, innerLens)
//
// // Compose prisms for optional nested access
// outerPrism := MakeSomePrism()
// innerPrism := MakeSomeOtherPrism()
// composed := prism.Compose(outerPrism, innerPrism)
//
// # Optional Lenses
//
// Optional lenses (suffixed with 'O') treat zero values as None:
// - Empty strings become None
// - Zero values of other types become None
// - Non-zero values become Some(value)
//
// This is useful for distinguishing between "field not set" and "field set to zero value":
//
// lenses := MakeMatchLenses()
// match := Match{Before: "", Groups: []string{"test"}, After: "!"}
//
// // Regular lens returns empty string
// before := lenses.Before.Get(match) // ""
//
// // Optional lens returns None
// beforeOpt := lenses.BeforeO.GetOption(match) // None
//
// // Setting None clears the field
// cleared := lenses.BeforeO.Set(match, option.None[string]())
// // cleared.Before is ""
//
// // Setting Some updates the field
// updated := lenses.BeforeO.Set(match, option.Some("prefix "))
// // updated.Before is "prefix "
//
// # Code Generation
//
// This package uses code generation for creating lens implementations.
// The generate directive at the top of this file triggers the lens generator:
//
// //go:generate go run ../../main.go lens --dir . --filename gen_lens.go
//
// To regenerate lenses after modifying structures, run:
//
// go generate ./optics/lenses
//
// # Performance Considerations
//
// Value-based lenses (MatchLenses, NamedMatchLenses):
// - Create new structures on each Set operation
// - Safe for concurrent use (immutable)
// - Suitable for functional programming patterns
//
// Reference-based lenses (MatchRefLenses, NamedMatchRefLenses):
// - Mutate existing structures
// - More efficient for repeated updates
// - Require careful handling in concurrent contexts
//
// # Related Packages
//
// - github.com/IBM/fp-go/v2/optics/lens: Core lens functionality
// - github.com/IBM/fp-go/v2/optics/prism: Core prism functionality
// - github.com/IBM/fp-go/v2/optics/iso: Isomorphisms for type conversions
// - github.com/IBM/fp-go/v2/option: Option type for optional values
//
// # See Also
//
// For more information on functional optics:
// - Lens laws: https://github.com/IBM/fp-go/blob/main/optics/lens/README.md
// - Prism laws: https://github.com/IBM/fp-go/blob/main/optics/prism/README.md
// - Optics tutorial: https://github.com/IBM/fp-go/blob/main/docs/optics.md
package lenses
//go:generate go run ../../main.go lens --dir . --filename gen_lens.go

627
v2/optics/lenses/matcher.go Normal file
View File

@@ -0,0 +1,627 @@
// 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 lenses
import (
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
__lens "github.com/IBM/fp-go/v2/optics/lens"
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
__prism "github.com/IBM/fp-go/v2/optics/prism"
__option "github.com/IBM/fp-go/v2/option"
)
// MatchLenses provides lenses for accessing and modifying fields of Match structures.
// Lenses enable functional updates to immutable data structures by providing
// composable getters and setters.
//
// This struct contains both mandatory lenses (for direct field access) and optional
// lenses (for fields that may be zero values, treating them as Option types).
//
// Fields:
// - Before: Lens for the text before the match
// - Groups: Lens for the capture groups array
// - After: Lens for the text after the match
// - BeforeO: Optional lens treating empty Before as None
// - AfterO: Optional lens treating empty After as None
//
// Example:
//
// lenses := MakeMatchLenses()
// match := Match{Before: "hello ", Groups: []string{"world"}, After: "!"}
//
// // Get a field value
// before := lenses.Before.Get(match) // "hello "
//
// // Set a field value (returns new Match)
// updated := lenses.Before.Set(match, "hi ")
// // updated.Before is now "hi "
type MatchLenses struct {
// mandatory fields
Before __lens.Lens[__prism.Match, string]
Groups __lens.Lens[__prism.Match, []string]
After __lens.Lens[__prism.Match, string]
// optional fields
BeforeO __lens_option.LensO[__prism.Match, string]
AfterO __lens_option.LensO[__prism.Match, string]
}
// MatchRefLenses provides lenses for accessing and modifying fields of Match structures
// via pointers. This is useful when working with mutable references to Match values.
//
// In addition to standard lenses, this struct also includes prisms for each field,
// which provide optional access patterns (useful for validation or conditional updates).
//
// Fields:
// - Before, Groups, After: Standard lenses for pointer-based access
// - BeforeO, AfterO: Optional lenses treating zero values as None
// - BeforeP, GroupsP, AfterP: Prisms for optional field access
//
// Example:
//
// lenses := MakeMatchRefLenses()
// match := &Match{Before: "hello ", Groups: []string{"world"}, After: "!"}
//
// // Get a field value
// before := lenses.Before.Get(match) // "hello "
//
// // Set a field value (mutates the pointer)
// lenses.Before.Set(match, "hi ")
// // match.Before is now "hi "
type MatchRefLenses struct {
// mandatory fields
Before __lens.Lens[*__prism.Match, string]
Groups __lens.Lens[*__prism.Match, []string]
After __lens.Lens[*__prism.Match, string]
// optional fields
BeforeO __lens_option.LensO[*__prism.Match, string]
AfterO __lens_option.LensO[*__prism.Match, string]
// prisms
BeforeP __prism.Prism[*__prism.Match, string]
GroupsP __prism.Prism[*__prism.Match, []string]
AfterP __prism.Prism[*__prism.Match, string]
}
// MatchPrisms provides prisms for accessing fields of Match structures.
// Prisms enable safe access to fields that may not be present (zero values),
// returning Option types instead of direct values.
//
// Fields:
// - Before: Prism for Before field (None if empty string)
// - Groups: Prism for Groups field (always Some)
// - After: Prism for After field (None if empty string)
//
// Example:
//
// prisms := MakeMatchPrisms()
// match := Match{Before: "", Groups: []string{"test"}, After: "!"}
//
// // Try to get Before (returns None because it's empty)
// beforeOpt := prisms.Before.GetOption(match) // None
//
// // Get After (returns Some because it's non-empty)
// afterOpt := prisms.After.GetOption(match) // Some("!")
type MatchPrisms struct {
Before __prism.Prism[__prism.Match, string]
Groups __prism.Prism[__prism.Match, []string]
After __prism.Prism[__prism.Match, string]
}
// MakeMatchLenses creates a new MatchLenses with lenses for all fields of Match.
// This function constructs both mandatory lenses (for direct field access) and
// optional lenses (for treating zero values as Option types).
//
// The returned lenses enable functional-style updates to Match structures,
// allowing you to get and set field values while maintaining immutability.
//
// Returns:
// - A MatchLenses struct with lenses for Before, Groups, After fields,
// plus optional lenses BeforeO and AfterO
//
// Example:
//
// lenses := MakeMatchLenses()
// match := Match{Before: "start ", Groups: []string{"middle"}, After: " end"}
//
// // Get field values
// before := lenses.Before.Get(match) // "start "
// groups := lenses.Groups.Get(match) // []string{"middle"}
//
// // Update a field (returns new Match)
// updated := lenses.After.Set(match, " finish")
// // updated is a new Match with After = " finish"
//
// // Use optional lens (treats empty string as None)
// emptyMatch := Match{Before: "", Groups: []string{}, After: ""}
// beforeOpt := lenses.BeforeO.GetOption(emptyMatch) // None
func MakeMatchLenses() MatchLenses {
// mandatory lenses
lensBefore := __lens.MakeLensWithName(
func(s __prism.Match) string { return s.Before },
func(s __prism.Match, v string) __prism.Match { s.Before = v; return s },
"Match.Before",
)
lensGroups := __lens.MakeLensWithName(
func(s __prism.Match) []string { return s.Groups },
func(s __prism.Match, v []string) __prism.Match { s.Groups = v; return s },
"Match.Groups",
)
lensAfter := __lens.MakeLensWithName(
func(s __prism.Match) string { return s.After },
func(s __prism.Match, v string) __prism.Match { s.After = v; return s },
"Match.After",
)
// optional lenses
lensBeforeO := __lens_option.FromIso[__prism.Match](__iso_option.FromZero[string]())(lensBefore)
lensAfterO := __lens_option.FromIso[__prism.Match](__iso_option.FromZero[string]())(lensAfter)
return MatchLenses{
// mandatory lenses
Before: lensBefore,
Groups: lensGroups,
After: lensAfter,
// optional lenses
BeforeO: lensBeforeO,
AfterO: lensAfterO,
}
}
// MakeMatchRefLenses creates a new MatchRefLenses with lenses for all fields of *Match.
// This function constructs lenses that work with pointers to Match structures,
// enabling both immutable-style updates and direct mutations.
//
// The returned lenses include:
// - Standard lenses for pointer-based field access
// - Optional lenses for treating zero values as Option types
// - Prisms for safe optional field access
//
// Returns:
// - A MatchRefLenses struct with lenses and prisms for all Match fields
//
// Example:
//
// lenses := MakeMatchRefLenses()
// match := &Match{Before: "prefix ", Groups: []string{"data"}, After: " suffix"}
//
// // Get field value
// before := lenses.Before.Get(match) // "prefix "
//
// // Set field value (mutates the pointer)
// lenses.Before.Set(match, "new ")
// // match.Before is now "new "
//
// // Use prism for optional access
// afterOpt := lenses.AfterP.GetOption(match) // Some(" suffix")
func MakeMatchRefLenses() MatchRefLenses {
// mandatory lenses
lensBefore := __lens.MakeLensStrictWithName(
func(s *__prism.Match) string { return s.Before },
func(s *__prism.Match, v string) *__prism.Match { s.Before = v; return s },
"(*Match).Before",
)
lensGroups := __lens.MakeLensRefWithName(
func(s *__prism.Match) []string { return s.Groups },
func(s *__prism.Match, v []string) *__prism.Match { s.Groups = v; return s },
"(*Match).Groups",
)
lensAfter := __lens.MakeLensStrictWithName(
func(s *__prism.Match) string { return s.After },
func(s *__prism.Match, v string) *__prism.Match { s.After = v; return s },
"(*Match).After",
)
// optional lenses
lensBeforeO := __lens_option.FromIso[*__prism.Match](__iso_option.FromZero[string]())(lensBefore)
lensAfterO := __lens_option.FromIso[*__prism.Match](__iso_option.FromZero[string]())(lensAfter)
return MatchRefLenses{
// mandatory lenses
Before: lensBefore,
Groups: lensGroups,
After: lensAfter,
// optional lenses
BeforeO: lensBeforeO,
AfterO: lensAfterO,
}
}
// MakeMatchPrisms creates a new MatchPrisms with prisms for all fields of Match.
// This function constructs prisms that provide safe optional access to Match fields,
// treating zero values (empty strings) as None.
//
// The returned prisms enable pattern matching on field presence:
// - Before and After prisms return None for empty strings
// - Groups prism always returns Some (even for empty slices)
//
// Returns:
// - A MatchPrisms struct with prisms for Before, Groups, and After fields
//
// Example:
//
// prisms := MakeMatchPrisms()
// match := Match{Before: "", Groups: []string{"data"}, After: "!"}
//
// // Try to get Before (returns None because it's empty)
// beforeOpt := prisms.Before.GetOption(match) // None
//
// // Get Groups (always returns Some)
// groupsOpt := prisms.Groups.GetOption(match) // Some([]string{"data"})
//
// // Get After (returns Some because it's non-empty)
// afterOpt := prisms.After.GetOption(match) // Some("!")
//
// // Construct a Match from a value using ReverseGet
// newMatch := prisms.Before.ReverseGet("prefix ")
// // newMatch is Match{Before: "prefix ", Groups: nil, After: ""}
func MakeMatchPrisms() MatchPrisms {
_fromNonZeroBefore := __option.FromNonZero[string]()
_prismBefore := __prism.MakePrismWithName(
func(s __prism.Match) __option.Option[string] { return _fromNonZeroBefore(s.Before) },
func(v string) __prism.Match {
return __prism.Match{Before: v}
},
"Match.Before",
)
_prismGroups := __prism.MakePrismWithName(
func(s __prism.Match) __option.Option[[]string] { return __option.Some(s.Groups) },
func(v []string) __prism.Match {
return __prism.Match{Groups: v}
},
"Match.Groups",
)
_fromNonZeroAfter := __option.FromNonZero[string]()
_prismAfter := __prism.MakePrismWithName(
func(s __prism.Match) __option.Option[string] { return _fromNonZeroAfter(s.After) },
func(v string) __prism.Match {
return __prism.Match{After: v}
},
"Match.After",
)
return MatchPrisms{
Before: _prismBefore,
Groups: _prismGroups,
After: _prismAfter,
}
}
// NamedMatchLenses provides lenses for accessing and modifying fields of NamedMatch structures.
// NamedMatch represents regex matches with named capture groups, and these lenses enable
// functional updates to its fields.
//
// This struct contains both mandatory lenses (for direct field access) and optional
// lenses (for fields that may be zero values, treating them as Option types).
//
// Fields:
// - Before: Lens for the text before the match
// - Groups: Lens for the named capture groups map
// - Full: Lens for the complete matched text
// - After: Lens for the text after the match
// - BeforeO, FullO, AfterO: Optional lenses treating empty strings as None
//
// Example:
//
// lenses := MakeNamedMatchLenses()
// match := NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{"user": "john", "domain": "example.com"},
// Full: "john@example.com",
// After: "",
// }
//
// // Get a field value
// full := lenses.Full.Get(match) // "john@example.com"
//
// // Set a field value (returns new NamedMatch)
// updated := lenses.Before.Set(match, "Contact: ")
// // updated.Before is now "Contact: "
type NamedMatchLenses struct {
// mandatory fields
Before __lens.Lens[__prism.NamedMatch, string]
Groups __lens.Lens[__prism.NamedMatch, map[string]string]
Full __lens.Lens[__prism.NamedMatch, string]
After __lens.Lens[__prism.NamedMatch, string]
// optional fields
BeforeO __lens_option.LensO[__prism.NamedMatch, string]
FullO __lens_option.LensO[__prism.NamedMatch, string]
AfterO __lens_option.LensO[__prism.NamedMatch, string]
}
// NamedMatchRefLenses provides lenses for accessing and modifying fields of NamedMatch
// structures via pointers. This is useful when working with mutable references to
// NamedMatch values.
//
// In addition to standard lenses, this struct also includes prisms for each field,
// which provide optional access patterns (useful for validation or conditional updates).
//
// Fields:
// - Before, Groups, Full, After: Standard lenses for pointer-based access
// - BeforeO, FullO, AfterO: Optional lenses treating zero values as None
// - BeforeP, GroupsP, FullP, AfterP: Prisms for optional field access
//
// Example:
//
// lenses := MakeNamedMatchRefLenses()
// match := &NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{"user": "alice", "domain": "test.org"},
// Full: "alice@test.org",
// After: " for info",
// }
//
// // Get a field value
// full := lenses.Full.Get(match) // "alice@test.org"
//
// // Set a field value (mutates the pointer)
// lenses.After.Set(match, " for contact")
// // match.After is now " for contact"
type NamedMatchRefLenses struct {
// mandatory fields
Before __lens.Lens[*__prism.NamedMatch, string]
Groups __lens.Lens[*__prism.NamedMatch, map[string]string]
Full __lens.Lens[*__prism.NamedMatch, string]
After __lens.Lens[*__prism.NamedMatch, string]
// optional fields
BeforeO __lens_option.LensO[*__prism.NamedMatch, string]
FullO __lens_option.LensO[*__prism.NamedMatch, string]
AfterO __lens_option.LensO[*__prism.NamedMatch, string]
// prisms
BeforeP __prism.Prism[*__prism.NamedMatch, string]
GroupsP __prism.Prism[*__prism.NamedMatch, map[string]string]
FullP __prism.Prism[*__prism.NamedMatch, string]
AfterP __prism.Prism[*__prism.NamedMatch, string]
}
// NamedMatchPrisms provides prisms for accessing fields of NamedMatch structures.
// Prisms enable safe access to fields that may not be present (zero values),
// returning Option types instead of direct values.
//
// Fields:
// - Before: Prism for Before field (None if empty string)
// - Groups: Prism for Groups field (always Some)
// - Full: Prism for Full field (None if empty string)
// - After: Prism for After field (None if empty string)
//
// Example:
//
// prisms := MakeNamedMatchPrisms()
// match := NamedMatch{
// Before: "",
// Groups: map[string]string{"user": "bob"},
// Full: "bob@example.com",
// After: "",
// }
//
// // Try to get Before (returns None because it's empty)
// beforeOpt := prisms.Before.GetOption(match) // None
//
// // Get Full (returns Some because it's non-empty)
// fullOpt := prisms.Full.GetOption(match) // Some("bob@example.com")
type NamedMatchPrisms struct {
Before __prism.Prism[__prism.NamedMatch, string]
Groups __prism.Prism[__prism.NamedMatch, map[string]string]
Full __prism.Prism[__prism.NamedMatch, string]
After __prism.Prism[__prism.NamedMatch, string]
}
// MakeNamedMatchLenses creates a new NamedMatchLenses with lenses for all fields of NamedMatch.
// This function constructs both mandatory lenses (for direct field access) and
// optional lenses (for treating zero values as Option types).
//
// The returned lenses enable functional-style updates to NamedMatch structures,
// allowing you to get and set field values while maintaining immutability.
//
// Returns:
// - A NamedMatchLenses struct with lenses for Before, Groups, Full, After fields,
// plus optional lenses BeforeO, FullO, and AfterO
//
// Example:
//
// lenses := MakeNamedMatchLenses()
// match := NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{"user": "john", "domain": "example.com"},
// Full: "john@example.com",
// After: "",
// }
//
// // Get field values
// full := lenses.Full.Get(match) // "john@example.com"
// groups := lenses.Groups.Get(match) // map[string]string{"user": "john", ...}
//
// // Update a field (returns new NamedMatch)
// updated := lenses.Before.Set(match, "Contact: ")
// // updated is a new NamedMatch with Before = "Contact: "
//
// // Use optional lens (treats empty string as None)
// afterOpt := lenses.AfterO.GetOption(match) // None (because After is empty)
func MakeNamedMatchLenses() NamedMatchLenses {
// mandatory lenses
lensBefore := __lens.MakeLensWithName(
func(s __prism.NamedMatch) string { return s.Before },
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.Before = v; return s },
"NamedMatch.Before",
)
lensGroups := __lens.MakeLensWithName(
func(s __prism.NamedMatch) map[string]string { return s.Groups },
func(s __prism.NamedMatch, v map[string]string) __prism.NamedMatch { s.Groups = v; return s },
"NamedMatch.Groups",
)
lensFull := __lens.MakeLensWithName(
func(s __prism.NamedMatch) string { return s.Full },
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.Full = v; return s },
"NamedMatch.Full",
)
lensAfter := __lens.MakeLensWithName(
func(s __prism.NamedMatch) string { return s.After },
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.After = v; return s },
"NamedMatch.After",
)
// optional lenses
lensBeforeO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensBefore)
lensFullO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensFull)
lensAfterO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensAfter)
return NamedMatchLenses{
// mandatory lenses
Before: lensBefore,
Groups: lensGroups,
Full: lensFull,
After: lensAfter,
// optional lenses
BeforeO: lensBeforeO,
FullO: lensFullO,
AfterO: lensAfterO,
}
}
// MakeNamedMatchRefLenses creates a new NamedMatchRefLenses with lenses for all fields of *NamedMatch.
// This function constructs lenses that work with pointers to NamedMatch structures,
// enabling both immutable-style updates and direct mutations.
//
// The returned lenses include:
// - Standard lenses for pointer-based field access
// - Optional lenses for treating zero values as Option types
// - Prisms for safe optional field access
//
// Returns:
// - A NamedMatchRefLenses struct with lenses and prisms for all NamedMatch fields
//
// Example:
//
// lenses := MakeNamedMatchRefLenses()
// match := &NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{"user": "alice", "domain": "test.org"},
// Full: "alice@test.org",
// After: "",
// }
//
// // Get field value
// full := lenses.Full.Get(match) // "alice@test.org"
//
// // Set field value (mutates the pointer)
// lenses.Before.Set(match, "Contact: ")
// // match.Before is now "Contact: "
//
// // Use prism for optional access
// fullOpt := lenses.FullP.GetOption(match) // Some("alice@test.org")
func MakeNamedMatchRefLenses() NamedMatchRefLenses {
// mandatory lenses
lensBefore := __lens.MakeLensStrictWithName(
func(s *__prism.NamedMatch) string { return s.Before },
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.Before = v; return s },
"(*NamedMatch).Before",
)
lensGroups := __lens.MakeLensRefWithName(
func(s *__prism.NamedMatch) map[string]string { return s.Groups },
func(s *__prism.NamedMatch, v map[string]string) *__prism.NamedMatch { s.Groups = v; return s },
"(*NamedMatch).Groups",
)
lensFull := __lens.MakeLensStrictWithName(
func(s *__prism.NamedMatch) string { return s.Full },
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.Full = v; return s },
"(*NamedMatch).Full",
)
lensAfter := __lens.MakeLensStrictWithName(
func(s *__prism.NamedMatch) string { return s.After },
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.After = v; return s },
"(*NamedMatch).After",
)
// optional lenses
lensBeforeO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensBefore)
lensFullO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensFull)
lensAfterO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensAfter)
return NamedMatchRefLenses{
// mandatory lenses
Before: lensBefore,
Groups: lensGroups,
Full: lensFull,
After: lensAfter,
// optional lenses
BeforeO: lensBeforeO,
FullO: lensFullO,
AfterO: lensAfterO,
}
}
// MakeNamedMatchPrisms creates a new NamedMatchPrisms with prisms for all fields of NamedMatch.
// This function constructs prisms that provide safe optional access to NamedMatch fields,
// treating zero values (empty strings) as None.
//
// The returned prisms enable pattern matching on field presence:
// - Before, Full, and After prisms return None for empty strings
// - Groups prism always returns Some (even for nil or empty maps)
//
// Returns:
// - A NamedMatchPrisms struct with prisms for Before, Groups, Full, and After fields
//
// Example:
//
// prisms := MakeNamedMatchPrisms()
// match := NamedMatch{
// Before: "",
// Groups: map[string]string{"user": "bob", "domain": "example.com"},
// Full: "bob@example.com",
// After: "",
// }
//
// // Try to get Before (returns None because it's empty)
// beforeOpt := prisms.Before.GetOption(match) // None
//
// // Get Groups (always returns Some)
// groupsOpt := prisms.Groups.GetOption(match)
// // Some(map[string]string{"user": "bob", "domain": "example.com"})
//
// // Get Full (returns Some because it's non-empty)
// fullOpt := prisms.Full.GetOption(match) // Some("bob@example.com")
//
// // Construct a NamedMatch from a value using ReverseGet
// newMatch := prisms.Full.ReverseGet("test@example.com")
// // newMatch is NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
func MakeNamedMatchPrisms() NamedMatchPrisms {
_fromNonZeroBefore := __option.FromNonZero[string]()
_prismBefore := __prism.MakePrismWithName(
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroBefore(s.Before) },
func(v string) __prism.NamedMatch {
return __prism.NamedMatch{Before: v}
},
"NamedMatch.Before",
)
_prismGroups := __prism.MakePrismWithName(
func(s __prism.NamedMatch) __option.Option[map[string]string] { return __option.Some(s.Groups) },
func(v map[string]string) __prism.NamedMatch {
return __prism.NamedMatch{Groups: v}
},
"NamedMatch.Groups",
)
_fromNonZeroFull := __option.FromNonZero[string]()
_prismFull := __prism.MakePrismWithName(
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroFull(s.Full) },
func(v string) __prism.NamedMatch {
return __prism.NamedMatch{Full: v}
},
"NamedMatch.Full",
)
_fromNonZeroAfter := __option.FromNonZero[string]()
_prismAfter := __prism.MakePrismWithName(
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroAfter(s.After) },
func(v string) __prism.NamedMatch {
return __prism.NamedMatch{After: v}
},
"NamedMatch.After",
)
return NamedMatchPrisms{
Before: _prismBefore,
Groups: _prismGroups,
Full: _prismFull,
After: _prismAfter,
}
}

View File

@@ -0,0 +1,558 @@
// 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 lenses
import (
"testing"
__prism "github.com/IBM/fp-go/v2/optics/prism"
__option "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestMatchLenses_Before tests the Before lens for Match
func TestMatchLenses_Before(t *testing.T) {
lenses := MakeMatchLenses()
match := __prism.Match{
Before: "prefix ",
Groups: []string{"match", "group1"},
After: " suffix",
}
// Test Get
before := lenses.Before.Get(match)
assert.Equal(t, "prefix ", before)
// Test Set (curried)
updated := lenses.Before.Set("new prefix ")(match)
assert.Equal(t, "new prefix ", updated.Before)
assert.Equal(t, match.Groups, updated.Groups) // Other fields unchanged
assert.Equal(t, match.After, updated.After)
assert.Equal(t, "prefix ", match.Before) // Original unchanged
}
// TestMatchLenses_Groups tests the Groups lens for Match
func TestMatchLenses_Groups(t *testing.T) {
lenses := MakeMatchLenses()
match := __prism.Match{
Before: "prefix ",
Groups: []string{"match", "group1"},
After: " suffix",
}
// Test Get
groups := lenses.Groups.Get(match)
assert.Equal(t, []string{"match", "group1"}, groups)
// Test Set (curried)
newGroups := []string{"new", "groups", "here"}
updated := lenses.Groups.Set(newGroups)(match)
assert.Equal(t, newGroups, updated.Groups)
assert.Equal(t, match.Before, updated.Before)
assert.Equal(t, match.After, updated.After)
}
// TestMatchLenses_After tests the After lens for Match
func TestMatchLenses_After(t *testing.T) {
lenses := MakeMatchLenses()
match := __prism.Match{
Before: "prefix ",
Groups: []string{"match"},
After: " suffix",
}
// Test Get
after := lenses.After.Get(match)
assert.Equal(t, " suffix", after)
// Test Set (curried)
updated := lenses.After.Set(" new suffix")(match)
assert.Equal(t, " new suffix", updated.After)
assert.Equal(t, match.Before, updated.Before)
assert.Equal(t, match.Groups, updated.Groups)
}
// TestMatchLenses_BeforeO tests the optional Before lens
func TestMatchLenses_BeforeO(t *testing.T) {
lenses := MakeMatchLenses()
t.Run("non-empty Before", func(t *testing.T) {
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
opt := lenses.BeforeO.Get(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "prefix ", value)
})
t.Run("empty Before", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
opt := lenses.BeforeO.Get(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("set Some", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
updated := lenses.BeforeO.Set(__option.Some("new "))(match)
assert.Equal(t, "new ", updated.Before)
})
t.Run("set None", func(t *testing.T) {
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
updated := lenses.BeforeO.Set(__option.None[string]())(match)
assert.Equal(t, "", updated.Before)
})
}
// TestMatchLenses_AfterO tests the optional After lens
func TestMatchLenses_AfterO(t *testing.T) {
lenses := MakeMatchLenses()
t.Run("non-empty After", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: " suffix"}
opt := lenses.AfterO.Get(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, " suffix", value)
})
t.Run("empty After", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
opt := lenses.AfterO.Get(match)
assert.True(t, __option.IsNone(opt))
})
}
// TestMatchRefLenses_Before tests the Before lens for *Match
func TestMatchRefLenses_Before(t *testing.T) {
lenses := MakeMatchRefLenses()
match := &__prism.Match{
Before: "prefix ",
Groups: []string{"match"},
After: " suffix",
}
// Test Get
before := lenses.Before.Get(match)
assert.Equal(t, "prefix ", before)
// Test Set (creates copy with MakeLensStrictWithName, curried)
result := lenses.Before.Set("new prefix ")(match)
assert.Equal(t, "new prefix ", result.Before)
assert.Equal(t, "prefix ", match.Before) // Original unchanged
assert.NotSame(t, match, result) // Returns new pointer
}
// TestMatchRefLenses_Groups tests the Groups lens for *Match
func TestMatchRefLenses_Groups(t *testing.T) {
lenses := MakeMatchRefLenses()
match := &__prism.Match{
Before: "prefix ",
Groups: []string{"match", "group1"},
After: " suffix",
}
// Test Get
groups := lenses.Groups.Get(match)
assert.Equal(t, []string{"match", "group1"}, groups)
// Test Set (creates copy with MakeLensRefWithName, curried)
newGroups := []string{"new", "groups"}
result := lenses.Groups.Set(newGroups)(match)
assert.Equal(t, newGroups, result.Groups)
assert.Equal(t, []string{"match", "group1"}, match.Groups) // Original unchanged
assert.NotSame(t, match, result)
}
// TestMatchRefLenses_After tests the After lens for *Match
func TestMatchRefLenses_After(t *testing.T) {
lenses := MakeMatchRefLenses()
match := &__prism.Match{
Before: "prefix ",
Groups: []string{"match"},
After: " suffix",
}
// Test Get
after := lenses.After.Get(match)
assert.Equal(t, " suffix", after)
// Test Set (creates copy with MakeLensStrictWithName, curried)
result := lenses.After.Set(" new suffix")(match)
assert.Equal(t, " new suffix", result.After)
assert.Equal(t, " suffix", match.After) // Original unchanged
assert.NotSame(t, match, result)
}
// TestMatchPrisms_Before tests the Before prism
func TestMatchPrisms_Before(t *testing.T) {
prisms := MakeMatchPrisms()
t.Run("GetOption with non-empty Before", func(t *testing.T) {
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
opt := prisms.Before.GetOption(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "prefix ", value)
})
t.Run("GetOption with empty Before", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
opt := prisms.Before.GetOption(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("ReverseGet", func(t *testing.T) {
match := prisms.Before.ReverseGet("test ")
assert.Equal(t, "test ", match.Before)
assert.Nil(t, match.Groups)
assert.Equal(t, "", match.After)
})
}
// TestMatchPrisms_Groups tests the Groups prism
func TestMatchPrisms_Groups(t *testing.T) {
prisms := MakeMatchPrisms()
t.Run("GetOption always returns Some", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{"a", "b"}, After: ""}
opt := prisms.Groups.GetOption(match)
assert.True(t, __option.IsSome(opt))
groups := __option.GetOrElse(func() []string { return nil })(opt)
assert.Equal(t, []string{"a", "b"}, groups)
})
t.Run("GetOption with nil Groups", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: nil, After: ""}
opt := prisms.Groups.GetOption(match)
assert.True(t, __option.IsSome(opt))
})
t.Run("ReverseGet", func(t *testing.T) {
groups := []string{"test", "groups"}
match := prisms.Groups.ReverseGet(groups)
assert.Equal(t, groups, match.Groups)
assert.Equal(t, "", match.Before)
assert.Equal(t, "", match.After)
})
}
// TestMatchPrisms_After tests the After prism
func TestMatchPrisms_After(t *testing.T) {
prisms := MakeMatchPrisms()
t.Run("GetOption with non-empty After", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: " suffix"}
opt := prisms.After.GetOption(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, " suffix", value)
})
t.Run("GetOption with empty After", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
opt := prisms.After.GetOption(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("ReverseGet", func(t *testing.T) {
match := prisms.After.ReverseGet(" test")
assert.Equal(t, " test", match.After)
assert.Nil(t, match.Groups)
assert.Equal(t, "", match.Before)
})
}
// TestNamedMatchLenses_Before tests the Before lens for NamedMatch
func TestNamedMatchLenses_Before(t *testing.T) {
lenses := MakeNamedMatchLenses()
match := __prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john", "domain": "example.com"},
Full: "john@example.com",
After: "",
}
// Test Get
before := lenses.Before.Get(match)
assert.Equal(t, "Email: ", before)
// Test Set (curried)
updated := lenses.Before.Set("Contact: ")(match)
assert.Equal(t, "Contact: ", updated.Before)
assert.Equal(t, match.Groups, updated.Groups)
assert.Equal(t, match.Full, updated.Full)
assert.Equal(t, match.After, updated.After)
assert.Equal(t, "Email: ", match.Before) // Original unchanged
}
// TestNamedMatchLenses_Groups tests the Groups lens for NamedMatch
func TestNamedMatchLenses_Groups(t *testing.T) {
lenses := MakeNamedMatchLenses()
match := __prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john", "domain": "example.com"},
Full: "john@example.com",
After: "",
}
// Test Get
groups := lenses.Groups.Get(match)
assert.Equal(t, map[string]string{"user": "john", "domain": "example.com"}, groups)
// Test Set (curried)
newGroups := map[string]string{"user": "alice", "domain": "test.org"}
updated := lenses.Groups.Set(newGroups)(match)
assert.Equal(t, newGroups, updated.Groups)
assert.Equal(t, match.Before, updated.Before)
}
// TestNamedMatchLenses_Full tests the Full lens for NamedMatch
func TestNamedMatchLenses_Full(t *testing.T) {
lenses := MakeNamedMatchLenses()
match := __prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john"},
Full: "john@example.com",
After: "",
}
// Test Get
full := lenses.Full.Get(match)
assert.Equal(t, "john@example.com", full)
// Test Set (curried)
updated := lenses.Full.Set("alice@test.org")(match)
assert.Equal(t, "alice@test.org", updated.Full)
assert.Equal(t, match.Before, updated.Before)
}
// TestNamedMatchLenses_After tests the After lens for NamedMatch
func TestNamedMatchLenses_After(t *testing.T) {
lenses := MakeNamedMatchLenses()
match := __prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john"},
Full: "john@example.com",
After: " for contact",
}
// Test Get
after := lenses.After.Get(match)
assert.Equal(t, " for contact", after)
// Test Set (curried)
updated := lenses.After.Set(" for info")(match)
assert.Equal(t, " for info", updated.After)
assert.Equal(t, match.Before, updated.Before)
}
// TestNamedMatchLenses_Optional tests optional lenses for NamedMatch
func TestNamedMatchLenses_Optional(t *testing.T) {
lenses := MakeNamedMatchLenses()
t.Run("BeforeO with non-empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "prefix ", Groups: nil, Full: "", After: ""}
opt := lenses.BeforeO.Get(match)
assert.True(t, __option.IsSome(opt))
})
t.Run("BeforeO with empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt := lenses.BeforeO.Get(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("FullO with non-empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
opt := lenses.FullO.Get(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "test@example.com", value)
})
t.Run("FullO with empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt := lenses.FullO.Get(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("AfterO with non-empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: " suffix"}
opt := lenses.AfterO.Get(match)
assert.True(t, __option.IsSome(opt))
})
t.Run("AfterO with empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt := lenses.AfterO.Get(match)
assert.True(t, __option.IsNone(opt))
})
}
// TestNamedMatchRefLenses_Immutability tests that reference lenses create copies
func TestNamedMatchRefLenses_Immutability(t *testing.T) {
lenses := MakeNamedMatchRefLenses()
match := &__prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john"},
Full: "john@example.com",
After: "",
}
// Test Before (creates copy, curried)
updated1 := lenses.Before.Set("Contact: ")(match)
assert.Equal(t, "Contact: ", updated1.Before)
assert.Equal(t, "Email: ", match.Before) // Original unchanged
// Test Groups (creates copy, curried)
newGroups := map[string]string{"user": "alice"}
updated2 := lenses.Groups.Set(newGroups)(match)
assert.Equal(t, newGroups, updated2.Groups)
assert.Equal(t, map[string]string{"user": "john"}, match.Groups) // Original unchanged
// Test Full (creates copy, curried)
updated3 := lenses.Full.Set("alice@test.org")(match)
assert.Equal(t, "alice@test.org", updated3.Full)
assert.Equal(t, "john@example.com", match.Full) // Original unchanged
// Test After (creates copy, curried)
updated4 := lenses.After.Set(" for info")(match)
assert.Equal(t, " for info", updated4.After)
assert.Equal(t, "", match.After) // Original unchanged
}
// TestNamedMatchPrisms tests prisms for NamedMatch
func TestNamedMatchPrisms(t *testing.T) {
prisms := MakeNamedMatchPrisms()
t.Run("Before prism", func(t *testing.T) {
match := __prism.NamedMatch{Before: "prefix ", Groups: nil, Full: "", After: ""}
opt := prisms.Before.GetOption(match)
assert.True(t, __option.IsSome(opt))
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt = prisms.Before.GetOption(emptyMatch)
assert.True(t, __option.IsNone(opt))
constructed := prisms.Before.ReverseGet("test ")
assert.Equal(t, "test ", constructed.Before)
})
t.Run("Groups prism always returns Some", func(t *testing.T) {
match := __prism.NamedMatch{
Before: "",
Groups: map[string]string{"key": "value"},
Full: "",
After: "",
}
opt := prisms.Groups.GetOption(match)
assert.True(t, __option.IsSome(opt))
nilMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt = prisms.Groups.GetOption(nilMatch)
assert.True(t, __option.IsSome(opt))
})
t.Run("Full prism", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
opt := prisms.Full.GetOption(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "test@example.com", value)
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt = prisms.Full.GetOption(emptyMatch)
assert.True(t, __option.IsNone(opt))
constructed := prisms.Full.ReverseGet("alice@test.org")
assert.Equal(t, "alice@test.org", constructed.Full)
})
t.Run("After prism", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: " suffix"}
opt := prisms.After.GetOption(match)
assert.True(t, __option.IsSome(opt))
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt = prisms.After.GetOption(emptyMatch)
assert.True(t, __option.IsNone(opt))
constructed := prisms.After.ReverseGet(" test")
assert.Equal(t, " test", constructed.After)
})
}
// TestMatchLenses_Immutability verifies that value lenses don't mutate originals
func TestMatchLenses_Immutability(t *testing.T) {
lenses := MakeMatchLenses()
original := __prism.Match{
Before: "original ",
Groups: []string{"group1", "group2"},
After: " original",
}
// Make a copy to compare later
originalBefore := original.Before
originalGroups := make([]string, len(original.Groups))
copy(originalGroups, original.Groups)
originalAfter := original.After
// Perform multiple updates (curried)
updated1 := lenses.Before.Set("updated ")(original)
updated2 := lenses.Groups.Set([]string{"new"})(updated1)
updated3 := lenses.After.Set(" updated")(updated2)
// Verify original is unchanged
assert.Equal(t, originalBefore, original.Before)
assert.Equal(t, originalGroups, original.Groups)
assert.Equal(t, originalAfter, original.After)
// Verify updates worked
assert.Equal(t, "updated ", updated3.Before)
assert.Equal(t, []string{"new"}, updated3.Groups)
assert.Equal(t, " updated", updated3.After)
}
// TestNamedMatchLenses_Immutability verifies that value lenses don't mutate originals
func TestNamedMatchLenses_Immutability(t *testing.T) {
lenses := MakeNamedMatchLenses()
original := __prism.NamedMatch{
Before: "original ",
Groups: map[string]string{"key": "value"},
Full: "original@example.com",
After: " original",
}
// Make copies to compare later
originalBefore := original.Before
originalFull := original.Full
originalAfter := original.After
// Perform multiple updates (curried)
updated1 := lenses.Before.Set("updated ")(original)
updated2 := lenses.Full.Set("updated@test.org")(updated1)
updated3 := lenses.After.Set(" updated")(updated2)
// Verify original is unchanged
assert.Equal(t, originalBefore, original.Before)
assert.Equal(t, originalFull, original.Full)
assert.Equal(t, originalAfter, original.After)
// Verify updates worked
assert.Equal(t, "updated ", updated3.Before)
assert.Equal(t, "updated@test.org", updated3.Full)
assert.Equal(t, " updated", updated3.After)
}

409
v2/optics/lenses/url.go Normal file
View File

@@ -0,0 +1,409 @@
package lenses
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-27 16:08:47.5483589 +0100 CET m=+0.003380301
import (
url "net/url"
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
__lens "github.com/IBM/fp-go/v2/optics/lens"
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
__option "github.com/IBM/fp-go/v2/option"
)
// ErrorLenses provides lenses for accessing fields of url.Error
type ErrorLenses struct {
// mandatory fields
Op __lens.Lens[url.Error, string]
URL __lens.Lens[url.Error, string]
Err __lens.Lens[url.Error, error]
// optional fields
OpO __lens_option.LensO[url.Error, string]
URLO __lens_option.LensO[url.Error, string]
ErrO __lens_option.LensO[url.Error, error]
}
// ErrorRefLenses provides lenses for accessing fields of url.Error via a reference to url.Error
type ErrorRefLenses struct {
// mandatory fields
Op __lens.Lens[*url.Error, string]
URL __lens.Lens[*url.Error, string]
Err __lens.Lens[*url.Error, error]
// optional fields
OpO __lens_option.LensO[*url.Error, string]
URLO __lens_option.LensO[*url.Error, string]
ErrO __lens_option.LensO[*url.Error, error]
}
// MakeErrorLenses creates a new ErrorLenses with lenses for all fields
func MakeErrorLenses() ErrorLenses {
// mandatory lenses
lensOp := __lens.MakeLensWithName(
func(s url.Error) string { return s.Op },
func(s url.Error, v string) url.Error { s.Op = v; return s },
"Error.Op",
)
lensURL := __lens.MakeLensWithName(
func(s url.Error) string { return s.URL },
func(s url.Error, v string) url.Error { s.URL = v; return s },
"Error.URL",
)
lensErr := __lens.MakeLensWithName(
func(s url.Error) error { return s.Err },
func(s url.Error, v error) url.Error { s.Err = v; return s },
"Error.Err",
)
// optional lenses
lensOpO := __lens_option.FromIso[url.Error](__iso_option.FromZero[string]())(lensOp)
lensURLO := __lens_option.FromIso[url.Error](__iso_option.FromZero[string]())(lensURL)
lensErrO := __lens_option.FromIso[url.Error](__iso_option.FromZero[error]())(lensErr)
return ErrorLenses{
// mandatory lenses
Op: lensOp,
URL: lensURL,
Err: lensErr,
// optional lenses
OpO: lensOpO,
URLO: lensURLO,
ErrO: lensErrO,
}
}
// MakeErrorRefLenses creates a new ErrorRefLenses with lenses for all fields
func MakeErrorRefLenses() ErrorRefLenses {
// mandatory lenses
lensOp := __lens.MakeLensStrictWithName(
func(s *url.Error) string { return s.Op },
func(s *url.Error, v string) *url.Error { s.Op = v; return s },
"(*url.Error).Op",
)
lensURL := __lens.MakeLensStrictWithName(
func(s *url.Error) string { return s.URL },
func(s *url.Error, v string) *url.Error { s.URL = v; return s },
"(*url.Error).url.URL",
)
lensErr := __lens.MakeLensStrictWithName(
func(s *url.Error) error { return s.Err },
func(s *url.Error, v error) *url.Error { s.Err = v; return s },
"(*url.Error).Err",
)
// optional lenses
lensOpO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[string]())(lensOp)
lensURLO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[string]())(lensURL)
lensErrO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[error]())(lensErr)
return ErrorRefLenses{
// mandatory lenses
Op: lensOp,
URL: lensURL,
Err: lensErr,
// optional lenses
OpO: lensOpO,
URLO: lensURLO,
ErrO: lensErrO,
}
}
// URLLenses provides lenses for accessing fields of url.URL
type URLLenses struct {
// mandatory fields
Scheme __lens.Lens[url.URL, string]
Opaque __lens.Lens[url.URL, string]
User __lens.Lens[url.URL, *url.Userinfo]
Host __lens.Lens[url.URL, string]
Path __lens.Lens[url.URL, string]
RawPath __lens.Lens[url.URL, string]
OmitHost __lens.Lens[url.URL, bool]
ForceQuery __lens.Lens[url.URL, bool]
RawQuery __lens.Lens[url.URL, string]
Fragment __lens.Lens[url.URL, string]
RawFragment __lens.Lens[url.URL, string]
// optional fields
SchemeO __lens_option.LensO[url.URL, string]
OpaqueO __lens_option.LensO[url.URL, string]
UserO __lens_option.LensO[url.URL, *url.Userinfo]
HostO __lens_option.LensO[url.URL, string]
PathO __lens_option.LensO[url.URL, string]
RawPathO __lens_option.LensO[url.URL, string]
OmitHostO __lens_option.LensO[url.URL, bool]
ForceQueryO __lens_option.LensO[url.URL, bool]
RawQueryO __lens_option.LensO[url.URL, string]
FragmentO __lens_option.LensO[url.URL, string]
RawFragmentO __lens_option.LensO[url.URL, string]
}
// URLRefLenses provides lenses for accessing fields of url.URL via a reference to url.URL
type URLRefLenses struct {
// mandatory fields
Scheme __lens.Lens[*url.URL, string]
Opaque __lens.Lens[*url.URL, string]
User __lens.Lens[*url.URL, *url.Userinfo]
Host __lens.Lens[*url.URL, string]
Path __lens.Lens[*url.URL, string]
RawPath __lens.Lens[*url.URL, string]
OmitHost __lens.Lens[*url.URL, bool]
ForceQuery __lens.Lens[*url.URL, bool]
RawQuery __lens.Lens[*url.URL, string]
Fragment __lens.Lens[*url.URL, string]
RawFragment __lens.Lens[*url.URL, string]
// optional fields
SchemeO __lens_option.LensO[*url.URL, string]
OpaqueO __lens_option.LensO[*url.URL, string]
UserO __lens_option.LensO[*url.URL, *url.Userinfo]
HostO __lens_option.LensO[*url.URL, string]
PathO __lens_option.LensO[*url.URL, string]
RawPathO __lens_option.LensO[*url.URL, string]
OmitHostO __lens_option.LensO[*url.URL, bool]
ForceQueryO __lens_option.LensO[*url.URL, bool]
RawQueryO __lens_option.LensO[*url.URL, string]
FragmentO __lens_option.LensO[*url.URL, string]
RawFragmentO __lens_option.LensO[*url.URL, string]
}
// MakeURLLenses creates a new URLLenses with lenses for all fields
func MakeURLLenses() URLLenses {
// mandatory lenses
lensScheme := __lens.MakeLensWithName(
func(s url.URL) string { return s.Scheme },
func(s url.URL, v string) url.URL { s.Scheme = v; return s },
"URL.Scheme",
)
lensOpaque := __lens.MakeLensWithName(
func(s url.URL) string { return s.Opaque },
func(s url.URL, v string) url.URL { s.Opaque = v; return s },
"URL.Opaque",
)
lensUser := __lens.MakeLensWithName(
func(s url.URL) *url.Userinfo { return s.User },
func(s url.URL, v *url.Userinfo) url.URL { s.User = v; return s },
"URL.User",
)
lensHost := __lens.MakeLensWithName(
func(s url.URL) string { return s.Host },
func(s url.URL, v string) url.URL { s.Host = v; return s },
"URL.Host",
)
lensPath := __lens.MakeLensWithName(
func(s url.URL) string { return s.Path },
func(s url.URL, v string) url.URL { s.Path = v; return s },
"URL.Path",
)
lensRawPath := __lens.MakeLensWithName(
func(s url.URL) string { return s.RawPath },
func(s url.URL, v string) url.URL { s.RawPath = v; return s },
"URL.RawPath",
)
lensOmitHost := __lens.MakeLensWithName(
func(s url.URL) bool { return s.OmitHost },
func(s url.URL, v bool) url.URL { s.OmitHost = v; return s },
"URL.OmitHost",
)
lensForceQuery := __lens.MakeLensWithName(
func(s url.URL) bool { return s.ForceQuery },
func(s url.URL, v bool) url.URL { s.ForceQuery = v; return s },
"URL.ForceQuery",
)
lensRawQuery := __lens.MakeLensWithName(
func(s url.URL) string { return s.RawQuery },
func(s url.URL, v string) url.URL { s.RawQuery = v; return s },
"URL.RawQuery",
)
lensFragment := __lens.MakeLensWithName(
func(s url.URL) string { return s.Fragment },
func(s url.URL, v string) url.URL { s.Fragment = v; return s },
"URL.Fragment",
)
lensRawFragment := __lens.MakeLensWithName(
func(s url.URL) string { return s.RawFragment },
func(s url.URL, v string) url.URL { s.RawFragment = v; return s },
"URL.RawFragment",
)
// optional lenses
lensSchemeO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensScheme)
lensOpaqueO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensOpaque)
lensUserO := __lens_option.FromIso[url.URL](__iso_option.FromZero[*url.Userinfo]())(lensUser)
lensHostO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensHost)
lensPathO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensPath)
lensRawPathO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawPath)
lensOmitHostO := __lens_option.FromIso[url.URL](__iso_option.FromZero[bool]())(lensOmitHost)
lensForceQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[bool]())(lensForceQuery)
lensRawQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawQuery)
lensFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensFragment)
lensRawFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawFragment)
return URLLenses{
// mandatory lenses
Scheme: lensScheme,
Opaque: lensOpaque,
User: lensUser,
Host: lensHost,
Path: lensPath,
RawPath: lensRawPath,
OmitHost: lensOmitHost,
ForceQuery: lensForceQuery,
RawQuery: lensRawQuery,
Fragment: lensFragment,
RawFragment: lensRawFragment,
// optional lenses
SchemeO: lensSchemeO,
OpaqueO: lensOpaqueO,
UserO: lensUserO,
HostO: lensHostO,
PathO: lensPathO,
RawPathO: lensRawPathO,
OmitHostO: lensOmitHostO,
ForceQueryO: lensForceQueryO,
RawQueryO: lensRawQueryO,
FragmentO: lensFragmentO,
RawFragmentO: lensRawFragmentO,
}
}
// MakeURLRefLenses creates a new URLRefLenses with lenses for all fields
func MakeURLRefLenses() URLRefLenses {
// mandatory lenses
lensScheme := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Scheme },
func(s *url.URL, v string) *url.URL { s.Scheme = v; return s },
"(*url.URL).Scheme",
)
lensOpaque := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Opaque },
func(s *url.URL, v string) *url.URL { s.Opaque = v; return s },
"(*url.URL).Opaque",
)
lensUser := __lens.MakeLensStrictWithName(
func(s *url.URL) *url.Userinfo { return s.User },
func(s *url.URL, v *url.Userinfo) *url.URL { s.User = v; return s },
"(*url.URL).User",
)
lensHost := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Host },
func(s *url.URL, v string) *url.URL { s.Host = v; return s },
"(*url.URL).Host",
)
lensPath := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Path },
func(s *url.URL, v string) *url.URL { s.Path = v; return s },
"(*url.URL).Path",
)
lensRawPath := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.RawPath },
func(s *url.URL, v string) *url.URL { s.RawPath = v; return s },
"(*url.URL).RawPath",
)
lensOmitHost := __lens.MakeLensStrictWithName(
func(s *url.URL) bool { return s.OmitHost },
func(s *url.URL, v bool) *url.URL { s.OmitHost = v; return s },
"(*url.URL).OmitHost",
)
lensForceQuery := __lens.MakeLensStrictWithName(
func(s *url.URL) bool { return s.ForceQuery },
func(s *url.URL, v bool) *url.URL { s.ForceQuery = v; return s },
"(*url.URL).ForceQuery",
)
lensRawQuery := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.RawQuery },
func(s *url.URL, v string) *url.URL { s.RawQuery = v; return s },
"(*url.URL).RawQuery",
)
lensFragment := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Fragment },
func(s *url.URL, v string) *url.URL { s.Fragment = v; return s },
"(*url.URL).Fragment",
)
lensRawFragment := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.RawFragment },
func(s *url.URL, v string) *url.URL { s.RawFragment = v; return s },
"(*url.URL).RawFragment",
)
// optional lenses
lensSchemeO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensScheme)
lensOpaqueO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensOpaque)
lensUserO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[*url.Userinfo]())(lensUser)
lensHostO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensHost)
lensPathO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensPath)
lensRawPathO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawPath)
lensOmitHostO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[bool]())(lensOmitHost)
lensForceQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[bool]())(lensForceQuery)
lensRawQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawQuery)
lensFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensFragment)
lensRawFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawFragment)
return URLRefLenses{
// mandatory lenses
Scheme: lensScheme,
Opaque: lensOpaque,
User: lensUser,
Host: lensHost,
Path: lensPath,
RawPath: lensRawPath,
OmitHost: lensOmitHost,
ForceQuery: lensForceQuery,
RawQuery: lensRawQuery,
Fragment: lensFragment,
RawFragment: lensRawFragment,
// optional lenses
SchemeO: lensSchemeO,
OpaqueO: lensOpaqueO,
UserO: lensUserO,
HostO: lensHostO,
PathO: lensPathO,
RawPathO: lensRawPathO,
OmitHostO: lensOmitHostO,
ForceQueryO: lensForceQueryO,
RawQueryO: lensRawQueryO,
FragmentO: lensFragmentO,
RawFragmentO: lensRawFragmentO,
}
}
// UserinfoRefLenses provides lenses for accessing fields of url.Userinfo via a reference to url.Userinfo
type UserinfoRefLenses struct {
// mandatory fields
Username __lens.Lens[*url.Userinfo, string]
Password __lens.Lens[*url.Userinfo, string]
// optional fields
UsernameO __lens_option.LensO[*url.Userinfo, string]
PasswordO __lens_option.LensO[*url.Userinfo, string]
}
// MakeUserinfoRefLenses creates a new UserinfoRefLenses with lenses for all fields
func MakeUserinfoRefLenses() UserinfoRefLenses {
// mandatory lenses
lensUsername := __lens.MakeLensStrictWithName(
(*url.Userinfo).Username,
func(s *url.Userinfo, v string) *url.Userinfo {
pwd, ok := s.Password()
if ok {
return url.UserPassword(v, pwd)
}
return url.User(v)
},
"(*url.Userinfo).Username",
)
lensPassword := __lens.MakeLensStrictWithName(
func(s *url.Userinfo) string {
pwd, _ := s.Password()
return pwd
},
func(s *url.Userinfo, v string) *url.Userinfo { return url.UserPassword(s.Username(), v) },
"(*url.Userinfo).Password",
)
// optional lenses
lensUsernameO := __lens_option.FromIso[*url.Userinfo](__iso_option.FromZero[string]())(lensUsername)
lensPasswordO := __lens.MakeLensStrictWithName(
__option.FromValidation((*url.Userinfo).Password),
func(s *url.Userinfo, v __option.Option[string]) *url.Userinfo {
return __option.MonadFold(v, func() *url.Userinfo { return url.User(s.Username()) }, func(pwd string) *url.Userinfo { return url.UserPassword(s.Username(), pwd) })
},
"(*url.Userinfo).Password",
)
return UserinfoRefLenses{
// mandatory lenses
Username: lensUsername,
Password: lensPassword,
// optional lenses
UsernameO: lensUsernameO,
PasswordO: lensPasswordO,
}
}

View File

@@ -0,0 +1,655 @@
// 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 lenses
import (
"errors"
"net/url"
"testing"
"github.com/IBM/fp-go/v2/option"
__option "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestUserinfoRefLenses_Username tests the Username lens
func TestUserinfoRefLenses_Username(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Get username from UserPassword", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
username := lenses.Username.Get(userinfo)
assert.Equal(t, "john", username)
})
t.Run("Get username from User", func(t *testing.T) {
userinfo := url.User("alice")
username := lenses.Username.Get(userinfo)
assert.Equal(t, "alice", username)
})
t.Run("Set username with password", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
updated := lenses.Username.Set("bob")(userinfo)
assert.Equal(t, "bob", updated.Username())
// Password should be preserved
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "secret123", pwd)
})
t.Run("Set username without password", func(t *testing.T) {
userinfo := url.User("alice")
updated := lenses.Username.Set("bob")(userinfo)
assert.Equal(t, "bob", updated.Username())
// Should still have no password
_, ok := updated.Password()
assert.False(t, ok)
})
}
// TestUserinfoRefLenses_Password tests the Password lens
func TestUserinfoRefLenses_Password(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Get password when present", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
password := lenses.Password.Get(userinfo)
assert.Equal(t, "secret123", password)
})
t.Run("Get password when absent", func(t *testing.T) {
userinfo := url.User("alice")
password := lenses.Password.Get(userinfo)
assert.Equal(t, "", password)
})
t.Run("Set password", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
updated := lenses.Password.Set("newpass")(userinfo)
assert.Equal(t, "john", updated.Username())
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "newpass", pwd)
})
t.Run("Set password on user without password", func(t *testing.T) {
userinfo := url.User("alice")
updated := lenses.Password.Set("newpass")(userinfo)
assert.Equal(t, "alice", updated.Username())
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "newpass", pwd)
})
}
// TestUserinfoRefLenses_UsernameO tests the optional Username lens
func TestUserinfoRefLenses_UsernameO(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Get non-empty username", func(t *testing.T) {
userinfo := url.User("john")
opt := lenses.UsernameO.Get(userinfo)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "john", value)
})
t.Run("Get empty username", func(t *testing.T) {
userinfo := url.User("")
opt := lenses.UsernameO.Get(userinfo)
assert.True(t, __option.IsNone(opt))
})
t.Run("Set Some username", func(t *testing.T) {
userinfo := url.User("")
updated := lenses.UsernameO.Set(__option.Some("alice"))(userinfo)
assert.Equal(t, "alice", updated.Username())
})
t.Run("Set None username", func(t *testing.T) {
userinfo := url.User("john")
updated := lenses.UsernameO.Set(__option.None[string]())(userinfo)
assert.Equal(t, "", updated.Username())
})
}
// TestUserinfoRefLenses_PasswordO tests the optional Password lens
func TestUserinfoRefLenses_PasswordO(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Get Some password when present", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
opt := lenses.PasswordO.Get(userinfo)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "secret123", value)
})
t.Run("Get None password when absent", func(t *testing.T) {
userinfo := url.User("alice")
opt := lenses.PasswordO.Get(userinfo)
assert.True(t, __option.IsNone(opt))
})
t.Run("Set Some password", func(t *testing.T) {
userinfo := url.User("john")
updated := lenses.PasswordO.Set(__option.Some("newpass"))(userinfo)
assert.Equal(t, "john", updated.Username())
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "newpass", pwd)
})
t.Run("Set None password removes it", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
assert.Equal(t, "john", updated.Username())
_, ok := updated.Password()
assert.False(t, ok)
})
t.Run("Set None password on user without password", func(t *testing.T) {
userinfo := url.User("alice")
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
assert.Equal(t, "alice", updated.Username())
_, ok := updated.Password()
assert.False(t, ok)
})
}
// TestUserinfoRefLenses_Composition tests composing lens operations
func TestUserinfoRefLenses_Composition(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Update both username and password", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
// Update username first
updated1 := lenses.Username.Set("alice")(userinfo)
// Then update password
updated2 := lenses.Password.Set("newpass")(updated1)
assert.Equal(t, "alice", updated2.Username())
pwd, ok := updated2.Password()
assert.True(t, ok)
assert.Equal(t, "newpass", pwd)
})
t.Run("Add password to user without password", func(t *testing.T) {
userinfo := url.User("bob")
updated := lenses.PasswordO.Set(__option.Some("pass123"))(userinfo)
assert.Equal(t, "bob", updated.Username())
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "pass123", pwd)
})
t.Run("Remove password from user with password", func(t *testing.T) {
userinfo := url.UserPassword("charlie", "oldpass")
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
assert.Equal(t, "charlie", updated.Username())
_, ok := updated.Password()
assert.False(t, ok)
})
}
// TestUserinfoRefLenses_EdgeCases tests edge cases
func TestUserinfoRefLenses_EdgeCases(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Empty username and password", func(t *testing.T) {
userinfo := url.UserPassword("", "")
username := lenses.Username.Get(userinfo)
assert.Equal(t, "", username)
password := lenses.Password.Get(userinfo)
assert.Equal(t, "", password)
})
t.Run("Special characters in username", func(t *testing.T) {
userinfo := url.User("user@domain.com")
username := lenses.Username.Get(userinfo)
assert.Equal(t, "user@domain.com", username)
updated := lenses.Username.Set("new@user.com")(userinfo)
assert.Equal(t, "new@user.com", updated.Username())
})
t.Run("Special characters in password", func(t *testing.T) {
userinfo := url.UserPassword("john", "p@$$w0rd!")
password := lenses.Password.Get(userinfo)
assert.Equal(t, "p@$$w0rd!", password)
updated := lenses.Password.Set("n3w!p@ss")(userinfo)
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "n3w!p@ss", pwd)
})
t.Run("Very long username", func(t *testing.T) {
longUsername := "verylongusernamethatexceedsnormallengthbutshouldbehanded"
userinfo := url.User(longUsername)
username := lenses.Username.Get(userinfo)
assert.Equal(t, longUsername, username)
})
t.Run("Very long password", func(t *testing.T) {
longPassword := "verylongpasswordthatexceedsnormallengthbutshouldbehanded"
userinfo := url.UserPassword("john", longPassword)
password := lenses.Password.Get(userinfo)
assert.Equal(t, longPassword, password)
})
}
// TestUserinfoRefLenses_Immutability tests that operations return new instances
func TestUserinfoRefLenses_Immutability(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Setting username returns new instance", func(t *testing.T) {
original := url.User("john")
updated := lenses.Username.Set("alice")(original)
// Original should be unchanged
assert.Equal(t, "john", original.Username())
// Updated should have new value
assert.Equal(t, "alice", updated.Username())
// Should be different instances
assert.NotSame(t, original, updated)
})
t.Run("Setting password returns new instance", func(t *testing.T) {
original := url.UserPassword("john", "pass1")
updated := lenses.Password.Set("pass2")(original)
// Original should be unchanged
pwd, _ := original.Password()
assert.Equal(t, "pass1", pwd)
// Updated should have new value
pwd, _ = updated.Password()
assert.Equal(t, "pass2", pwd)
// Should be different instances
assert.NotSame(t, original, updated)
})
t.Run("Multiple updates create new instances", func(t *testing.T) {
original := url.UserPassword("john", "pass1")
updated1 := lenses.Username.Set("alice")(original)
updated2 := lenses.Password.Set("pass2")(updated1)
// Original unchanged
assert.Equal(t, "john", original.Username())
pwd, _ := original.Password()
assert.Equal(t, "pass1", pwd)
// First update has new username, old password
assert.Equal(t, "alice", updated1.Username())
pwd, _ = updated1.Password()
assert.Equal(t, "pass1", pwd)
// Second update has new username and password
assert.Equal(t, "alice", updated2.Username())
pwd, _ = updated2.Password()
assert.Equal(t, "pass2", pwd)
// All different instances
assert.NotSame(t, original, updated1)
assert.NotSame(t, updated1, updated2)
assert.NotSame(t, original, updated2)
})
}
// TestUserinfoRefLenses_PasswordPresence tests password presence detection
func TestUserinfoRefLenses_PasswordPresence(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Distinguish between no password and empty password", func(t *testing.T) {
// User with no password
userNoPass := url.User("john")
optNoPass := lenses.PasswordO.Get(userNoPass)
assert.True(t, __option.IsNone(optNoPass))
// User with empty password (still has password set)
userEmptyPass := url.UserPassword("john", "")
optEmptyPass := lenses.PasswordO.Get(userEmptyPass)
// Empty password is still Some (password is set, just empty)
assert.True(t, __option.IsSome(optEmptyPass))
value := __option.GetOrElse(func() string { return "default" })(optEmptyPass)
assert.Equal(t, "", value)
})
t.Run("Password lens returns empty string for no password", func(t *testing.T) {
userinfo := url.User("john")
password := lenses.Password.Get(userinfo)
assert.Equal(t, "", password)
// But PasswordO returns None
opt := lenses.PasswordO.Get(userinfo)
assert.True(t, __option.IsNone(opt))
})
}
// TestErrorLenses tests lenses for url.Error
func TestErrorLenses(t *testing.T) {
lenses := MakeErrorLenses()
t.Run("Get and Set Op field", func(t *testing.T) {
urlErr := url.Error{
Op: "Get",
URL: "https://example.com",
Err: assert.AnError,
}
// Test Get
op := lenses.Op.Get(urlErr)
assert.Equal(t, "Get", op)
// Test Set (curried, returns new Error)
updated := lenses.Op.Set("Post")(urlErr)
assert.Equal(t, "Post", updated.Op)
assert.Equal(t, "Get", urlErr.Op) // Original unchanged
})
t.Run("Get and Set URL field", func(t *testing.T) {
urlErr := url.Error{
Op: "Get",
URL: "https://example.com",
Err: assert.AnError,
}
// Test Get
urlStr := lenses.URL.Get(urlErr)
assert.Equal(t, "https://example.com", urlStr)
// Test Set (curried)
updated := lenses.URL.Set("https://newsite.com")(urlErr)
assert.Equal(t, "https://newsite.com", updated.URL)
assert.Equal(t, "https://example.com", urlErr.URL) // Original unchanged
})
t.Run("Get and Set Err field", func(t *testing.T) {
originalErr := assert.AnError
urlErr := url.Error{
Op: "Get",
URL: "https://example.com",
Err: originalErr,
}
// Test Get
err := lenses.Err.Get(urlErr)
assert.Equal(t, originalErr, err)
// Test Set (curried)
newErr := errors.New("new error")
updated := lenses.Err.Set(newErr)(urlErr)
assert.Equal(t, newErr, updated.Err)
assert.Equal(t, originalErr, urlErr.Err) // Original unchanged
})
t.Run("Optional lenses", func(t *testing.T) {
urlErr := url.Error{
Op: "Get",
URL: "https://example.com",
Err: assert.AnError,
}
// Test OpO
opOpt := lenses.OpO.Get(urlErr)
assert.True(t, __option.IsSome(opOpt))
// Test with empty Op
emptyErr := url.Error{Op: "", URL: "test", Err: nil}
opOpt = lenses.OpO.Get(emptyErr)
assert.True(t, __option.IsNone(opOpt))
// Test URLO
urlOpt := lenses.URLO.Get(urlErr)
assert.True(t, __option.IsSome(urlOpt))
// Test ErrO
errOpt := lenses.ErrO.Get(urlErr)
assert.True(t, __option.IsSome(errOpt))
// Test with nil error
nilErrErr := url.Error{Op: "Get", URL: "test", Err: nil}
errOpt = lenses.ErrO.Get(nilErrErr)
assert.True(t, __option.IsNone(errOpt))
})
}
// TestErrorRefLenses tests reference lenses for url.Error
func TestErrorRefLenses(t *testing.T) {
lenses := MakeErrorRefLenses()
t.Run("Get and Set creates new instance", func(t *testing.T) {
urlErr := &url.Error{
Op: "Get",
URL: "https://example.com",
Err: assert.AnError,
}
// Test Get
op := lenses.Op.Get(urlErr)
assert.Equal(t, "Get", op)
// Test Set (creates copy)
updated := lenses.Op.Set("Post")(urlErr)
assert.Equal(t, "Post", updated.Op)
assert.Equal(t, "Get", urlErr.Op) // Original unchanged
assert.NotSame(t, urlErr, updated)
})
}
// TestURLLenses tests lenses for url.URL
func TestURLLenses(t *testing.T) {
lenses := MakeURLLenses()
t.Run("Get and Set Scheme", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com"}
scheme := lenses.Scheme.Get(u)
assert.Equal(t, "https", scheme)
updated := lenses.Scheme.Set("http")(u)
assert.Equal(t, "http", updated.Scheme)
assert.Equal(t, "https", u.Scheme) // Original unchanged
})
t.Run("Get and Set Host", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com"}
host := lenses.Host.Get(u)
assert.Equal(t, "example.com", host)
updated := lenses.Host.Set("newsite.com")(u)
assert.Equal(t, "newsite.com", updated.Host)
assert.Equal(t, "example.com", u.Host)
})
t.Run("Get and Set Path", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", Path: "/api/v1"}
path := lenses.Path.Get(u)
assert.Equal(t, "/api/v1", path)
updated := lenses.Path.Set("/api/v2")(u)
assert.Equal(t, "/api/v2", updated.Path)
assert.Equal(t, "/api/v1", u.Path)
})
t.Run("Get and Set RawQuery", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", RawQuery: "page=1"}
query := lenses.RawQuery.Get(u)
assert.Equal(t, "page=1", query)
updated := lenses.RawQuery.Set("page=2&limit=10")(u)
assert.Equal(t, "page=2&limit=10", updated.RawQuery)
assert.Equal(t, "page=1", u.RawQuery)
})
t.Run("Get and Set Fragment", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", Fragment: "section1"}
fragment := lenses.Fragment.Get(u)
assert.Equal(t, "section1", fragment)
updated := lenses.Fragment.Set("section2")(u)
assert.Equal(t, "section2", updated.Fragment)
assert.Equal(t, "section1", u.Fragment)
})
t.Run("Get and Set User", func(t *testing.T) {
userinfo := url.User("john")
u := url.URL{Scheme: "https", Host: "example.com", User: userinfo}
user := lenses.User.Get(u)
assert.Equal(t, userinfo, user)
newUser := url.UserPassword("alice", "pass")
updated := lenses.User.Set(newUser)(u)
assert.Equal(t, newUser, updated.User)
assert.Equal(t, userinfo, u.User)
})
t.Run("Boolean fields", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", ForceQuery: true}
forceQuery := lenses.ForceQuery.Get(u)
assert.True(t, forceQuery)
updated := lenses.ForceQuery.Set(false)(u)
assert.False(t, updated.ForceQuery)
assert.True(t, u.ForceQuery)
})
t.Run("Optional lenses", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", Path: "/test"}
// Non-empty scheme
schemeOpt := lenses.SchemeO.Get(u)
assert.True(t, __option.IsSome(schemeOpt))
// Empty RawQuery
queryOpt := lenses.RawQueryO.Get(u)
assert.True(t, __option.IsNone(queryOpt))
// Set Some
withQuery := lenses.RawQueryO.Set(__option.Some("q=test"))(u)
assert.Equal(t, "q=test", withQuery.RawQuery)
// Set None
cleared := lenses.RawQueryO.Set(__option.None[string]())(withQuery)
assert.Equal(t, "", cleared.RawQuery)
})
}
// TestURLRefLenses tests reference lenses for url.URL
func TestURLRefLenses(t *testing.T) {
lenses := MakeURLRefLenses()
t.Run("Creates new instances", func(t *testing.T) {
u := &url.URL{Scheme: "https", Host: "example.com", Path: "/api"}
// Test Get
scheme := lenses.Scheme.Get(u)
assert.Equal(t, "https", scheme)
// Test Set (creates copy)
updated := lenses.Scheme.Set("http")(u)
assert.Equal(t, "http", updated.Scheme)
assert.Equal(t, "https", u.Scheme) // Original unchanged
assert.NotSame(t, u, updated)
})
t.Run("Multiple field updates", func(t *testing.T) {
u := &url.URL{Scheme: "https", Host: "example.com"}
updated1 := lenses.Path.Set("/api/v1")(u)
updated2 := lenses.RawQuery.Set("page=1")(updated1)
updated3 := lenses.Fragment.Set("top")(updated2)
// Original unchanged
assert.Equal(t, "", u.Path)
assert.Equal(t, "", u.RawQuery)
assert.Equal(t, "", u.Fragment)
// Final result has all updates
assert.Equal(t, "/api/v1", updated3.Path)
assert.Equal(t, "page=1", updated3.RawQuery)
assert.Equal(t, "top", updated3.Fragment)
})
}
// TestURLLenses_ComplexScenarios tests complex URL manipulation scenarios
func TestURLLenses_ComplexScenarios(t *testing.T) {
lenses := MakeURLLenses()
t.Run("Build URL incrementally", func(t *testing.T) {
u := url.URL{}
u = lenses.Scheme.Set("https")(u)
u = lenses.Host.Set("api.example.com")(u)
u = lenses.Path.Set("/v1/users")(u)
u = lenses.RawQuery.Set("limit=10&offset=0")(u)
u = lenses.Fragment.Set("results")(u)
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "api.example.com", u.Host)
assert.Equal(t, "/v1/users", u.Path)
assert.Equal(t, "limit=10&offset=0", u.RawQuery)
assert.Equal(t, "results", u.Fragment)
})
t.Run("Update URL with authentication", func(t *testing.T) {
u := url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api",
}
userinfo := url.UserPassword("admin", "secret")
updated := lenses.User.Set(userinfo)(u)
assert.NotNil(t, updated.User)
assert.Equal(t, "admin", updated.User.Username())
pwd, ok := updated.User.Password()
assert.True(t, ok)
assert.Equal(t, "secret", pwd)
})
t.Run("Clear optional fields", func(t *testing.T) {
u := url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api",
RawQuery: "page=1",
Fragment: "top",
}
// Clear query and fragment
u = lenses.RawQueryO.Set(option.None[string]())(u)
u = lenses.FragmentO.Set(option.None[string]())(u)
assert.Equal(t, "", u.RawQuery)
assert.Equal(t, "", u.Fragment)
assert.Equal(t, "https", u.Scheme) // Other fields unchanged
assert.Equal(t, "example.com", u.Host)
})
}

View File

@@ -430,6 +430,8 @@ func FromNonZero[T comparable]() Prism[T, T] {
// // Groups: []string{"123"},
// // After: "",
// // }
//
// fp-go:Lens
type Match struct {
Before string // Text before the match
Groups []string // Capture groups (index 0 is full match)
@@ -599,6 +601,8 @@ func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
// // Full: "user@example.com",
// // After: "",
// // }
//
// fp-go:Lens
type NamedMatch struct {
Before string
Groups map[string]string
@@ -1097,3 +1101,163 @@ func FromOption[T any]() Prism[Option[T], T] {
func NonEmptyString() Prism[string, string] {
return FromNonZero[string]()
}
// ErrorPrisms provides prisms for accessing fields of url.Error
type ErrorPrisms struct {
Op Prism[url.Error, string]
URL Prism[url.Error, string]
Err Prism[url.Error, error]
}
// MakeErrorPrisms creates a new ErrorPrisms with prisms for all fields
func MakeErrorPrisms() ErrorPrisms {
_fromNonZeroOp := option.FromNonZero[string]()
_prismOp := MakePrismWithName(
func(s url.Error) Option[string] { return _fromNonZeroOp(s.Op) },
func(v string) url.Error {
return url.Error{Op: v}
},
"Error.Op",
)
_fromNonZeroURL := option.FromNonZero[string]()
_prismURL := MakePrismWithName(
func(s url.Error) Option[string] { return _fromNonZeroURL(s.URL) },
func(v string) url.Error {
return url.Error{URL: v}
},
"Error.URL",
)
_fromNonZeroErr := option.FromNonZero[error]()
_prismErr := MakePrismWithName(
func(s url.Error) Option[error] { return _fromNonZeroErr(s.Err) },
func(v error) url.Error {
return url.Error{Err: v}
},
"Error.Err",
)
return ErrorPrisms{
Op: _prismOp,
URL: _prismURL,
Err: _prismErr,
}
}
// URLPrisms provides prisms for accessing fields of url.URL
type URLPrisms struct {
Scheme Prism[url.URL, string]
Opaque Prism[url.URL, string]
User Prism[url.URL, *url.Userinfo]
Host Prism[url.URL, string]
Path Prism[url.URL, string]
RawPath Prism[url.URL, string]
OmitHost Prism[url.URL, bool]
ForceQuery Prism[url.URL, bool]
RawQuery Prism[url.URL, string]
Fragment Prism[url.URL, string]
RawFragment Prism[url.URL, string]
}
// MakeURLPrisms creates a new URLPrisms with prisms for all fields
func MakeURLPrisms() URLPrisms {
_fromNonZeroScheme := option.FromNonZero[string]()
_prismScheme := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroScheme(s.Scheme) },
func(v string) url.URL {
return url.URL{Scheme: v}
},
"URL.Scheme",
)
_fromNonZeroOpaque := option.FromNonZero[string]()
_prismOpaque := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroOpaque(s.Opaque) },
func(v string) url.URL {
return url.URL{Opaque: v}
},
"URL.Opaque",
)
_fromNonZeroUser := option.FromNonZero[*url.Userinfo]()
_prismUser := MakePrismWithName(
func(s url.URL) Option[*url.Userinfo] { return _fromNonZeroUser(s.User) },
func(v *url.Userinfo) url.URL {
return url.URL{User: v}
},
"URL.User",
)
_fromNonZeroHost := option.FromNonZero[string]()
_prismHost := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroHost(s.Host) },
func(v string) url.URL {
return url.URL{Host: v}
},
"URL.Host",
)
_fromNonZeroPath := option.FromNonZero[string]()
_prismPath := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroPath(s.Path) },
func(v string) url.URL {
return url.URL{Path: v}
},
"URL.Path",
)
_fromNonZeroRawPath := option.FromNonZero[string]()
_prismRawPath := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroRawPath(s.RawPath) },
func(v string) url.URL {
return url.URL{RawPath: v}
},
"URL.RawPath",
)
_fromNonZeroOmitHost := option.FromNonZero[bool]()
_prismOmitHost := MakePrismWithName(
func(s url.URL) Option[bool] { return _fromNonZeroOmitHost(s.OmitHost) },
func(v bool) url.URL {
return url.URL{OmitHost: v}
},
"URL.OmitHost",
)
_fromNonZeroForceQuery := option.FromNonZero[bool]()
_prismForceQuery := MakePrismWithName(
func(s url.URL) Option[bool] { return _fromNonZeroForceQuery(s.ForceQuery) },
func(v bool) url.URL {
return url.URL{ForceQuery: v}
},
"URL.ForceQuery",
)
_fromNonZeroRawQuery := option.FromNonZero[string]()
_prismRawQuery := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroRawQuery(s.RawQuery) },
func(v string) url.URL {
return url.URL{RawQuery: v}
},
"URL.RawQuery",
)
_fromNonZeroFragment := option.FromNonZero[string]()
_prismFragment := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroFragment(s.Fragment) },
func(v string) url.URL {
return url.URL{Fragment: v}
},
"URL.Fragment",
)
_fromNonZeroRawFragment := option.FromNonZero[string]()
_prismRawFragment := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroRawFragment(s.RawFragment) },
func(v string) url.URL {
return url.URL{RawFragment: v}
},
"URL.RawFragment",
)
return URLPrisms{
Scheme: _prismScheme,
Opaque: _prismOpaque,
User: _prismUser,
Host: _prismHost,
Path: _prismPath,
RawPath: _prismRawPath,
OmitHost: _prismOmitHost,
ForceQuery: _prismForceQuery,
RawQuery: _prismRawQuery,
Fragment: _prismFragment,
RawFragment: _prismRawFragment,
}
}

View File

@@ -18,6 +18,7 @@ package prism
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
@@ -127,4 +128,6 @@ type (
Operator[S, A, B any] = func(Prism[S, A]) Prism[S, B]
Predicate[A any] = predicate.Predicate[A]
Lens[S, A any] = lens.Lens[S, A]
)

106
v2/optics/prism/url_test.go Normal file
View File

@@ -0,0 +1,106 @@
package prism
import (
"net/url"
"testing"
"github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestURLPrisms tests prisms for url.URL
func TestURLPrisms(t *testing.T) {
prisms := MakeURLPrisms()
t.Run("Scheme prism", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com"}
opt := prisms.Scheme.GetOption(u)
assert.True(t, option.IsSome(opt))
emptyU := url.URL{Scheme: "", Host: "example.com"}
opt = prisms.Scheme.GetOption(emptyU)
assert.True(t, option.IsNone(opt))
// ReverseGet
constructed := prisms.Scheme.ReverseGet("ftp")
assert.Equal(t, "ftp", constructed.Scheme)
})
t.Run("Host prism", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com"}
opt := prisms.Host.GetOption(u)
assert.True(t, option.IsSome(opt))
value := option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "example.com", value)
})
t.Run("Path prism", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", Path: "/api"}
opt := prisms.Path.GetOption(u)
assert.True(t, option.IsSome(opt))
emptyPath := url.URL{Scheme: "https", Host: "example.com", Path: ""}
opt = prisms.Path.GetOption(emptyPath)
assert.True(t, option.IsNone(opt))
})
t.Run("User prism", func(t *testing.T) {
userinfo := url.User("john")
u := url.URL{Scheme: "https", Host: "example.com", User: userinfo}
opt := prisms.User.GetOption(u)
assert.True(t, option.IsSome(opt))
noUser := url.URL{Scheme: "https", Host: "example.com", User: nil}
opt = prisms.User.GetOption(noUser)
assert.True(t, option.IsNone(opt))
})
t.Run("Boolean prisms", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", ForceQuery: true}
opt := prisms.ForceQuery.GetOption(u)
assert.True(t, option.IsSome(opt))
noForce := url.URL{Scheme: "https", Host: "example.com", ForceQuery: false}
opt = prisms.ForceQuery.GetOption(noForce)
assert.True(t, option.IsNone(opt))
})
}
// TestErrorPrisms tests prisms for url.Error
func TestErrorPrisms(t *testing.T) {
prisms := MakeErrorPrisms()
t.Run("Op prism", func(t *testing.T) {
urlErr := url.Error{Op: "Get", URL: "test", Err: nil}
opt := prisms.Op.GetOption(urlErr)
assert.True(t, option.IsSome(opt))
emptyErr := url.Error{Op: "", URL: "test", Err: nil}
opt = prisms.Op.GetOption(emptyErr)
assert.True(t, option.IsNone(opt))
// ReverseGet
constructed := prisms.Op.ReverseGet("Post")
assert.Equal(t, "Post", constructed.Op)
})
t.Run("URL prism", func(t *testing.T) {
urlErr := url.Error{Op: "Get", URL: "https://example.com", Err: nil}
opt := prisms.URL.GetOption(urlErr)
assert.True(t, option.IsSome(opt))
emptyErr := url.Error{Op: "Get", URL: "", Err: nil}
opt = prisms.URL.GetOption(emptyErr)
assert.True(t, option.IsNone(opt))
})
t.Run("Err prism", func(t *testing.T) {
urlErr := url.Error{Op: "Get", URL: "test", Err: assert.AnError}
opt := prisms.Err.GetOption(urlErr)
assert.True(t, option.IsSome(opt))
nilErr := url.Error{Op: "Get", URL: "test", Err: nil}
opt = prisms.Err.GetOption(nilErr)
assert.True(t, option.IsNone(opt))
})
}

View File

@@ -61,7 +61,7 @@ func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
Of[Seq[B]],
Map[Seq[B]],
MonadAp[Seq[B]],
Ap[Seq[B]],
f,
)

121
v2/reader/iter.go Normal file
View File

@@ -0,0 +1,121 @@
// 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 reader
import (
INTI "github.com/IBM/fp-go/v2/internal/iter"
)
// TraverseIter traverses an iterator sequence, applying a Reader-producing function to each element
// and collecting the results in a Reader that produces an iterator.
//
// This function transforms a sequence of values through a function that produces Readers,
// then "flips" the nesting so that instead of having an iterator of Readers, you get a
// single Reader that produces an iterator of values. All Readers share the same environment R.
//
// This is particularly useful when you have a collection of values that each need to be
// transformed using environment-dependent logic, and you want to defer the environment
// injection until the final execution.
//
// Type Parameters:
// - R: The shared environment/context type
// - A: The input element type in the iterator
// - B: The output element type in the resulting iterator
//
// Parameters:
// - f: A Kleisli arrow that transforms each element A into a Reader[R, B]
//
// Returns:
// - A Kleisli arrow that takes an iterator of A and returns a Reader producing an iterator of B
//
// Example:
//
// type Config struct { Multiplier int }
//
// // Function that creates a Reader for each number
// multiplyByConfig := func(x int) reader.Reader[Config, int] {
// return func(c Config) int { return x * c.Multiplier }
// }
//
// // Create an iterator of numbers
// numbers := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
//
// // Traverse the iterator
// traversed := reader.TraverseIter(multiplyByConfig)(numbers)
//
// // Execute with config
// result := traversed(Config{Multiplier: 10})
// // result is an iterator that yields: 10, 20, 30
func TraverseIter[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, Seq[A], Seq[B]] {
return INTI.Traverse[Seq[A]](
Map[R, B],
Of[R, Seq[B]],
Map[R, Seq[B]],
Ap[Seq[B]],
f,
)
}
// SequenceIter sequences an iterator of Readers into a Reader that produces an iterator.
//
// This function "flips" the nesting of an iterator and Reader types. Given an iterator
// where each element is a Reader[R, A], it produces a single Reader[R, Seq[A]] that,
// when executed with an environment, evaluates all the Readers with that environment
// and collects their results into an iterator.
//
// This is a special case of TraverseIter where the transformation function is the identity.
// All Readers in the input iterator share the same environment R and are evaluated with it.
//
// Type Parameters:
// - R: The shared environment/context type
// - A: The result type produced by each Reader
//
// Parameters:
// - as: An iterator sequence where each element is a Reader[R, A]
//
// Returns:
// - A Reader that, when executed, produces an iterator of all the Reader results
//
// Example:
//
// type Config struct { Base int }
//
// // Create an iterator of Readers
// readers := func(yield func(reader.Reader[Config, int]) bool) {
// yield(func(c Config) int { return c.Base + 1 })
// yield(func(c Config) int { return c.Base + 2 })
// yield(func(c Config) int { return c.Base + 3 })
// }
//
// // Sequence the iterator
// sequenced := reader.SequenceIter(readers)
//
// // Execute with config
// result := sequenced(Config{Base: 10})
// // result is an iterator that yields: 11, 12, 13
func SequenceIter[R, A any](as Seq[Reader[R, A]]) Reader[R, Seq[A]] {
return INTI.MonadSequence(
Map[R](INTI.Of[Seq[A]]),
ApplicativeMonoid[R](INTI.Monoid[Seq[A]]()),
as,
)
}

403
v2/reader/iter_test.go Normal file
View File

@@ -0,0 +1,403 @@
// 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 reader
import (
"fmt"
"slices"
"strconv"
"testing"
INTI "github.com/IBM/fp-go/v2/internal/iter"
"github.com/stretchr/testify/assert"
)
// Helper function to collect iterator values into a slice
func collectIter[A any](seq Seq[A]) []A {
return INTI.ToArray[Seq[A], []A](seq)
}
// Helper function to create an iterator from a slice
func fromSlice[A any](items []A) Seq[A] {
return slices.Values(items)
}
func TestTraverseIter(t *testing.T) {
type Config struct {
Multiplier int
Prefix string
}
t.Run("traverses empty iterator", func(t *testing.T) {
empty := INTI.Empty[Seq[int]]()
multiplyByConfig := func(x int) Reader[Config, int] {
return func(c Config) int { return x * c.Multiplier }
}
traversed := TraverseIter(multiplyByConfig)(empty)
result := traversed(Config{Multiplier: 10})
collected := collectIter(result)
assert.Empty(t, collected)
})
t.Run("traverses single element iterator", func(t *testing.T) {
single := INTI.Of[Seq[int]](5)
multiplyByConfig := func(x int) Reader[Config, int] {
return func(c Config) int { return x * c.Multiplier }
}
traversed := TraverseIter(multiplyByConfig)(single)
result := traversed(Config{Multiplier: 3})
collected := collectIter(result)
assert.Equal(t, []int{15}, collected)
})
t.Run("traverses multiple elements", func(t *testing.T) {
numbers := INTI.From(1, 2, 3, 4)
multiplyByConfig := func(x int) Reader[Config, int] {
return func(c Config) int { return x * c.Multiplier }
}
traversed := TraverseIter(multiplyByConfig)(numbers)
result := traversed(Config{Multiplier: 10})
collected := collectIter(result)
assert.Equal(t, []int{10, 20, 30, 40}, collected)
})
t.Run("transforms types during traversal", func(t *testing.T) {
numbers := INTI.From(1, 2, 3)
intToString := func(x int) Reader[Config, string] {
return func(c Config) string {
return fmt.Sprintf("%s%d", c.Prefix, x)
}
}
traversed := TraverseIter(intToString)(numbers)
result := traversed(Config{Prefix: "num-"})
collected := collectIter(result)
assert.Equal(t, []string{"num-1", "num-2", "num-3"}, collected)
})
t.Run("all readers share same environment", func(t *testing.T) {
numbers := INTI.From(1, 2, 3)
// Each reader accesses the same config
addBase := func(x int) Reader[Config, int] {
return func(c Config) int {
return x + c.Multiplier
}
}
traversed := TraverseIter(addBase)(numbers)
result := traversed(Config{Multiplier: 100})
collected := collectIter(result)
assert.Equal(t, []int{101, 102, 103}, collected)
})
t.Run("works with complex transformations", func(t *testing.T) {
words := INTI.From("hello", "world")
wordLength := func(s string) Reader[Config, int] {
return func(c Config) int {
return len(s) * c.Multiplier
}
}
traversed := TraverseIter(wordLength)(words)
result := traversed(Config{Multiplier: 2})
collected := collectIter(result)
assert.Equal(t, []int{10, 10}, collected) // "hello" = 5*2, "world" = 5*2
})
t.Run("preserves order of elements", func(t *testing.T) {
numbers := fromSlice([]int{5, 3, 8, 1, 9})
identity := func(x int) Reader[Config, int] {
return Of[Config](x)
}
traversed := TraverseIter(identity)(numbers)
result := traversed(Config{})
collected := collectIter(result)
assert.Equal(t, []int{5, 3, 8, 1, 9}, collected)
})
t.Run("can be used with different config types", func(t *testing.T) {
type StringConfig struct {
Suffix string
}
words := INTI.From("test", "data")
addSuffix := func(s string) Reader[StringConfig, string] {
return func(c StringConfig) string {
return s + c.Suffix
}
}
traversed := TraverseIter(addSuffix)(words)
result := traversed(StringConfig{Suffix: ".txt"})
collected := collectIter(result)
assert.Equal(t, []string{"test.txt", "data.txt"}, collected)
})
}
func TestSequenceIter(t *testing.T) {
type Config struct {
Base int
Multiplier int
}
t.Run("sequences empty iterator", func(t *testing.T) {
empty := func(yield func(Reader[Config, int]) bool) {}
sequenced := SequenceIter(empty)
result := sequenced(Config{Base: 10})
collected := collectIter(result)
assert.Empty(t, collected)
})
t.Run("sequences single reader", func(t *testing.T) {
single := func(yield func(Reader[Config, int]) bool) {
yield(func(c Config) int { return c.Base + 5 })
}
sequenced := SequenceIter(single)
result := sequenced(Config{Base: 10})
collected := collectIter(result)
assert.Equal(t, []int{15}, collected)
})
t.Run("sequences multiple readers", func(t *testing.T) {
readers := INTI.From(
func(c Config) int { return c.Base + 1 },
func(c Config) int { return c.Base + 2 },
func(c Config) int { return c.Base + 3 },
)
sequenced := SequenceIter(readers)
result := sequenced(Config{Base: 10})
collected := collectIter(result)
assert.Equal(t, []int{11, 12, 13}, collected)
})
t.Run("all readers receive same environment", func(t *testing.T) {
readers := INTI.From(
func(c Config) int { return c.Base * c.Multiplier },
func(c Config) int { return c.Base + c.Multiplier },
func(c Config) int { return c.Base - c.Multiplier },
)
sequenced := SequenceIter(readers)
result := sequenced(Config{Base: 10, Multiplier: 3})
collected := collectIter(result)
assert.Equal(t, []int{30, 13, 7}, collected)
})
t.Run("works with string readers", func(t *testing.T) {
type StringConfig struct {
Prefix string
Suffix string
}
readers := INTI.From(
func(c StringConfig) string { return c.Prefix + "first" },
func(c StringConfig) string { return c.Prefix + "second" },
func(c StringConfig) string { return "third" + c.Suffix },
)
sequenced := SequenceIter(readers)
result := sequenced(StringConfig{Prefix: "pre-", Suffix: "-post"})
collected := collectIter(result)
assert.Equal(t, []string{"pre-first", "pre-second", "third-post"}, collected)
})
t.Run("preserves order of readers", func(t *testing.T) {
readers := INTI.From(
Of[Config](5),
Of[Config](3),
Of[Config](8),
Of[Config](1),
)
sequenced := SequenceIter(readers)
result := sequenced(Config{})
collected := collectIter(result)
assert.Equal(t, []int{5, 3, 8, 1}, collected)
})
t.Run("works with complex reader logic", func(t *testing.T) {
readers := INTI.From(
func(c Config) string {
return strconv.Itoa(c.Base * 2)
},
func(c Config) string {
return fmt.Sprintf("mult-%d", c.Multiplier)
},
func(c Config) string {
return fmt.Sprintf("sum-%d", c.Base+c.Multiplier)
},
)
sequenced := SequenceIter(readers)
result := sequenced(Config{Base: 5, Multiplier: 3})
collected := collectIter(result)
assert.Equal(t, []string{"10", "mult-3", "sum-8"}, collected)
})
t.Run("can handle large number of readers", func(t *testing.T) {
readers := func(yield func(Reader[Config, int]) bool) {
for i := 0; i < 100; i++ {
i := i // capture loop variable
yield(func(c Config) int { return c.Base + i })
}
}
sequenced := SequenceIter(readers)
result := sequenced(Config{Base: 1000})
collected := collectIter(result)
assert.Len(t, collected, 100)
assert.Equal(t, 1000, collected[0])
assert.Equal(t, 1099, collected[99])
})
}
func TestTraverseIterAndSequenceIterRelationship(t *testing.T) {
type Config struct {
Value int
}
t.Run("SequenceIter is TraverseIter with identity", func(t *testing.T) {
// Create an iterator of readers
readers := INTI.From(
func(c Config) int { return c.Value + 1 },
func(c Config) int { return c.Value + 2 },
func(c Config) int { return c.Value + 3 },
)
// Using SequenceIter
sequenced := SequenceIter(readers)
sequencedResult := sequenced(Config{Value: 10})
// Using TraverseIter with identity function
identity := Asks[Config, int]
traversed := TraverseIter(identity)(readers)
traversedResult := traversed(Config{Value: 10})
// Both should produce the same results
sequencedCollected := collectIter(sequencedResult)
traversedCollected := collectIter(traversedResult)
assert.Equal(t, sequencedCollected, traversedCollected)
assert.Equal(t, []int{11, 12, 13}, sequencedCollected)
})
}
func TestIteratorIntegration(t *testing.T) {
type AppConfig struct {
DatabaseURL string
APIKey string
Port int
}
t.Run("real-world example: processing configuration values", func(t *testing.T) {
// Iterator of field names
fields := INTI.From(
"database",
"api",
"port",
)
// Function that creates a reader for each field
getConfigValue := func(field string) Reader[AppConfig, string] {
return func(c AppConfig) string {
switch field {
case "database":
return c.DatabaseURL
case "api":
return c.APIKey
case "port":
return strconv.Itoa(c.Port)
default:
return "unknown"
}
}
}
// Traverse to get all config values
traversed := TraverseIter(getConfigValue)(fields)
result := traversed(AppConfig{
DatabaseURL: "postgres://localhost",
APIKey: "secret-key",
Port: 8080,
})
collected := collectIter(result)
assert.Equal(t, []string{
"postgres://localhost",
"secret-key",
"8080",
}, collected)
})
t.Run("real-world example: batch processing with shared config", func(t *testing.T) {
type ProcessConfig struct {
Prefix string
Suffix string
}
// Iterator of items to process
items := fromSlice([]string{"item1", "item2", "item3"})
// Processing function that uses config
processItem := func(item string) Reader[ProcessConfig, string] {
return func(c ProcessConfig) string {
return c.Prefix + item + c.Suffix
}
}
// Process all items with shared config
traversed := TraverseIter(processItem)(items)
result := traversed(ProcessConfig{
Prefix: "[",
Suffix: "]",
})
collected := collectIter(result)
assert.Equal(t, []string{"[item1]", "[item2]", "[item3]"}, collected)
})
}

View File

@@ -15,7 +15,11 @@
package reader
import "github.com/IBM/fp-go/v2/tailrec"
import (
"iter"
"github.com/IBM/fp-go/v2/tailrec"
)
type (
// Reader represents a computation that depends on a shared environment of type R and produces a value of type A.
@@ -96,4 +100,7 @@ type (
// without stack overflow. It's used for implementing stack-safe recursive algorithms
// in the context of Reader computations.
Trampoline[B, L any] = tailrec.Trampoline[B, L]
// Seq represents an iterator sequence over values of type T.
Seq[T any] = iter.Seq[T]
)

View File

@@ -16,6 +16,7 @@
package readerio
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
)
@@ -234,3 +235,54 @@ func Local[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
return reader.Contramap[IO[A]](f)
}
// LocalIOK transforms the environment of a ReaderIO using an IO-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation before
// passing it to the ReaderIO.
//
// This is useful when the environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources.
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed to the ReaderIO[R1, A] to produce the final result
//
// Type Parameters:
// - A: The result type produced by the ReaderIO
// - R1: The original environment type expected by the ReaderIO
// - R2: The new input environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A Kleisli arrow that takes a ReaderIO[R1, A] and returns a ReaderIO[R2, A]
//
// Example:
//
// // Transform a config path into a loaded config
// loadConfig := func(path string) IO[Config] {
// return func() Config {
// // Load config from file
// return parseConfig(readFile(path))
// }
// }
//
// // Use the config to perform some operation
// useConfig := func(cfg Config) IO[string] {
// return Of("Using: " + cfg.Name)
// }
//
// // Compose them using LocalIOK
// result := LocalIOK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config and uses it
//
//go:inline
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, ReaderIO[R1, A], A] {
return func(ri ReaderIO[R1, A]) ReaderIO[R2, A] {
return F.Flow2(
f,
io.Chain(ri),
)
}
}

View File

@@ -612,3 +612,133 @@ func TestRealWorldScenarios(t *testing.T) {
assert.Equal(t, "[API] Status: 200, Body: OK", result)
})
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) io.IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Host: "localhost", Port: 8080}
}
}
// ReaderIO that uses the config
useConfig := func(cfg SimpleConfig) io.IO[string] {
return io.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
}
// Compose using LocalIOK
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
result := adapted("config.json")()
assert.Equal(t, "localhost:8080", result)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) io.IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) io.IO[string] {
return io.Of(fmt.Sprintf("Processed: %d", n))
}
adapted := LocalIOK[string, int, string](loadData)(processData)
result := adapted("test")()
assert.Equal(t, "Processed: 40", result)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("compose multiple LocalIOK", func(t *testing.T) {
// First transformation: string -> int
parseID := func(s string) io.IO[int] {
return func() int {
id, _ := strconv.Atoi(s)
return id
}
}
// Second transformation: int -> UserEnv
loadUser := func(id int) io.IO[UserEnv] {
return func() UserEnv {
return UserEnv{UserID: id}
}
}
// Use the UserEnv
formatUser := func(env UserEnv) io.IO[string] {
return io.Of(fmt.Sprintf("User ID: %d", env.UserID))
}
// Compose transformations
step1 := LocalIOK[string, UserEnv, int](loadUser)(formatUser)
step2 := LocalIOK[string, int, string](parseID)(step1)
result := step2("42")()
assert.Equal(t, "User ID: 42", result)
})
t.Run("environment extraction with IO", func(t *testing.T) {
// Extract database config from app config
extractDB := func(app AppConfig) io.IO[DatabaseConfig] {
return func() DatabaseConfig {
// Could perform validation or default setting here
cfg := app.Database
if cfg.Host == "" {
cfg.Host = "localhost"
}
return cfg
}
}
// Use the database config
connectDB := func(cfg DatabaseConfig) io.IO[string] {
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
}
adapted := LocalIOK[string, DatabaseConfig, AppConfig](extractDB)(connectDB)
result := adapted(AppConfig{
Database: DatabaseConfig{Host: "", Port: 5432},
})()
assert.Equal(t, "Connected to localhost:5432", result)
})
t.Run("real-world: load and parse config file", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Simulate reading file content
readFile := func(cf ConfigFile) io.IO[string] {
return func() string {
return `{"host":"example.com","port":9000}`
}
}
// Parse the content
parseConfig := func(content string) io.IO[SimpleConfig] {
return io.Of(SimpleConfig{Host: "example.com", Port: 9000})
}
// Use the parsed config
useConfig := func(cfg SimpleConfig) io.IO[string] {
return io.Of(fmt.Sprintf("Using %s:%d", cfg.Host, cfg.Port))
}
// Compose the pipeline
step1 := LocalIOK[string, SimpleConfig, string](parseConfig)(useConfig)
step2 := LocalIOK[string, string, ConfigFile](readFile)(step1)
result := step2(ConfigFile{Path: "app.json"})()
assert.Equal(t, "Using example.com:9000", result)
})
}

View File

@@ -16,8 +16,11 @@
package readerioeither
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOEither.
@@ -74,3 +77,117 @@ func Promap[R, E, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, E, ReaderIOE
func Contramap[E, A, R1, R2 any](f func(R2) R1) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
return reader.Contramap[IOEither[E, A]](f)
}
// LocalIOK transforms the environment of a ReaderIOEither using an IO-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation before
// passing it to the ReaderIOEither.
//
// This is useful when the environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources,
// but these effects cannot fail (or failures are not relevant to the error type E).
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed to the ReaderIOEither[R1, E, A] to produce the final result
//
// Type Parameters:
// - E: The error type (unchanged through the transformation)
// - A: The success type produced by the ReaderIOEither
// - R1: The original environment type expected by the ReaderIOEither
// - R2: The new input environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOEither[R1, E, A] and returns a ReaderIOEither[R2, E, A]
//
// Example:
//
// // Transform a config path into a loaded config (infallible)
// loadConfig := func(path string) IO[Config] {
// return func() Config {
// return getDefaultConfig() // Always succeeds
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOEither[error, string] {
// return func() Either[error, string] {
// if cfg.Valid {
// return Right[error]("Success")
// }
// return Left[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOK
// result := LocalIOK[error, string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config and uses it
//
//go:inline
func LocalIOK[E, A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
return readerio.LocalIOK[Either[E, A]](f)
}
// LocalIOEitherK transforms the environment of a ReaderIOEither using an IOEither-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation that can fail before
// passing it to the ReaderIOEither.
//
// This is useful when the environment transformation itself requires IO effects that can fail,
// such as reading from a file that might not exist, making a network call that might timeout,
// or parsing data that might be invalid.
//
// The transformation happens in two stages:
// 1. The IOEither effect f is executed with the R2 environment to produce Either[E, R1]
// 2. If successful (Right), the R1 value is passed to the ReaderIOEither[R1, E, A]
// 3. If failed (Left), the error E is propagated without executing the ReaderIOEither
//
// Type Parameters:
// - A: The success type produced by the ReaderIOEither
// - R1: The original environment type expected by the ReaderIOEither
// - R2: The new input environment type
// - E: The error type (shared by both the transformation and the ReaderIOEither)
//
// Parameters:
// - f: An IOEither Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOEither[R1, E, A] and returns a ReaderIOEither[R2, E, A]
//
// Example:
//
// // Transform a config path into a loaded config (can fail)
// loadConfig := func(path string) IOEither[error, Config] {
// return func() Either[error, Config] {
// cfg, err := readConfigFile(path)
// if err != nil {
// return Left[Config](err)
// }
// return Right[error](cfg)
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOEither[error, string] {
// return func() Either[error, string] {
// if cfg.Valid {
// return Right[error]("Success: " + cfg.Name)
// }
// return Left[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOEitherK
// result := LocalIOEitherK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
//
//go:inline
func LocalIOEitherK[A, R1, R2, E any](f ioeither.Kleisli[E, R2, R1]) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
return func(ri ReaderIOEither[R1, E, A]) ReaderIOEither[R2, E, A] {
return function.Flow2(
f,
ioeither.Chain(ri),
)
}
}

View File

@@ -131,3 +131,248 @@ func TestPromapWithIO(t *testing.T) {
assert.Equal(t, 1, counter) // Side effect occurred
})
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) IOE.IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Port: 8080}
}
}
// ReaderIOEither that uses the config
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
// Compose using LocalIOK
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(useConfig)
result := adapted("config.json")()
assert.Equal(t, E.Of[string]("Port: 8080"), result)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) IOE.IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) IOEither[string, string] {
return IOE.Of[string]("Processed: " + strconv.Itoa(n))
}
adapted := LocalIOK[string, string, int, string](loadData)(processData)
result := adapted("test")()
assert.Equal(t, E.Of[string]("Processed: 40"), result)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("error propagation in ReaderIOEither", func(t *testing.T) {
loadConfig := func(path string) IOE.IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderIOEither that returns an error
failingOperation := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Left[string]("operation failed")
}
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(failingOperation)
result := adapted("config.json")()
assert.Equal(t, E.Left[string]("operation failed"), result)
})
t.Run("compose multiple LocalIOK", func(t *testing.T) {
// First transformation: string -> int
parseID := func(s string) IOE.IO[int] {
return func() int {
id, _ := strconv.Atoi(s)
return id
}
}
// Second transformation: int -> SimpleConfig
loadConfig := func(id int) IOE.IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8000 + id}
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
// Compose transformations
step1 := LocalIOK[string, string, SimpleConfig, int](loadConfig)(formatConfig)
step2 := LocalIOK[string, string, int, string](parseID)(step1)
result := step2("42")()
assert.Equal(t, E.Of[string]("Port: 8042"), result)
})
}
// TestLocalIOEitherK tests LocalIOEitherK functionality
func TestLocalIOEitherK(t *testing.T) {
t.Run("basic IOEither transformation", func(t *testing.T) {
// IOEither effect that loads config from a path (can fail)
loadConfig := func(path string) IOEither[string, SimpleConfig] {
return func() E.Either[string, SimpleConfig] {
if path == "" {
return E.Left[SimpleConfig]("empty path")
}
return E.Of[string](SimpleConfig{Port: 8080})
}
}
// ReaderIOEither that uses the config
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
// Success case
result := adapted("config.json")()
assert.Equal(t, E.Of[string]("Port: 8080"), result)
// Failure case
resultErr := adapted("")()
assert.Equal(t, E.Left[string]("empty path"), resultErr)
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) IOEither[string, SimpleConfig] {
return func() E.Either[string, SimpleConfig] {
return E.Left[SimpleConfig]("file not found")
}
}
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
result := adapted("missing.json")()
// Error from loadConfig should propagate
assert.Equal(t, E.Left[string]("file not found"), result)
})
t.Run("error propagation from ReaderIOEither", func(t *testing.T) {
loadConfig := func(path string) IOEither[string, SimpleConfig] {
return IOE.Of[string](SimpleConfig{Port: 8080})
}
// ReaderIOEither that returns an error
failingOperation := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Left[string]("operation failed")
}
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(failingOperation)
result := adapted("config.json")()
// Error from ReaderIOEither should propagate
assert.Equal(t, E.Left[string]("operation failed"), result)
})
t.Run("compose multiple LocalIOEitherK", func(t *testing.T) {
// First transformation: string -> int (can fail)
parseID := func(s string) IOEither[string, int] {
return func() E.Either[string, int] {
id, err := strconv.Atoi(s)
if err != nil {
return E.Left[int]("invalid ID")
}
return E.Of[string](id)
}
}
// Second transformation: int -> SimpleConfig (can fail)
loadConfig := func(id int) IOEither[string, SimpleConfig] {
return func() E.Either[string, SimpleConfig] {
if id < 0 {
return E.Left[SimpleConfig]("invalid ID")
}
return E.Of[string](SimpleConfig{Port: 8000 + id})
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
// Compose transformations
step1 := LocalIOEitherK[string, SimpleConfig, int, string](loadConfig)(formatConfig)
step2 := LocalIOEitherK[string, int, string, string](parseID)(step1)
// Success case
result := step2("42")()
assert.Equal(t, E.Of[string]("Port: 8042"), result)
// Failure in first transformation
resultErr1 := step2("invalid")()
assert.Equal(t, E.Left[string]("invalid ID"), resultErr1)
// Failure in second transformation
resultErr2 := step2("-5")()
assert.Equal(t, E.Left[string]("invalid ID"), resultErr2)
})
t.Run("real-world: load and validate config", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Read file (can fail)
readFile := func(cf ConfigFile) IOEither[string, string] {
return func() E.Either[string, string] {
if cf.Path == "" {
return E.Left[string]("empty path")
}
return E.Of[string](`{"port":9000}`)
}
}
// Parse config (can fail)
parseConfig := func(content string) IOEither[string, SimpleConfig] {
return func() E.Either[string, SimpleConfig] {
if content == "" {
return E.Left[SimpleConfig]("empty content")
}
return E.Of[string](SimpleConfig{Port: 9000})
}
}
// Use the config
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Using port: " + strconv.Itoa(cfg.Port))
}
// Compose the pipeline
step1 := LocalIOEitherK[string, SimpleConfig, string, string](parseConfig)(useConfig)
step2 := LocalIOEitherK[string, string, ConfigFile, string](readFile)(step1)
// Success case
result := step2(ConfigFile{Path: "app.json"})()
assert.Equal(t, E.Of[string]("Using port: 9000"), result)
// Failure case
resultErr := step2(ConfigFile{Path: ""})()
assert.Equal(t, E.Left[string]("empty path"), resultErr)
})
}

View File

@@ -16,6 +16,9 @@
package readerioresult
import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
)
@@ -71,3 +74,165 @@ func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderIOResult[
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOResult[R1, A], A] {
return RIOE.Contramap[error, A](f)
}
// LocalIOK transforms the environment of a ReaderIOResult using an IO-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation before
// passing it to the ReaderIOResult.
//
// This is useful when the environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources,
// but these effects cannot fail (or failures are not relevant).
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed to the ReaderIOResult[R1, A] to produce the final result
//
// Type Parameters:
// - A: The success type produced by the ReaderIOResult
// - R1: The original environment type expected by the ReaderIOResult
// - R2: The new input environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
//
// Example:
//
// // Transform a config path into a loaded config (infallible)
// loadConfig := func(path string) IO[Config] {
// return func() Config {
// return getDefaultConfig() // Always succeeds
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOResult[string] {
// return func() Result[string] {
// if cfg.Valid {
// return Ok[string]("Success")
// }
// return Err[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOK
// result := LocalIOK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config and uses it
//
//go:inline
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
return RIOE.LocalIOK[error, A](f)
}
// LocalIOEitherK transforms the environment of a ReaderIOResult using an IOEither-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation that can fail before
// passing it to the ReaderIOResult.
//
// This is useful when the environment transformation itself requires IO effects that can fail,
// such as reading from a file that might not exist, making a network call that might timeout,
// or parsing data that might be invalid.
//
// The transformation happens in two stages:
// 1. The IOEither effect f is executed with the R2 environment to produce Either[error, R1]
// 2. If successful (Right), the R1 value is passed to the ReaderIOResult[R1, A]
// 3. If failed (Left), the error is propagated without executing the ReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderIOResult
// - R1: The original environment type expected by the ReaderIOResult
// - R2: The new input environment type
//
// Parameters:
// - f: An IOEither Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
//
// Example:
//
// // Transform a config path into a loaded config (can fail)
// loadConfig := func(path string) IOEither[error, Config] {
// return func() Either[error, Config] {
// cfg, err := readConfigFile(path)
// if err != nil {
// return Left[Config](err)
// }
// return Right[error](cfg)
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOResult[string] {
// return func() Result[string] {
// if cfg.Valid {
// return Ok[string]("Success: " + cfg.Name)
// }
// return Err[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOEitherK
// result := LocalIOEitherK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
//
//go:inline
func LocalIOEitherK[A, R1, R2 any](f ioeither.Kleisli[error, R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
return RIOE.LocalIOEitherK[A](f)
}
// LocalIOResultK transforms the environment of a ReaderIOResult using an IOResult-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation that can fail before
// passing it to the ReaderIOResult.
//
// This is a type-safe alias for LocalIOEitherK specialized for error type, providing a more
// idiomatic API when working with Result types (which use error as the error type).
//
// The transformation happens in two stages:
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed to the ReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderIOResult
// - R1: The original environment type expected by the ReaderIOResult
// - R2: The new input environment type
//
// Parameters:
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
//
// Example:
//
// // Transform a config path into a loaded config (can fail)
// loadConfig := func(path string) IOResult[Config] {
// return func() Result[Config] {
// cfg, err := readConfigFile(path)
// if err != nil {
// return Err[Config](err)
// }
// return Ok(cfg)
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOResult[string] {
// return func() Result[string] {
// if cfg.Valid {
// return Ok("Success: " + cfg.Name)
// }
// return Err[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOResultK
// result := LocalIOResultK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
//
//go:inline
func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
return RIOE.LocalIOEitherK[A](f)
}

View File

@@ -17,10 +17,12 @@ package readerioresult
import (
"context"
"errors"
"fmt"
"log"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
R "github.com/IBM/fp-go/v2/reader"
@@ -323,3 +325,297 @@ func TestReadIO(t *testing.T) {
assert.Equal(t, "Processing user alice (ID: 123)", result.GetOrElse(func(error) string { return "" })(res))
})
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
type SimpleConfig struct {
Port int
}
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Port: 8080}
}
}
// ReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
// Compose using LocalIOK
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
res := adapted("config.json")()
assert.Equal(t, result.Of("Port: 8080"), res)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Processed: %d", n))
}
}
adapted := LocalIOK[string, int, string](loadData)(processData)
res := adapted("test")()
assert.Equal(t, result.Of("Processed: 40"), res)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("error propagation in ReaderIOResult", func(t *testing.T) {
loadConfig := func(path string) IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(failingOperation)
res := adapted("config.json")()
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOEitherK tests LocalIOEitherK functionality
func TestLocalIOEitherK(t *testing.T) {
type SimpleConfig struct {
Port int
}
t.Run("basic IOEither transformation", func(t *testing.T) {
// IOEither effect that loads config from a path (can fail)
loadConfig := func(path string) IOEither[error, SimpleConfig] {
return func() Either[error, SimpleConfig] {
if path == "" {
return E.Left[SimpleConfig](errors.New("empty path"))
}
return E.Of[error](SimpleConfig{Port: 8080})
}
}
// ReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")()
assert.True(t, result.IsLeft(resErr))
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) IOEither[error, SimpleConfig] {
return func() Either[error, SimpleConfig] {
return E.Left[SimpleConfig](errors.New("file not found"))
}
}
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(useConfig)
res := adapted("missing.json")()
// Error from loadConfig should propagate
assert.True(t, result.IsLeft(res))
})
t.Run("error propagation from ReaderIOResult", func(t *testing.T) {
loadConfig := func(path string) IOEither[error, SimpleConfig] {
return func() Either[error, SimpleConfig] {
return E.Of[error](SimpleConfig{Port: 8080})
}
}
// ReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(failingOperation)
res := adapted("config.json")()
// Error from ReaderIOResult should propagate
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOResultK tests LocalIOResultK functionality
func TestLocalIOResultK(t *testing.T) {
type SimpleConfig struct {
Port int
}
t.Run("basic IOResult transformation", func(t *testing.T) {
// IOResult effect that loads config from a path (can fail)
loadConfig := func(path string) IOResult[SimpleConfig] {
return func() Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
// ReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
// Compose using LocalIOResultK
adapted := LocalIOResultK[string, SimpleConfig, string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")()
assert.True(t, result.IsLeft(resErr))
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) IOResult[SimpleConfig] {
return func() Result[SimpleConfig] {
return result.Left[SimpleConfig](errors.New("file not found"))
}
}
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
adapted := LocalIOResultK[string, SimpleConfig, string](loadConfig)(useConfig)
res := adapted("missing.json")()
// Error from loadConfig should propagate
assert.True(t, result.IsLeft(res))
})
t.Run("compose multiple LocalIOResultK", func(t *testing.T) {
// First transformation: string -> int (can fail)
parseID := func(s string) IOResult[int] {
return func() Result[int] {
if s == "" {
return result.Left[int](errors.New("empty string"))
}
return result.Of(len(s) * 10)
}
}
// Second transformation: int -> SimpleConfig (can fail)
loadConfig := func(id int) IOResult[SimpleConfig] {
return func() Result[SimpleConfig] {
if id < 0 {
return result.Left[SimpleConfig](errors.New("invalid ID"))
}
return result.Of(SimpleConfig{Port: 8000 + id})
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
// Compose transformations
step1 := LocalIOResultK[string, SimpleConfig, int](loadConfig)(formatConfig)
step2 := LocalIOResultK[string, int, string](parseID)(step1)
// Success case
res := step2("test")()
assert.Equal(t, result.Of("Port: 8040"), res)
// Failure in first transformation
resErr1 := step2("")()
assert.True(t, result.IsLeft(resErr1))
})
t.Run("real-world: load and validate config", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Read file (can fail)
readFile := func(cf ConfigFile) IOResult[string] {
return func() Result[string] {
if cf.Path == "" {
return result.Left[string](errors.New("empty path"))
}
return result.Of(`{"port":9000}`)
}
}
// Parse config (can fail)
parseConfig := func(content string) IOResult[SimpleConfig] {
return func() Result[SimpleConfig] {
if content == "" {
return result.Left[SimpleConfig](errors.New("empty content"))
}
return result.Of(SimpleConfig{Port: 9000})
}
}
// Use the config
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Using port: %d", cfg.Port))
}
}
// Compose the pipeline
step1 := LocalIOResultK[string, SimpleConfig, string](parseConfig)(useConfig)
step2 := LocalIOResultK[string, string, ConfigFile](readFile)(step1)
// Success case
res := step2(ConfigFile{Path: "app.json"})()
assert.Equal(t, result.Of("Using port: 9000"), res)
// Failure case
resErr := step2(ConfigFile{Path: ""})()
assert.True(t, result.IsLeft(resErr))
})
}

View File

@@ -0,0 +1,72 @@
package readerreaderioeither
import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerioeither"
)
//go:inline
func Local[C, E, A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return reader.Local[ReaderIOEither[C, E, A]](f)
}
//go:inline
func LocalIOK[C, E, A, R1, R2 any](f io.Kleisli[R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow4(
f,
io.Map(rri),
readerioeither.FromIO[C],
readerioeither.Flatten,
)
}
}
//go:inline
func LocalIOEitherK[C, A, R1, R2, E any](f ioeither.Kleisli[E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow4(
f,
ioeither.Map[E](rri),
readerioeither.FromIOEither[C],
readerioeither.Flatten,
)
}
}
//go:inline
func LocalEitherK[C, A, R1, R2, E any](f either.Kleisli[E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow4(
f,
either.Map[E](rri),
readerioeither.FromEither[C],
readerioeither.Flatten,
)
}
}
//go:inline
func LocalReaderIOEitherK[A, C, E, R1, R2 any](f readerioeither.Kleisli[C, E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow3(
f,
readerioeither.Map[C, E](rri),
readerioeither.Flatten,
)
}
}
//go:inline
func LocalReaderReaderIOEitherK[A, C, E, R1, R2 any](f Kleisli[R2, C, E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow2(
reader.AsksReader(f),
readerioeither.Chain(rri),
)
}
}

View File

@@ -271,6 +271,15 @@ func ChainReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R,
)
}
//go:inline
func ChainReaderIOEitherK[C, R, E, A, B any](f RIOE.Kleisli[R, E, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, C, E, A, B],
FromReaderIOEither[C, E, R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -595,11 +604,6 @@ func MapLeft[R, C, A, E1, E2 any](f func(E1) E2) func(ReaderReaderIOEither[R, C,
return reader.Map[R](RIOE.MapLeft[C, A](f))
}
//go:inline
func Local[C, E, A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return reader.Local[ReaderIOEither[C, E, A]](f)
}
//go:inline
func Read[C, E, A, R any](r R) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return reader.Read[ReaderIOEither[C, E, A]](r)
@@ -659,3 +663,13 @@ func Delay[R, C, E, A any](delay time.Duration) Operator[R, C, E, A, A] {
func After[R, C, E, A any](timestamp time.Time) Operator[R, C, E, A, A] {
return reader.Map[R](RIOE.After[C, E, A](timestamp))
}
func Defer[R, C, E, A any](fa Lazy[ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return func(r R) ReaderIOEither[C, E, A] {
return func(c C) RIOE.IOEither[E, A] {
return func() IOE.Either[E, A] {
return fa()(r)(c)()
}
}
}
}

View File

@@ -0,0 +1,15 @@
package readerreaderioeither
import (
RA "github.com/IBM/fp-go/v2/internal/array"
)
func TraverseArray[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Kleisli[R, C, E, []A, []B] {
return RA.Traverse[[]A, []B](
Of,
Map,
Ap,
f,
)
}

View File

@@ -20,6 +20,8 @@
package result
import (
"fmt"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
@@ -603,3 +605,54 @@ func MonadAlt[A any](fa Result[A], that Lazy[Result[A]]) Result[A] {
func Zero[A any]() Result[A] {
return either.Zero[error, A]()
}
// InstanceOf attempts to perform a type assertion on an any value to convert it to type A.
// If the type assertion succeeds, it returns a Right containing the converted value.
// If the type assertion fails, it returns a Left containing an error describing the type mismatch.
//
// This function is useful for safely converting interface{}/any values to concrete types
// in a functional programming style, where type assertion failures are represented as
// Left values rather than panics or boolean checks.
//
// Type Parameters:
// - A: The target type to convert to
//
// Parameters:
// - a: The value of type any to be type-asserted
//
// Returns:
// - Result[A]: Right(value) if type assertion succeeds, Left(error) if it fails
//
// Example:
//
// // Successful type assertion
// var value any = 42
// result := result.InstanceOf[int](value) // Right(42)
//
// // Failed type assertion
// var value any = "hello"
// result := result.InstanceOf[int](value) // Left(error: "expected int, got string")
//
// // Using with pipe for safe type conversion
// var data any = 3.14
// result := F.Pipe1(
// data,
// result.InstanceOf[float64],
// ) // Right(3.14)
//
// // Chaining with other operations
// var value any = 10
// result := F.Pipe2(
// value,
// result.InstanceOf[int],
// result.Map(func(n int) int { return n * 2 }),
// ) // Right(20)
//
//go:inline
func InstanceOf[A any](a any) Result[A] {
var res, ok = a.(A)
if ok {
return Of(res)
}
return Left[A](fmt.Errorf("expected %T, got %T", res, a))
}

View File

@@ -183,3 +183,176 @@ func TestZeroEqualsDefaultInitialization(t *testing.T) {
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
}
// TestInstanceOf tests the InstanceOf function for type assertions
func TestInstanceOf(t *testing.T) {
// Test successful type assertion with int
t.Run("successful int assertion", func(t *testing.T) {
var value any = 42
result := InstanceOf[int](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right(42), result)
})
// Test successful type assertion with string
t.Run("successful string assertion", func(t *testing.T) {
var value any = "hello"
result := InstanceOf[string](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right("hello"), result)
})
// Test successful type assertion with float64
t.Run("successful float64 assertion", func(t *testing.T) {
var value any = 3.14
result := InstanceOf[float64](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right(3.14), result)
})
// Test successful type assertion with pointer
t.Run("successful pointer assertion", func(t *testing.T) {
val := 42
var value any = &val
result := InstanceOf[*int](value)
assert.True(t, IsRight(result))
v, err := UnwrapError(result)
assert.NoError(t, err)
assert.Equal(t, 42, *v)
})
// Test successful type assertion with struct
t.Run("successful struct assertion", func(t *testing.T) {
type Person struct {
Name string
Age int
}
var value any = Person{Name: "Alice", Age: 30}
result := InstanceOf[Person](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right(Person{Name: "Alice", Age: 30}), result)
})
// Test failed type assertion - int to string
t.Run("failed int to string assertion", func(t *testing.T) {
var value any = 42
result := InstanceOf[string](value)
assert.True(t, IsLeft(result))
_, err := UnwrapError(result)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expected")
assert.Contains(t, err.Error(), "got")
})
// Test failed type assertion - string to int
t.Run("failed string to int assertion", func(t *testing.T) {
var value any = "hello"
result := InstanceOf[int](value)
assert.True(t, IsLeft(result))
_, err := UnwrapError(result)
assert.Error(t, err)
})
// Test failed type assertion - int to float64
t.Run("failed int to float64 assertion", func(t *testing.T) {
var value any = 42
result := InstanceOf[float64](value)
assert.True(t, IsLeft(result))
})
// Test with nil value
t.Run("nil value assertion", func(t *testing.T) {
var value any = nil
result := InstanceOf[string](value)
assert.True(t, IsLeft(result))
})
// Test chaining with Map
t.Run("chaining with Map", func(t *testing.T) {
var value any = 10
result := F.Pipe2(
value,
InstanceOf[int],
Map(func(n int) int { return n * 2 }),
)
assert.Equal(t, Right(20), result)
})
// Test chaining with Map on failed assertion
t.Run("chaining with Map on failed assertion", func(t *testing.T) {
var value any = "not a number"
result := F.Pipe2(
value,
InstanceOf[int],
Map(func(n int) int { return n * 2 }),
)
assert.True(t, IsLeft(result))
})
// Test with Chain for dependent operations
t.Run("chaining with Chain", func(t *testing.T) {
var value any = 5
result := F.Pipe2(
value,
InstanceOf[int],
Chain(func(n int) Result[string] {
if n > 0 {
return Right(fmt.Sprintf("positive: %d", n))
}
return Left[string](errors.New("not positive"))
}),
)
assert.Equal(t, Right("positive: 5"), result)
})
// Test with GetOrElse for default value
t.Run("GetOrElse with failed assertion", func(t *testing.T) {
var value any = "not an int"
result := F.Pipe2(
value,
InstanceOf[int],
GetOrElse(func(err error) int { return -1 }),
)
assert.Equal(t, -1, result)
})
// Test with GetOrElse for successful assertion
t.Run("GetOrElse with successful assertion", func(t *testing.T) {
var value any = 42
result := F.Pipe2(
value,
InstanceOf[int],
GetOrElse(func(err error) int { return -1 }),
)
assert.Equal(t, 42, result)
})
// Test with interface type
t.Run("interface type assertion", func(t *testing.T) {
var value any = errors.New("test error")
result := InstanceOf[error](value)
assert.True(t, IsRight(result))
v, err := UnwrapError(result)
assert.NoError(t, err)
assert.Equal(t, "test error", v.Error())
})
// Test with slice type
t.Run("slice type assertion", func(t *testing.T) {
var value any = []int{1, 2, 3}
result := InstanceOf[[]int](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right([]int{1, 2, 3}), result)
})
// Test with map type
t.Run("map type assertion", func(t *testing.T) {
var value any = map[string]int{"a": 1, "b": 2}
result := InstanceOf[map[string]int](value)
assert.True(t, IsRight(result))
v, err := UnwrapError(result)
assert.NoError(t, err)
assert.Equal(t, 1, v["a"])
assert.Equal(t, 2, v["b"])
})
}

View File

@@ -2,7 +2,7 @@ package builder
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-23 16:15:30.703391 +0100 CET m=+0.003782501
// 2026-01-27 10:18:05.2249315 +0100 CET m=+0.004416801
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"

View File

@@ -15,7 +15,9 @@
package lens
import "github.com/IBM/fp-go/v2/optics/lens/option"
import (
"github.com/IBM/fp-go/v2/optics/lens/option"
)
//go:generate go run ../../main.go lens --dir . --filename gen_lens.go

View File

@@ -2,7 +2,7 @@ package lens
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-23 16:09:35.747264 +0100 CET m=+0.003865601
// 2026-01-27 10:33:42.2879434 +0100 CET m=+0.002788201
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"