mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-11 23:17:16 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a37f379a3c | ||
|
|
ece0cd135d |
5
v2/consumer/types.go
Normal file
5
v2/consumer/types.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package consumer
|
||||
|
||||
type (
|
||||
Consumer[A any] = func(A)
|
||||
)
|
||||
@@ -31,6 +31,8 @@ import (
|
||||
// TenantID string
|
||||
// }
|
||||
// result := readereither.Do(State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderResult[S] {
|
||||
@@ -78,6 +80,8 @@ func Do[S any](
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
@@ -86,6 +90,8 @@ func Bind[S1, S2, T any](
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
@@ -94,6 +100,8 @@ func Let[S1, S2, T any](
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
@@ -102,6 +110,8 @@ func LetTo[S1, S2, T any](
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Kleisli[ReaderResult[T], S1] {
|
||||
@@ -145,6 +155,8 @@ func BindTo[S1, T any](
|
||||
// getTenantID,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderResult[T],
|
||||
@@ -183,6 +195,8 @@ func ApS[S1, S2, T any](
|
||||
// readereither.Do(Person{Name: "Alice", Age: 25}),
|
||||
// readereither.ApSL(ageLens, getAge),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderResult[T],
|
||||
@@ -227,6 +241,8 @@ func ApSL[S, T any](
|
||||
// readereither.Of[error](Counter{Value: 42}),
|
||||
// readereither.BindL(valueLens, increment),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
@@ -262,6 +278,8 @@ func BindL[S, T any](
|
||||
// readereither.LetL(valueLens, double),
|
||||
// )
|
||||
// // result when executed will be Right(Counter{Value: 42})
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
@@ -296,6 +314,8 @@ func LetL[S, T any](
|
||||
// readereither.LetToL(debugLens, false),
|
||||
// )
|
||||
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
@@ -7,11 +22,131 @@ import (
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// SequenceReader swaps the order of environment parameters when the inner computation is a Reader.
|
||||
//
|
||||
// This function is specialized for the context.Context-based ReaderResult monad. It takes a
|
||||
// ReaderResult that produces a Reader and returns a reader.Kleisli that produces Results.
|
||||
// The context.Context is implicitly used as the outer environment type.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The inner environment type (becomes outer after flip)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderResult that takes context.Context and may produce a Reader[R, A]
|
||||
//
|
||||
// Returns:
|
||||
// - A reader.Kleisli[context.Context, R, Result[A]], which is func(context.Context) func(R) Result[A]
|
||||
//
|
||||
// The function preserves error handling from the outer ReaderResult layer. If the outer
|
||||
// computation fails, the error is propagated to the inner Result.
|
||||
//
|
||||
// Note: This is an inline wrapper around readerresult.SequenceReader, specialized for
|
||||
// context.Context as the outer environment type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// // Original: takes context, may fail, produces Reader[Database, string]
|
||||
// original := func(ctx context.Context) result.Result[reader.Reader[Database, string]] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[reader.Reader[Database, string]](ctx.Err())
|
||||
// }
|
||||
// return result.Ok[error](func(db Database) string {
|
||||
// return fmt.Sprintf("Query on %s", db.ConnectionString)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Sequenced: takes context first, then Database
|
||||
// sequenced := SequenceReader(original)
|
||||
//
|
||||
// ctx := context.Background()
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
//
|
||||
// // Apply context first to get a function that takes database
|
||||
// dbReader := sequenced(ctx)
|
||||
// // Then apply database to get the final result
|
||||
// result := dbReader(db)
|
||||
// // result is Result[string]
|
||||
//
|
||||
// Use Cases:
|
||||
// - Dependency injection: Flip parameter order to inject context first, then dependencies
|
||||
// - Testing: Separate context handling from business logic for easier testing
|
||||
// - Composition: Enable point-free style by fixing the context parameter first
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] {
|
||||
return RR.SequenceReader(ma)
|
||||
}
|
||||
|
||||
// TraverseReader transforms a value using a Reader function and swaps environment parameter order.
|
||||
//
|
||||
// This function combines mapping and parameter flipping in a single operation. It takes a
|
||||
// Reader function (pure computation without error handling) and returns a function that:
|
||||
// 1. Maps a ReaderResult[A] to ReaderResult[B] using the provided Reader function
|
||||
// 2. Flips the parameter order so R comes before context.Context
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The inner environment type (becomes outer after flip)
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A reader.Kleisli[R, A, B], which is func(R) func(A) B - a pure Reader function
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes ReaderResult[A] and returns Kleisli[R, B]
|
||||
// - Kleisli[R, B] is func(R) ReaderResult[B], which is func(R) func(context.Context) Result[B]
|
||||
//
|
||||
// The function preserves error handling from the input ReaderResult. If the input computation
|
||||
// fails, the error is propagated without applying the transformation function.
|
||||
//
|
||||
// Note: This is a wrapper around readerresult.TraverseReader, specialized for context.Context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxRetries int
|
||||
// }
|
||||
//
|
||||
// // A pure Reader function that depends on Config
|
||||
// formatMessage := func(cfg Config) func(int) string {
|
||||
// return func(value int) string {
|
||||
// return fmt.Sprintf("Value: %d, MaxRetries: %d", value, cfg.MaxRetries)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Original computation that may fail
|
||||
// computation := func(ctx context.Context) result.Result[int] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[int](ctx.Err())
|
||||
// }
|
||||
// return result.Ok[error](42)
|
||||
// }
|
||||
//
|
||||
// // Create a traversal that applies formatMessage and flips parameters
|
||||
// traverse := TraverseReader[Config, int, string](formatMessage)
|
||||
//
|
||||
// // Apply to the computation
|
||||
// flipped := traverse(computation)
|
||||
//
|
||||
// // Now we can provide Config first, then context
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// result := flipped(cfg)(ctx)
|
||||
// // result is Result[string] containing "Value: 42, MaxRetries: 3"
|
||||
//
|
||||
// Use Cases:
|
||||
// - Dependency injection: Inject configuration/dependencies before context
|
||||
// - Testing: Separate pure business logic from context handling
|
||||
// - Composition: Build pipelines where dependencies are fixed before execution
|
||||
// - Point-free style: Enable partial application by fixing dependencies first
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderResult[A]) Kleisli[R, B] {
|
||||
|
||||
231
v2/context/readerresult/logging.go
Normal file
231
v2/context/readerresult/logging.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package readerresult provides logging utilities for the ReaderResult monad,
|
||||
// which combines the Reader monad (for dependency injection via context.Context)
|
||||
// with the Result monad (for error handling).
|
||||
//
|
||||
// The logging functions in this package allow you to log Result values (both
|
||||
// successes and errors) while preserving the functional composition style.
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
var (
|
||||
// slogError creates a slog.Attr with key "error" for logging error values
|
||||
slogError = F.Bind1st(slog.Any, "error")
|
||||
// slogValue creates a slog.Attr with key "value" for logging success values
|
||||
slogValue = F.Bind1st(slog.Any, "value")
|
||||
)
|
||||
|
||||
// curriedLog creates a curried logging function that takes an slog.Attr and a context,
|
||||
// then logs the attribute with the specified log level and message.
|
||||
//
|
||||
// This is an internal helper function used to create the logging pipeline in a
|
||||
// point-free style. The currying allows for partial application in functional
|
||||
// composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelError)
|
||||
// - cb: A callback function that retrieves a logger from the context
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A curried function that takes an slog.Attr, then a context, and performs logging
|
||||
func curriedLog(
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) func(slog.Attr) Reader[context.Context, struct{}] {
|
||||
return F.Curry2(func(a slog.Attr, ctx context.Context) struct{} {
|
||||
cb(ctx).LogAttrs(ctx, logLevel, message, a)
|
||||
return struct{}{}
|
||||
})
|
||||
}
|
||||
|
||||
// SLogWithCallback creates a Kleisli arrow that logs a Result value using a custom
|
||||
// logger callback and log level. The Result value is logged and then returned unchanged,
|
||||
// making this function suitable for use in functional pipelines.
|
||||
//
|
||||
// This function logs both successful values and errors:
|
||||
// - Success values are logged with the key "value"
|
||||
// - Error values are logged with the key "error"
|
||||
//
|
||||
// The logging is performed as a side effect while preserving the Result value,
|
||||
// allowing it to be used in the middle of a computation pipeline without
|
||||
// interrupting the flow.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value in the Result
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelDebug, LevelError)
|
||||
// - cb: A callback function that retrieves a *slog.Logger from the context
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
|
||||
// The returned ReaderResult, when executed with a context, logs the Result
|
||||
// and returns it unchanged
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// // Custom logger callback
|
||||
// getLogger := func(ctx context.Context) *slog.Logger {
|
||||
// return slog.Default()
|
||||
// }
|
||||
//
|
||||
// // Create a logging function for debug level
|
||||
// logDebug := SLogWithCallback[User](slog.LevelDebug, getLogger, "User data")
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// ctx := context.Background()
|
||||
// user := result.Of(User{ID: 123, Name: "Alice"})
|
||||
// logged := logDebug(user)(ctx) // Logs: level=DEBUG msg="User data" value={ID:123 Name:Alice}
|
||||
// // logged still contains the User value
|
||||
//
|
||||
// Example with error:
|
||||
//
|
||||
// err := errors.New("user not found")
|
||||
// userResult := result.Left[User](err)
|
||||
// logged := logDebug(userResult)(ctx) // Logs: level=DEBUG msg="User data" error="user not found"
|
||||
// // logged still contains the error
|
||||
func SLogWithCallback[A any](
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) reader.Kleisli[context.Context, Result[A], Result[A]] {
|
||||
|
||||
return F.Pipe1(
|
||||
F.Flow2(
|
||||
result.Fold(
|
||||
F.Flow2(
|
||||
F.ToAny[error],
|
||||
slogError,
|
||||
),
|
||||
F.Flow2(
|
||||
F.ToAny[A],
|
||||
slogValue,
|
||||
),
|
||||
),
|
||||
curriedLog(logLevel, cb, message),
|
||||
),
|
||||
reader.Chain(reader.Sequence(F.Flow2(
|
||||
reader.Of[struct{}, Result[A]],
|
||||
reader.Map[context.Context, struct{}, Result[A]],
|
||||
))),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// SLog creates a Kleisli arrow that logs a Result value at INFO level using the
|
||||
// logger from the context. This is a convenience function that uses SLogWithCallback
|
||||
// with default settings.
|
||||
//
|
||||
// The Result value is logged and then returned unchanged, making this function
|
||||
// suitable for use in functional pipelines for debugging or monitoring purposes.
|
||||
//
|
||||
// This function logs both successful values and errors:
|
||||
// - Success values are logged with the key "value"
|
||||
// - Error values are logged with the key "error"
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value in the Result
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
|
||||
// The returned ReaderResult, when executed with a context, logs the Result
|
||||
// at INFO level and returns it unchanged
|
||||
//
|
||||
// Example - Logging a successful computation:
|
||||
//
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// // Simple value logging
|
||||
// res := result.Of(42)
|
||||
// logged := SLog[int]("Processing number")(res)(ctx)
|
||||
// // Logs: level=INFO msg="Processing number" value=42
|
||||
// // logged == result.Of(42)
|
||||
//
|
||||
// Example - Logging in a pipeline:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// fetchUser := func(id int) result.Result[User] {
|
||||
// return result.Of(User{ID: id, Name: "Alice"})
|
||||
// }
|
||||
//
|
||||
// processUser := func(user User) result.Result[string] {
|
||||
// return result.Of(fmt.Sprintf("Processed: %s", user.Name))
|
||||
// }
|
||||
//
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// // Log at each step
|
||||
// userResult := fetchUser(123)
|
||||
// logged1 := SLog[User]("Fetched user")(userResult)(ctx)
|
||||
// // Logs: level=INFO msg="Fetched user" value={ID:123 Name:Alice}
|
||||
//
|
||||
// processed := result.Chain(processUser)(logged1)
|
||||
// logged2 := SLog[string]("Processed user")(processed)(ctx)
|
||||
// // Logs: level=INFO msg="Processed user" value="Processed: Alice"
|
||||
//
|
||||
// Example - Logging errors:
|
||||
//
|
||||
// err := errors.New("database connection failed")
|
||||
// errResult := result.Left[User](err)
|
||||
// logged := SLog[User]("Database operation")(errResult)(ctx)
|
||||
// // Logs: level=INFO msg="Database operation" error="database connection failed"
|
||||
// // logged still contains the error
|
||||
//
|
||||
// Example - Using with context logger:
|
||||
//
|
||||
// // Set up a custom logger in the context
|
||||
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
// ctx := logging.WithLogger(logger)(context.Background())
|
||||
//
|
||||
// res := result.Of("important data")
|
||||
// logged := SLog[string]("Critical operation")(res)(ctx)
|
||||
// // Uses the logger from context to log the message
|
||||
//
|
||||
// Note: The function uses logging.GetLoggerFromContext to retrieve the logger,
|
||||
// which falls back to the global logger if no logger is found in the context.
|
||||
//
|
||||
//go:inline
|
||||
func SLog[A any](message string) reader.Kleisli[context.Context, Result[A], Result[A]] {
|
||||
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapSLog[A any](message string) Operator[A, A] {
|
||||
return reader.Chain(SLog[A](message))
|
||||
}
|
||||
302
v2/context/readerresult/logging_test.go
Normal file
302
v2/context/readerresult/logging_test.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSLogLogsSuccessValue tests that SLog logs successful Result values
|
||||
func TestSLogLogsSuccessValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a Result and log it
|
||||
res1 := result.Of(42)
|
||||
logged := SLog[int]("Result value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Result value")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
}
|
||||
|
||||
// TestSLogLogsErrorValue tests that SLog logs error Result values
|
||||
func TestSLogLogsErrorValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Create an error Result and log it
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLog[int]("Result value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, res1, logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Result value")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "test error")
|
||||
}
|
||||
|
||||
// TestSLogInPipeline tests SLog in a functional pipeline
|
||||
func TestSLogInPipeline(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// SLog takes a Result[A] and returns ReaderResult[A]
|
||||
// So we need to start with a Result, apply SLog, then execute with context
|
||||
res1 := result.Of(10)
|
||||
logged := SLog[int]("Initial value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(10), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Initial value")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
}
|
||||
|
||||
// TestSLogWithContextLogger tests SLog using logger from context
|
||||
func TestSLogWithContextLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
|
||||
res1 := result.Of("test value")
|
||||
logged := SLog[string]("Context logger test")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of("test value"), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Context logger test")
|
||||
assert.Contains(t, logOutput, `value="test value"`)
|
||||
}
|
||||
|
||||
// TestSLogDisabled tests that SLog respects logger level
|
||||
func TestSLogDisabled(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Create logger with level that disables info logs
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelError, // Only log errors
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
res1 := result.Of(42)
|
||||
logged := SLog[int]("This should not be logged")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
// Should have no logs since level is ERROR
|
||||
logOutput := buf.String()
|
||||
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
|
||||
}
|
||||
|
||||
// TestSLogWithStruct tests SLog with structured data
|
||||
func TestSLogWithStruct(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
|
||||
res1 := result.Of(user)
|
||||
logged := SLog[User]("User data")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(user), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "User data")
|
||||
assert.Contains(t, logOutput, "ID:123")
|
||||
assert.Contains(t, logOutput, "Name:Alice")
|
||||
}
|
||||
|
||||
// TestSLogWithCallbackCustomLevel tests SLogWithCallback with custom log level
|
||||
func TestSLogWithCallbackCustomLevel(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a Result and log it with custom callback
|
||||
res1 := result.Of(42)
|
||||
logged := SLogWithCallback[int](slog.LevelDebug, customCallback, "Debug result")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Debug result")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
assert.Contains(t, logOutput, "level=DEBUG")
|
||||
}
|
||||
|
||||
// TestSLogWithCallbackLogsError tests SLogWithCallback logs errors
|
||||
func TestSLogWithCallbackLogsError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelWarn,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("warning error")
|
||||
|
||||
// Create an error Result and log it with custom callback
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLogWithCallback[int](slog.LevelWarn, customCallback, "Warning result")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, res1, logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Warning result")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "warning error")
|
||||
assert.Contains(t, logOutput, "level=WARN")
|
||||
}
|
||||
|
||||
// TestSLogChainedOperations tests SLog in chained operations
|
||||
func TestSLogChainedOperations(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// First log step 1
|
||||
res1 := result.Of(5)
|
||||
logged1 := SLog[int]("Step 1")(res1)(ctx)
|
||||
|
||||
// Then log step 2 with doubled value
|
||||
res2 := result.Map(N.Mul(2))(logged1)
|
||||
logged2 := SLog[int]("Step 2")(res2)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(10), logged2)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Step 1")
|
||||
assert.Contains(t, logOutput, "value=5")
|
||||
assert.Contains(t, logOutput, "Step 2")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
}
|
||||
|
||||
// TestSLogPreservesError tests that SLog preserves error through the pipeline
|
||||
func TestSLogPreservesError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("original error")
|
||||
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLog[int]("Logging error")(res1)(ctx)
|
||||
|
||||
// Apply map to verify error is preserved
|
||||
res2 := result.Map(N.Mul(2))(logged)
|
||||
|
||||
assert.Equal(t, res1, res2)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Logging error")
|
||||
assert.Contains(t, logOutput, "original error")
|
||||
}
|
||||
|
||||
// TestSLogMultipleValues tests logging multiple different values
|
||||
func TestSLogMultipleValues(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with different types
|
||||
intRes := SLog[int]("Integer")(result.Of(42))(ctx)
|
||||
assert.Equal(t, result.Of(42), intRes)
|
||||
|
||||
strRes := SLog[string]("String")(result.Of("hello"))(ctx)
|
||||
assert.Equal(t, result.Of("hello"), strRes)
|
||||
|
||||
boolRes := SLog[bool]("Boolean")(result.Of(true))(ctx)
|
||||
assert.Equal(t, result.Of(true), boolRes)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Integer")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
assert.Contains(t, logOutput, "String")
|
||||
assert.Contains(t, logOutput, "value=hello")
|
||||
assert.Contains(t, logOutput, "Boolean")
|
||||
assert.Contains(t, logOutput, "value=true")
|
||||
}
|
||||
@@ -18,9 +18,15 @@ package readerresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
|
||||
return readereither.FromReader[error](r)
|
||||
}
|
||||
|
||||
func FromEither[A any](e Either[A]) ReaderResult[A] {
|
||||
return readereither.FromEither[context.Context](e)
|
||||
}
|
||||
@@ -97,3 +103,197 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
|
||||
return readereither.Read[error, A](r)
|
||||
}
|
||||
|
||||
// MonadMapTo executes a ReaderResult computation, discards its success value, and returns a constant value.
|
||||
// This is the monadic version that takes both the ReaderResult and the constant value as parameters.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadMapTo WILL
|
||||
// execute the original ReaderResult to allow any side effects to occur, then discard the success result
|
||||
// and return the constant value. If the original computation fails, the error is preserved.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The type of the constant value to return on success
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderResult to execute (side effects will occur, success value discarded)
|
||||
// - b: The constant value to return if ma succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that executes ma, preserves errors, but replaces success values with b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int }
|
||||
// increment := func(ctx context.Context) result.Result[int] {
|
||||
// // Side effect: log the operation
|
||||
// fmt.Println("incrementing")
|
||||
// return result.Of(5)
|
||||
// }
|
||||
// r := readerresult.MonadMapTo(increment, "done")
|
||||
// result := r(context.Background()) // Prints "incrementing", returns Right("done")
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](ma ReaderResult[A], b B) ReaderResult[B] {
|
||||
return MonadMap(ma, reader.Of[A](b))
|
||||
}
|
||||
|
||||
// MapTo creates an operator that executes a ReaderResult computation, discards its success value,
|
||||
// and returns a constant value. This is the curried version where the constant value is provided first,
|
||||
// returning a function that can be applied to any ReaderResult.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MapTo WILL
|
||||
// execute the input ReaderResult to allow any side effects to occur, then discard the success result
|
||||
// and return the constant value. If the computation fails, the error is preserved.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the input ReaderResult (will be discarded if successful)
|
||||
// - B: The type of the constant value to return on success
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to return on success
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that executes a ReaderResult[A], preserves errors, but replaces success with b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logStep := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("step executed")
|
||||
// return result.Of(42)
|
||||
// }
|
||||
// toDone := readerresult.MapTo[int, string]("done")
|
||||
// pipeline := toDone(logStep)
|
||||
// result := pipeline(context.Background()) // Prints "step executed", returns Right("done")
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// step1 := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("processing")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// readerresult.MapTo[int, string]("complete"),
|
||||
// )
|
||||
// output := pipeline(context.Background()) // Prints "processing", returns Right("complete")
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return Map(reader.Of[A](b))
|
||||
}
|
||||
|
||||
// MonadChainTo sequences two ReaderResult computations where the second ignores the first's success value.
|
||||
// This is the monadic version that takes both ReaderResults as parameters.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadChainTo WILL
|
||||
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
|
||||
// execute the second ReaderResult with the same context. If the first computation fails, the error is
|
||||
// returned immediately without executing the second computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The success type of the second ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderResult to execute (side effects will occur, success value discarded)
|
||||
// - b: The second ReaderResult to execute if ma succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that executes ma, then b if ma succeeds, returning b's result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logStart := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("starting")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// logEnd := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("ending")
|
||||
// return result.Of("done")
|
||||
// }
|
||||
// r := readerresult.MonadChainTo(logStart, logEnd)
|
||||
// result := r(context.Background()) // Prints "starting" then "ending", returns Right("done")
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainTo[A, B any](ma ReaderResult[A], b ReaderResult[B]) ReaderResult[B] {
|
||||
return MonadChain(ma, reader.Of[A](b))
|
||||
}
|
||||
|
||||
// ChainTo creates an operator that sequences two ReaderResult computations where the second ignores
|
||||
// the first's success value. This is the curried version where the second ReaderResult is provided first,
|
||||
// returning a function that can be applied to any first ReaderResult.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, ChainTo WILL
|
||||
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
|
||||
// execute the second ReaderResult with the same context. If the first computation fails, the error is
|
||||
// returned immediately without executing the second computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The success type of the second ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The second ReaderResult to execute after the first succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that executes the first ReaderResult, then b if successful
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logEnd := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("ending")
|
||||
// return result.Of("done")
|
||||
// }
|
||||
// thenLogEnd := readerresult.ChainTo[int, string](logEnd)
|
||||
//
|
||||
// logStart := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("starting")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// pipeline := thenLogEnd(logStart)
|
||||
// result := pipeline(context.Background()) // Prints "starting" then "ending", returns Right("done")
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// step1 := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("step 1")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// step2 := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("step 2")
|
||||
// return result.Of("complete")
|
||||
// }
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// readerresult.ChainTo[int, string](step2),
|
||||
// )
|
||||
// output := pipeline(context.Background()) // Prints "step 1" then "step 2", returns Right("complete")
|
||||
//
|
||||
//go:inline
|
||||
func ChainTo[A, B any](b ReaderResult[B]) Operator[A, B] {
|
||||
return Chain(reader.Of[A](b))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[A] {
|
||||
return chain.MonadChainFirst(
|
||||
MonadChain,
|
||||
MonadMap,
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(
|
||||
Chain,
|
||||
Map,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
315
v2/context/readerresult/reader_test.go
Normal file
315
v2/context/readerresult/reader_test.go
Normal file
@@ -0,0 +1,315 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
// Apply MapTo operator
|
||||
toDone := MapTo[int]("done")
|
||||
resultReader := toDone(originalReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
// Verify the original reader WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original reader should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes reader in functional pipeline", func(t *testing.T) {
|
||||
executed := false
|
||||
step1 := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](100)
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
MapTo[int]("complete"),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("complete"), result)
|
||||
assert.True(t, executed, "original reader should be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("executes reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
|
||||
sideEffectOccurred = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
resultReader := MapTo[int](true)(readerWithSideEffect)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](true), result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur")
|
||||
})
|
||||
|
||||
t.Run("preserves errors from original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
testErr := assert.AnError
|
||||
failingReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
|
||||
resultReader := MapTo[int]("done")(failingReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
assert.True(t, executed, "failing reader should still be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
// Apply MonadMapTo
|
||||
resultReader := MonadMapTo(originalReader, "done")
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
// Verify the original reader WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original reader should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes complex computation with side effects", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReader := func(ctx context.Context) E.Either[error, string] {
|
||||
computationExecuted = true
|
||||
return E.Of[error]("complex result")
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(complexReader, 42)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, computationExecuted, "complex computation should be executed")
|
||||
})
|
||||
|
||||
t.Run("preserves errors from original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
testErr := assert.AnError
|
||||
failingReader := func(ctx context.Context) E.Either[error, []string] {
|
||||
executed = true
|
||||
return E.Left[[]string](testErr)
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(failingReader, 99)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
assert.True(t, executed, "failing reader should still be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainTo(t *testing.T) {
|
||||
t.Run("executes first reader then second reader on success", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
firstReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
// Apply ChainTo operator
|
||||
thenSecond := ChainTo[int](secondReader)
|
||||
resultReader := thenSecond(firstReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
// Verify both readers were executed
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("executes both readers in functional pipeline", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
step1 := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](100)
|
||||
}
|
||||
|
||||
step2 := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("complete")
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
ChainTo[int](step2),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("complete"), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed in pipeline")
|
||||
assert.True(t, secondExecuted, "second reader should be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("executes first reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
|
||||
sideEffectOccurred = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, bool] {
|
||||
return E.Of[error](true)
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](true), result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur in first reader")
|
||||
})
|
||||
|
||||
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
testErr := assert.AnError
|
||||
|
||||
failingReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(failingReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainTo(t *testing.T) {
|
||||
t.Run("executes first reader then second reader on success", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
firstReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
// Apply MonadChainTo
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
// Verify both readers were executed
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("executes complex first computation with side effects", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
complexFirstReader := func(ctx context.Context) E.Either[error, []int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error]([]int{1, 2, 3})
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("done")
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(complexFirstReader, secondReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
assert.True(t, firstExecuted, "complex first computation should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
testErr := assert.AnError
|
||||
|
||||
failingReader := func(ctx context.Context) E.Either[error, map[string]int] {
|
||||
firstExecuted = true
|
||||
return E.Left[map[string]int](testErr)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, float64] {
|
||||
secondExecuted = true
|
||||
return E.Of[error](3.14)
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(failingReader, secondReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[float64](testErr), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
9917
v2/coverage.out
9917
v2/coverage.out
File diff suppressed because it is too large
Load Diff
@@ -117,9 +117,13 @@ func Nullary2[F1 ~func() T1, F2 ~func(T1) T2, T1, T2 any](f1 F1, f2 F2) func() T
|
||||
|
||||
// Curry2 takes a function with 2 parameters and returns a cascade of functions each taking only one parameter.
|
||||
// The inverse function is [Uncurry2]
|
||||
//go:inline
|
||||
func Curry2[FCT ~func(T0, T1) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) T2 {
|
||||
//go:inline
|
||||
return func(t0 T0) func(t1 T1) T2 {
|
||||
//go:inline
|
||||
return func(t1 T1) T2 {
|
||||
//go:inline
|
||||
return f(t0, t1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,61 @@ func Sequence[R1, R2, A any](ma Reader[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return function.Flip(ma)
|
||||
}
|
||||
|
||||
// Traverse applies a Kleisli arrow to a value wrapped in a Reader, then sequences the result.
|
||||
// It transforms a Reader[R2, A] into a function that takes R1 and returns Reader[R2, B],
|
||||
// where the transformation from A to B is defined by a Kleisli arrow that depends on R1.
|
||||
//
|
||||
// This is useful when you have a Reader computation that produces a value, and you want to
|
||||
// apply another Reader computation to that value, but with a different environment type.
|
||||
// The result is a function that takes the second environment and returns a Reader that
|
||||
// takes the first environment.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R2: The first environment type (outer Reader)
|
||||
// - R1: The second environment type (inner Reader/Kleisli)
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow from A to B that depends on environment R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Reader[R2, A] and returns a Kleisli[R2, R1, B]
|
||||
//
|
||||
// The signature can be understood as:
|
||||
// - Input: Reader[R2, A] (a computation that produces A given R2)
|
||||
// - Output: func(R1) Reader[R2, B] (a function that takes R1 and produces a computation that produces B given R2)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct { ConnectionString string }
|
||||
// type Config struct { TableName string }
|
||||
//
|
||||
// // A Reader that gets a user ID from the database
|
||||
// getUserID := func(db Database) int {
|
||||
// // Simulate database query
|
||||
// return 42
|
||||
// }
|
||||
//
|
||||
// // A Kleisli arrow that takes a user ID and returns a Reader that formats it with config
|
||||
// formatUser := func(id int) reader.Reader[Config, string] {
|
||||
// return func(c Config) string {
|
||||
// return fmt.Sprintf("User %d from table %s", id, c.TableName)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Traverse applies formatUser to the result of getUserID
|
||||
// traversed := reader.Traverse(formatUser)(getUserID)
|
||||
//
|
||||
// // Now we can apply both environments
|
||||
// config := Config{TableName: "users"}
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
// result := traversed(config)(db) // "User 42 from table users"
|
||||
//
|
||||
// The Traverse operation is particularly useful when:
|
||||
// - You need to compose computations that depend on different environments
|
||||
// - You want to apply a transformation that itself requires environmental context
|
||||
// - You're building pipelines where each stage has its own configuration
|
||||
func Traverse[R2, R1, A, B any](
|
||||
f Kleisli[R1, A, B],
|
||||
) func(Reader[R2, A]) Kleisli[R2, R1, B] {
|
||||
|
||||
@@ -338,3 +338,371 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
assert.Equal(t, "value: 42", sequenced(transform)(42))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverse(t *testing.T) {
|
||||
t.Run("basic traverse with two environments", func(t *testing.T) {
|
||||
type Database struct {
|
||||
UserID int
|
||||
}
|
||||
type Config struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Reader that gets user ID from database
|
||||
getUserID := func(db Database) int {
|
||||
return db.UserID
|
||||
}
|
||||
|
||||
// Kleisli that formats user ID with config
|
||||
formatUser := func(id int) Reader[Config, string] {
|
||||
return func(c Config) string {
|
||||
return fmt.Sprintf("%s%d", c.Prefix, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse applies formatUser to the result of getUserID
|
||||
traversed := Traverse[Database](formatUser)(getUserID)
|
||||
|
||||
// Apply both environments
|
||||
config := Config{Prefix: "User-"}
|
||||
db := Database{UserID: 42}
|
||||
result := traversed(config)(db)
|
||||
|
||||
assert.Equal(t, "User-42", result)
|
||||
})
|
||||
|
||||
t.Run("traverse with computation", func(t *testing.T) {
|
||||
type Source struct {
|
||||
Value int
|
||||
}
|
||||
type Multiplier struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
// Reader that extracts value from source
|
||||
getValue := func(s Source) int {
|
||||
return s.Value
|
||||
}
|
||||
|
||||
// Kleisli that multiplies value with multiplier
|
||||
multiply := func(n int) Reader[Multiplier, int] {
|
||||
return func(m Multiplier) int {
|
||||
return n * m.Factor
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Source](multiply)(getValue)
|
||||
|
||||
source := Source{Value: 10}
|
||||
multiplier := Multiplier{Factor: 5}
|
||||
result := traversed(multiplier)(source)
|
||||
|
||||
assert.Equal(t, 50, result)
|
||||
})
|
||||
|
||||
t.Run("traverse with string transformation", func(t *testing.T) {
|
||||
type Input struct {
|
||||
Text string
|
||||
}
|
||||
type Format struct {
|
||||
Template string
|
||||
}
|
||||
|
||||
// Reader that gets text from input
|
||||
getText := func(i Input) string {
|
||||
return i.Text
|
||||
}
|
||||
|
||||
// Kleisli that formats text with template
|
||||
format := func(text string) Reader[Format, string] {
|
||||
return func(f Format) string {
|
||||
return fmt.Sprintf(f.Template, text)
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Input](format)(getText)
|
||||
|
||||
input := Input{Text: "world"}
|
||||
formatCfg := Format{Template: "Hello, %s!"}
|
||||
result := traversed(formatCfg)(input)
|
||||
|
||||
assert.Equal(t, "Hello, world!", result)
|
||||
})
|
||||
|
||||
t.Run("traverse with boolean logic", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
type Threshold struct {
|
||||
Limit int
|
||||
}
|
||||
|
||||
// Reader that gets value from data
|
||||
getValue := func(d Data) int {
|
||||
return d.Value
|
||||
}
|
||||
|
||||
// Kleisli that checks if value exceeds threshold
|
||||
checkThreshold := func(val int) Reader[Threshold, bool] {
|
||||
return func(t Threshold) bool {
|
||||
return val > t.Limit
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Data](checkThreshold)(getValue)
|
||||
|
||||
data := Data{Value: 100}
|
||||
threshold := Threshold{Limit: 50}
|
||||
result := traversed(threshold)(data)
|
||||
|
||||
assert.True(t, result)
|
||||
|
||||
threshold2 := Threshold{Limit: 150}
|
||||
result2 := traversed(threshold2)(data)
|
||||
|
||||
assert.False(t, result2)
|
||||
})
|
||||
|
||||
t.Run("traverse with slice transformation", func(t *testing.T) {
|
||||
type Source struct {
|
||||
Items []string
|
||||
}
|
||||
type Config struct {
|
||||
Separator string
|
||||
}
|
||||
|
||||
// Reader that gets items from source
|
||||
getItems := func(s Source) []string {
|
||||
return s.Items
|
||||
}
|
||||
|
||||
// Kleisli that joins items with separator
|
||||
joinItems := func(items []string) Reader[Config, string] {
|
||||
return func(c Config) string {
|
||||
result := ""
|
||||
for i, item := range items {
|
||||
if i > 0 {
|
||||
result += c.Separator
|
||||
}
|
||||
result += item
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Source](joinItems)(getItems)
|
||||
|
||||
source := Source{Items: []string{"a", "b", "c"}}
|
||||
config := Config{Separator: ", "}
|
||||
result := traversed(config)(source)
|
||||
|
||||
assert.Equal(t, "a, b, c", result)
|
||||
})
|
||||
|
||||
t.Run("traverse with struct transformation", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
type Database struct {
|
||||
TablePrefix string
|
||||
}
|
||||
type UserRecord struct {
|
||||
Table string
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
// Reader that gets user
|
||||
getUser := func(db Database) User {
|
||||
return User{ID: 123, Name: "Alice"}
|
||||
}
|
||||
|
||||
// Kleisli that creates user record
|
||||
createRecord := func(user User) Reader[Database, UserRecord] {
|
||||
return func(db Database) UserRecord {
|
||||
return UserRecord{
|
||||
Table: db.TablePrefix + "users",
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Database](createRecord)(getUser)
|
||||
|
||||
db := Database{TablePrefix: "prod_"}
|
||||
result := traversed(db)(db)
|
||||
|
||||
assert.Equal(t, UserRecord{
|
||||
Table: "prod_users",
|
||||
ID: 123,
|
||||
Name: "Alice",
|
||||
}, result)
|
||||
})
|
||||
|
||||
t.Run("traverse with nil handling", func(t *testing.T) {
|
||||
type Source struct {
|
||||
Value *int
|
||||
}
|
||||
type Config struct {
|
||||
Default int
|
||||
}
|
||||
|
||||
// Reader that gets pointer value
|
||||
getValue := func(s Source) *int {
|
||||
return s.Value
|
||||
}
|
||||
|
||||
// Kleisli that handles nil with default
|
||||
handleNil := func(ptr *int) Reader[Config, int] {
|
||||
return func(c Config) int {
|
||||
if ptr == nil {
|
||||
return c.Default
|
||||
}
|
||||
return *ptr
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Source](handleNil)(getValue)
|
||||
|
||||
config := Config{Default: 999}
|
||||
|
||||
// Test with non-nil value
|
||||
val := 42
|
||||
source1 := Source{Value: &val}
|
||||
result1 := traversed(config)(source1)
|
||||
assert.Equal(t, 42, result1)
|
||||
|
||||
// Test with nil value
|
||||
source2 := Source{Value: nil}
|
||||
result2 := traversed(config)(source2)
|
||||
assert.Equal(t, 999, result2)
|
||||
})
|
||||
|
||||
t.Run("traverse composition", func(t *testing.T) {
|
||||
type Env1 struct {
|
||||
Base int
|
||||
}
|
||||
type Env2 struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Reader that gets base value
|
||||
getBase := func(e Env1) int {
|
||||
return e.Base
|
||||
}
|
||||
|
||||
// Kleisli that multiplies
|
||||
multiply := func(n int) Reader[Env2, int] {
|
||||
return func(e Env2) int {
|
||||
return n * e.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Another Kleisli that adds
|
||||
add := func(n int) Reader[Env2, int] {
|
||||
return func(e Env2) int {
|
||||
return n + e.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse with multiply
|
||||
traversed1 := Traverse[Env1](multiply)(getBase)
|
||||
env1 := Env1{Base: 10}
|
||||
env2 := Env2{Multiplier: 5}
|
||||
result1 := traversed1(env2)(env1)
|
||||
assert.Equal(t, 50, result1)
|
||||
|
||||
// Traverse with add
|
||||
traversed2 := Traverse[Env1](add)(getBase)
|
||||
result2 := traversed2(env2)(env1)
|
||||
assert.Equal(t, 15, result2)
|
||||
})
|
||||
|
||||
t.Run("traverse with identity", func(t *testing.T) {
|
||||
type Env1 struct {
|
||||
Value string
|
||||
}
|
||||
type Env2 struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Reader that gets value
|
||||
getValue := func(e Env1) string {
|
||||
return e.Value
|
||||
}
|
||||
|
||||
// Identity Kleisli (just wraps in Of)
|
||||
identity := func(s string) Reader[Env2, string] {
|
||||
return Of[Env2](s)
|
||||
}
|
||||
|
||||
traversed := Traverse[Env1](identity)(getValue)
|
||||
|
||||
env1 := Env1{Value: "test"}
|
||||
env2 := Env2{Prefix: "ignored"}
|
||||
result := traversed(env2)(env1)
|
||||
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("traverse with complex computation", func(t *testing.T) {
|
||||
type Request struct {
|
||||
UserID int
|
||||
}
|
||||
type Database struct {
|
||||
Users map[int]string
|
||||
}
|
||||
type Response struct {
|
||||
UserID int
|
||||
UserName string
|
||||
Found bool
|
||||
}
|
||||
|
||||
// Reader that gets user ID from request
|
||||
getUserID := func(r Request) int {
|
||||
return r.UserID
|
||||
}
|
||||
|
||||
// Kleisli that looks up user in database
|
||||
lookupUser := func(id int) Reader[Database, Response] {
|
||||
return func(db Database) Response {
|
||||
name, found := db.Users[id]
|
||||
return Response{
|
||||
UserID: id,
|
||||
UserName: name,
|
||||
Found: found,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Request](lookupUser)(getUserID)
|
||||
|
||||
request := Request{UserID: 42}
|
||||
db := Database{
|
||||
Users: map[int]string{
|
||||
42: "Alice",
|
||||
99: "Bob",
|
||||
},
|
||||
}
|
||||
|
||||
result := traversed(db)(request)
|
||||
|
||||
assert.Equal(t, Response{
|
||||
UserID: 42,
|
||||
UserName: "Alice",
|
||||
Found: true,
|
||||
}, result)
|
||||
|
||||
// Test with missing user
|
||||
request2 := Request{UserID: 123}
|
||||
result2 := traversed(db)(request2)
|
||||
|
||||
assert.Equal(t, Response{
|
||||
UserID: 123,
|
||||
UserName: "",
|
||||
Found: false,
|
||||
}, result2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import (
|
||||
// type Config struct { Host string }
|
||||
// r := reader.Ask[Config]()
|
||||
// config := r(Config{Host: "localhost"}) // Returns the config itself
|
||||
//
|
||||
//go:inline
|
||||
func Ask[R any]() Reader[R, R] {
|
||||
return function.Identity[R]
|
||||
}
|
||||
@@ -42,6 +44,8 @@ func Ask[R any]() Reader[R, R] {
|
||||
// type Config struct { Port int }
|
||||
// getPort := reader.Asks(func(c Config) int { return c.Port })
|
||||
// port := getPort(Config{Port: 8080}) // Returns 8080
|
||||
//
|
||||
//go:inline
|
||||
func Asks[R, A any](f Reader[R, A]) Reader[R, A] {
|
||||
return f
|
||||
}
|
||||
@@ -60,7 +64,10 @@ func Asks[R, A any](f Reader[R, A]) Reader[R, A] {
|
||||
// }
|
||||
// return reader.Of[Config]("fresh")
|
||||
// })
|
||||
//
|
||||
//go:inline
|
||||
func AsksReader[R, A any](f Kleisli[R, R, A]) Reader[R, A] {
|
||||
//go:inline
|
||||
return func(r R) A {
|
||||
return f(r)(r)
|
||||
}
|
||||
@@ -75,10 +82,44 @@ func AsksReader[R, A any](f Kleisli[R, R, A]) Reader[R, A] {
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// getPortStr := reader.MonadMap(getPort, strconv.Itoa)
|
||||
// result := getPortStr(Config{Port: 8080}) // "8080"
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[E, A, B any](fa Reader[E, A], f func(A) B) Reader[E, B] {
|
||||
return function.Flow2(fa, f)
|
||||
}
|
||||
|
||||
// MonadMapTo creates a new Reader that completely ignores the first Reader and returns a constant value.
|
||||
// This is the monadic version that takes both the Reader and the constant value as parameters.
|
||||
//
|
||||
// IMPORTANT: Readers are pure functions with no side effects. This function does NOT compose or evaluate
|
||||
// the first Reader - it completely ignores it and returns a new Reader that always returns the constant value.
|
||||
// The first Reader is neither executed during composition nor when the resulting Reader runs.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The environment type
|
||||
// - A: The result type of the first Reader (completely ignored)
|
||||
// - B: The type of the constant value to return
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The first Reader (completely ignored, never evaluated)
|
||||
// - b: The constant value to return
|
||||
//
|
||||
// Returns:
|
||||
// - A new Reader that ignores the environment and always returns b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int }
|
||||
// increment := func(c Config) int { return c.Counter + 1 }
|
||||
// // Create a Reader that ignores increment and returns "done"
|
||||
// r := reader.MonadMapTo(increment, "done")
|
||||
// result := r(Config{Counter: 5}) // "done" (increment was never evaluated)
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[E, A, B any](_ Reader[E, A], b B) Reader[E, B] {
|
||||
return Of[E](b)
|
||||
}
|
||||
|
||||
// Map transforms the result value of a Reader using the provided function.
|
||||
// This is the Functor operation that allows you to transform values inside the Reader context.
|
||||
//
|
||||
@@ -91,10 +132,55 @@ func MonadMap[E, A, B any](fa Reader[E, A], f func(A) B) Reader[E, B] {
|
||||
// getPort := reader.Asks(func(c Config) int { return c.Port })
|
||||
// getPortStr := reader.Map(strconv.Itoa)(getPort)
|
||||
// result := getPortStr(Config{Port: 8080}) // "8080"
|
||||
//
|
||||
//go:inline
|
||||
func Map[E, A, B any](f func(A) B) Operator[E, A, B] {
|
||||
return function.Bind2nd(MonadMap[E, A, B], f)
|
||||
}
|
||||
|
||||
// MapTo creates an operator that completely ignores any Reader and returns a constant value.
|
||||
// This is the curried version where the constant value is provided first,
|
||||
// returning a function that can be applied to any Reader.
|
||||
//
|
||||
// IMPORTANT: Readers are pure functions with no side effects. This operator does NOT compose or evaluate
|
||||
// the input Reader - it completely ignores it and returns a new Reader that always returns the constant value.
|
||||
// The input Reader is neither executed during composition nor when the resulting Reader runs.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The environment type
|
||||
// - A: The result type of the input Reader (completely ignored)
|
||||
// - B: The type of the constant value to return
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to return
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a Reader[E, A] and returns Reader[E, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int }
|
||||
// increment := reader.Asks(func(c Config) int { return c.Counter + 1 })
|
||||
// // Create an operator that ignores any Reader and returns "done"
|
||||
// toDone := reader.MapTo[Config, int, string]("done")
|
||||
// pipeline := toDone(increment)
|
||||
// result := pipeline(Config{Counter: 5}) // "done" (increment was never evaluated)
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// type Env struct { Step int }
|
||||
// step1 := reader.Asks(func(e Env) int { return e.Step })
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// reader.MapTo[Env, int, string]("complete"),
|
||||
// )
|
||||
// output := pipeline(Env{Step: 1}) // "complete" (step1 was never evaluated)
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[E, A, B any](b B) Operator[E, A, B] {
|
||||
return Of[Reader[E, A]](Of[E](b))
|
||||
}
|
||||
|
||||
// MonadAp applies a Reader containing a function to a Reader containing a value.
|
||||
// Both Readers share the same environment and are evaluated with it.
|
||||
// This is the monadic version that takes both parameters.
|
||||
@@ -176,6 +262,86 @@ func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return function.Bind2nd(MonadChain[R, A, B], f)
|
||||
}
|
||||
|
||||
// MonadChainTo completely ignores the first Reader and returns the second Reader.
|
||||
// This is the monadic version that takes both Readers as parameters.
|
||||
//
|
||||
// IMPORTANT: Readers are pure functions with no side effects. This function does NOT compose or evaluate
|
||||
// the first Reader - it completely ignores it and returns the second Reader directly.
|
||||
// The first Reader is neither executed during composition nor when the resulting Reader runs.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type of the first Reader (completely ignored)
|
||||
// - R: The environment type
|
||||
// - B: The result type of the second Reader
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The first Reader (completely ignored, never evaluated)
|
||||
// - b: The second Reader to return
|
||||
//
|
||||
// Returns:
|
||||
// - The second Reader unchanged
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int; Message string }
|
||||
// increment := func(c Config) int { return c.Counter + 1 }
|
||||
// getMessage := func(c Config) string { return c.Message }
|
||||
// // Ignore increment and return getMessage
|
||||
// r := reader.MonadChainTo(increment, getMessage)
|
||||
// result := r(Config{Counter: 5, Message: "done"}) // "done" (increment was never evaluated)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainTo[A, R, B any](_ Reader[R, A], b Reader[R, B]) Reader[R, B] {
|
||||
return b
|
||||
}
|
||||
|
||||
// ChainTo creates an operator that completely ignores any Reader and returns a specific Reader.
|
||||
// This is the curried version where the second Reader is provided first,
|
||||
// returning a function that can be applied to any first Reader (which will be ignored).
|
||||
//
|
||||
// IMPORTANT: Readers are pure functions with no side effects. This operator does NOT compose or evaluate
|
||||
// the input Reader - it completely ignores it and returns the specified Reader directly.
|
||||
// The input Reader is neither executed during composition nor when the resulting Reader runs.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type of the first Reader (completely ignored)
|
||||
// - R: The environment type
|
||||
// - B: The result type of the second Reader
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The Reader to return (ignoring any input Reader)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a Reader[R, A] and returns Reader[R, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int; Message string }
|
||||
// getMessage := func(c Config) string { return c.Message }
|
||||
// // Create an operator that ignores any Reader and returns getMessage
|
||||
// thenGetMessage := reader.ChainTo[int, Config, string](getMessage)
|
||||
//
|
||||
// increment := func(c Config) int { return c.Counter + 1 }
|
||||
// pipeline := thenGetMessage(increment)
|
||||
// result := pipeline(Config{Counter: 5, Message: "done"}) // "done" (increment was never evaluated)
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// type Env struct { Step int; Result string }
|
||||
// step1 := reader.Asks(func(e Env) int { return e.Step })
|
||||
// getResult := reader.Asks(func(e Env) string { return e.Result })
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// reader.ChainTo[int, Env, string](getResult),
|
||||
// )
|
||||
// output := pipeline(Env{Step: 1, Result: "success"}) // "success" (step1 was never evaluated)
|
||||
//
|
||||
//go:inline
|
||||
func ChainTo[A, R, B any](b Reader[R, B]) Operator[R, A, B] {
|
||||
return Of[Reader[R, A]](b)
|
||||
}
|
||||
|
||||
// Flatten removes one level of Reader nesting.
|
||||
// Converts Reader[R, Reader[R, A]] to Reader[R, A].
|
||||
//
|
||||
|
||||
@@ -201,3 +201,287 @@ func TestFlap(t *testing.T) {
|
||||
result := r(config)
|
||||
assert.Equal(t, 15, result)
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
t.Run("returns constant value without executing original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(c Config) int {
|
||||
executed = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
// Apply MapTo operator
|
||||
toDone := MapTo[Config, int]("done")
|
||||
resultReader := toDone(originalReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, "done", result)
|
||||
// Verify the original reader was never executed
|
||||
assert.False(t, executed, "original reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("works in functional pipeline without executing original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
step1 := func(c Config) int {
|
||||
executed = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
MapTo[Config, int]("complete"),
|
||||
)
|
||||
|
||||
result := pipeline(Config{Port: 8080})
|
||||
|
||||
assert.Equal(t, "complete", result)
|
||||
assert.False(t, executed, "original reader should not be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("ignores reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(c Config) int {
|
||||
sideEffectOccurred = true
|
||||
return c.Port * 2
|
||||
}
|
||||
|
||||
resultReader := MapTo[Config, int](true)(readerWithSideEffect)
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
assert.True(t, result)
|
||||
assert.False(t, sideEffectOccurred, "side effect should not occur")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("returns constant value without executing original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(c Config) int {
|
||||
executed = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
// Apply MonadMapTo
|
||||
resultReader := MonadMapTo(originalReader, "done")
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, "done", result)
|
||||
// Verify the original reader was never executed
|
||||
assert.False(t, executed, "original reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("ignores complex computation", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReader := func(c Config) string {
|
||||
computationExecuted = true
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(complexReader, 42)
|
||||
result := resultReader(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
assert.False(t, computationExecuted, "complex computation should not be executed")
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
executed := false
|
||||
intReader := func(c Config) int {
|
||||
executed = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(intReader, []string{"a", "b", "c"})
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
assert.False(t, executed, "original reader should not be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainTo(t *testing.T) {
|
||||
t.Run("returns second reader without executing first reader", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
firstReader := func(c Config) int {
|
||||
firstExecuted = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
secondReader := func(c Config) string {
|
||||
return c.Host
|
||||
}
|
||||
|
||||
// Apply ChainTo operator
|
||||
thenSecond := ChainTo[int](secondReader)
|
||||
resultReader := thenSecond(firstReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, "localhost", result)
|
||||
// Verify the first reader was never executed
|
||||
assert.False(t, firstExecuted, "first reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("works in functional pipeline without executing first reader", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
step1 := func(c Config) int {
|
||||
firstExecuted = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
step2 := func(c Config) string {
|
||||
return fmt.Sprintf("Result: %s", c.Host)
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
ChainTo[int](step2),
|
||||
)
|
||||
|
||||
result := pipeline(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, "Result: localhost", result)
|
||||
assert.False(t, firstExecuted, "first reader should not be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("ignores reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(c Config) int {
|
||||
sideEffectOccurred = true
|
||||
return c.Port * 2
|
||||
}
|
||||
|
||||
secondReader := func(c Config) bool {
|
||||
return c.Port > 0
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
assert.True(t, result)
|
||||
assert.False(t, sideEffectOccurred, "side effect should not occur")
|
||||
})
|
||||
|
||||
t.Run("chains multiple ChainTo operations", func(t *testing.T) {
|
||||
executed1 := false
|
||||
executed2 := false
|
||||
|
||||
reader1 := func(c Config) int {
|
||||
executed1 = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
reader2 := func(c Config) string {
|
||||
executed2 = true
|
||||
return c.Host
|
||||
}
|
||||
|
||||
reader3 := func(c Config) bool {
|
||||
return c.Port > 0
|
||||
}
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
reader1,
|
||||
ChainTo[int](reader2),
|
||||
ChainTo[string](reader3),
|
||||
)
|
||||
|
||||
result := pipeline(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.True(t, result)
|
||||
assert.False(t, executed1, "first reader should not be executed")
|
||||
assert.False(t, executed2, "second reader should not be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainTo(t *testing.T) {
|
||||
t.Run("returns second reader without executing first reader", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
firstReader := func(c Config) int {
|
||||
firstExecuted = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
secondReader := func(c Config) string {
|
||||
return c.Host
|
||||
}
|
||||
|
||||
// Apply MonadChainTo
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, "localhost", result)
|
||||
// Verify the first reader was never executed
|
||||
assert.False(t, firstExecuted, "first reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("ignores complex first computation", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
complexFirstReader := func(c Config) []int {
|
||||
firstExecuted = true
|
||||
result := make([]int, c.Port)
|
||||
for i := range result {
|
||||
result[i] = i * c.Multiplier
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
secondReader := func(c Config) string {
|
||||
return c.Prefix + c.Host
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(complexFirstReader, secondReader)
|
||||
result := resultReader(Config{Host: "localhost", Port: 100, Prefix: "server:"})
|
||||
|
||||
assert.Equal(t, "server:localhost", result)
|
||||
assert.False(t, firstExecuted, "complex first computation should not be executed")
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
firstReader := func(c Config) map[string]int {
|
||||
firstExecuted = true
|
||||
return map[string]int{"port": c.Port}
|
||||
}
|
||||
|
||||
secondReader := func(c Config) float64 {
|
||||
return float64(c.Multiplier) * 3.14
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
result := resultReader(Config{Multiplier: 2})
|
||||
|
||||
assert.Equal(t, 6.28, result)
|
||||
assert.False(t, firstExecuted, "first reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("preserves second reader behavior", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
firstReader := func(c Config) int {
|
||||
firstExecuted = true
|
||||
return 999
|
||||
}
|
||||
|
||||
secondReader := func(c Config) string {
|
||||
// Second reader should still have access to the environment
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
result := resultReader(Config{Host: "example.com", Port: 443})
|
||||
|
||||
assert.Equal(t, "example.com:443", result)
|
||||
assert.False(t, firstExecuted, "first reader should not be executed")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func Right[E, L, A any](r A) ReaderEither[E, L, A] {
|
||||
return eithert.Right(reader.Of[E, Either[L, A]], r)
|
||||
}
|
||||
|
||||
func FromReader[E, L, A any](r Reader[E, A]) ReaderEither[E, L, A] {
|
||||
func FromReader[L, E, A any](r Reader[E, A]) ReaderEither[E, L, A] {
|
||||
return RightReader[L](r)
|
||||
}
|
||||
|
||||
@@ -66,19 +66,19 @@ func Chain[E, L, A, B any](f func(A) ReaderEither[E, L, B]) func(ReaderEither[E,
|
||||
return readert.Chain[ReaderEither[E, L, A]](ET.Chain[L, A, B], f)
|
||||
}
|
||||
|
||||
func MonadChainReaderK[E, L, A, B any](ma ReaderEither[E, L, A], f reader.Kleisli[E, A, B]) ReaderEither[E, L, B] {
|
||||
return MonadChain(ma, function.Flow2(f, FromReader[E, L, B]))
|
||||
func MonadChainReaderK[L, E, A, B any](ma ReaderEither[E, L, A], f reader.Kleisli[E, A, B]) ReaderEither[E, L, B] {
|
||||
return MonadChain(ma, function.Flow2(f, FromReader[L, E, B]))
|
||||
}
|
||||
|
||||
func ChainReaderK[E, L, A, B any](f reader.Kleisli[E, A, B]) func(ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
return Chain(function.Flow2(f, FromReader[E, L, B]))
|
||||
func ChainReaderK[L, E, A, B any](f reader.Kleisli[E, A, B]) func(ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
return Chain(function.Flow2(f, FromReader[L, E, B]))
|
||||
}
|
||||
|
||||
func Of[E, L, A any](a A) ReaderEither[E, L, A] {
|
||||
return readert.MonadOf[ReaderEither[E, L, A]](ET.Of[L, A], a)
|
||||
}
|
||||
|
||||
func MonadAp[E, L, A, B any](fab ReaderEither[E, L, func(A) B], fa ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
func MonadAp[B, E, L, A any](fab ReaderEither[E, L, func(A) B], fa ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
return readert.MonadAp[ReaderEither[E, L, A], ReaderEither[E, L, B], ReaderEither[E, L, func(A) B], E, A](ET.MonadAp[B, L, A], fab, fa)
|
||||
}
|
||||
|
||||
@@ -112,11 +112,11 @@ func OrLeft[A, L1, E, L2 any](onLeft func(L1) Reader[E, L2]) func(ReaderEither[E
|
||||
}
|
||||
|
||||
func Ask[E, L any]() ReaderEither[E, L, E] {
|
||||
return fromreader.Ask(FromReader[E, L, E])()
|
||||
return fromreader.Ask(FromReader[L, E, E])()
|
||||
}
|
||||
|
||||
func Asks[L, E, A any](r Reader[E, A]) ReaderEither[E, L, A] {
|
||||
return fromreader.Asks(FromReader[E, L, A])(r)
|
||||
return fromreader.Asks(FromReader[L, E, A])(r)
|
||||
}
|
||||
|
||||
func MonadChainEitherK[E, L, A, B any](ma ReaderEither[E, L, A], f func(A) Either[L, B]) ReaderEither[E, L, B] {
|
||||
|
||||
@@ -172,26 +172,36 @@ func MonadMap[R, A, B any](fa ReaderIO[R, A], f func(A) B) ReaderIO[R, B] {
|
||||
return readert.MonadMap[ReaderIO[R, A], ReaderIO[R, B]](io.MonadMap[A, B], fa, f)
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the value inside a ReaderIO with a constant value.
|
||||
// This is useful when you want to discard the result of a computation but keep its effects.
|
||||
// MonadMapTo executes a ReaderIO computation, discards its result, and returns a constant value.
|
||||
// This is the monadic version that takes both the ReaderIO and the constant value as parameters.
|
||||
//
|
||||
// IMPORTANT: ReaderIO represents a side-effectful computation (IO effects). For this reason,
|
||||
// MonadMapTo WILL execute the original ReaderIO to allow any side effects to occur (such as
|
||||
// logging, file I/O, network calls, etc.), then discard the result and return the constant value.
|
||||
// The side effects are preserved even though the result value is discarded.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: Reader environment type
|
||||
// - A: Input value type (discarded)
|
||||
// - B: Output value type
|
||||
// - A: Input value type (result will be discarded after execution)
|
||||
// - B: Output value type (constant to return)
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIO whose value will be replaced
|
||||
// - b: The constant value to use
|
||||
// - fa: The ReaderIO to execute (side effects will occur, result discarded)
|
||||
// - b: The constant value to return after executing fa
|
||||
//
|
||||
// Returns:
|
||||
// - A new ReaderIO with the constant value
|
||||
// - A new ReaderIO that executes fa for its side effects, then returns b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rio := readerio.Of[Config](42)
|
||||
// replaced := readerio.MonadMapTo(rio, "constant")
|
||||
// result := replaced(config)() // Returns "constant"
|
||||
// logAndCompute := func(r Config) io.IO[int] {
|
||||
// return io.Of(func() int {
|
||||
// fmt.Println("Computing...") // Side effect
|
||||
// return 42
|
||||
// })
|
||||
// }
|
||||
// replaced := readerio.MonadMapTo(logAndCompute, "done")
|
||||
// result := replaced(config)() // Prints "Computing...", returns "done"
|
||||
func MonadMapTo[R, A, B any](fa ReaderIO[R, A], b B) ReaderIO[R, B] {
|
||||
return MonadMap(fa, function.Constant1[A](b))
|
||||
}
|
||||
@@ -220,26 +230,37 @@ func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
|
||||
return readert.Map[ReaderIO[R, A], ReaderIO[R, B]](io.Map[A, B], f)
|
||||
}
|
||||
|
||||
// MapTo creates a function that replaces a ReaderIO value with a constant.
|
||||
// This is the curried version of [MonadMapTo], suitable for use in pipelines.
|
||||
// MapTo creates an operator that executes a ReaderIO computation, discards its result,
|
||||
// and returns a constant value. This is the curried version of [MonadMapTo], suitable for use in pipelines.
|
||||
//
|
||||
// IMPORTANT: ReaderIO represents a side-effectful computation (IO effects). For this reason,
|
||||
// MapTo WILL execute the input ReaderIO to allow any side effects to occur (such as logging,
|
||||
// file I/O, network calls, etc.), then discard the result and return the constant value.
|
||||
// The side effects are preserved even though the result value is discarded.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: Reader environment type
|
||||
// - A: Input value type (discarded)
|
||||
// - B: Output value type
|
||||
// - A: Input value type (result will be discarded after execution)
|
||||
// - B: Output value type (constant to return)
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to use
|
||||
// - b: The constant value to return after executing the ReaderIO
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that replaces ReaderIO values with the constant
|
||||
// - An Operator that executes a ReaderIO for its side effects, then returns b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logStep := func(r Config) io.IO[int] {
|
||||
// return io.Of(func() int {
|
||||
// fmt.Println("Step executed") // Side effect
|
||||
// return 42
|
||||
// })
|
||||
// }
|
||||
// result := F.Pipe1(
|
||||
// readerio.Of[Config](42),
|
||||
// readerio.MapTo[Config, int]("constant"),
|
||||
// )(config)() // Returns "constant"
|
||||
// logStep,
|
||||
// readerio.MapTo[Config, int]("complete"),
|
||||
// )(config)() // Prints "Step executed", returns "complete"
|
||||
func MapTo[R, A, B any](b B) Operator[R, A, B] {
|
||||
return Map[R](function.Constant1[A](b))
|
||||
}
|
||||
|
||||
@@ -323,6 +323,161 @@ func TestMapTo(t *testing.T) {
|
||||
assert.Equal(t, "constant", result(config)())
|
||||
}
|
||||
|
||||
func TestMapToExecutesSideEffects(t *testing.T) {
|
||||
t.Run("executes original ReaderIO and returns constant value", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReaderIO := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
executed = true
|
||||
return 42
|
||||
}
|
||||
}
|
||||
|
||||
// Apply MapTo operator
|
||||
toDone := MapTo[ReaderTestConfig, int]("done")
|
||||
resultReaderIO := toDone(originalReaderIO)
|
||||
|
||||
// Execute the resulting ReaderIO
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, "done", result)
|
||||
// Verify the original ReaderIO WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original ReaderIO should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes ReaderIO in functional pipeline", func(t *testing.T) {
|
||||
executed := false
|
||||
step1 := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
executed = true
|
||||
return 100
|
||||
}
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
MapTo[ReaderTestConfig, int]("complete"),
|
||||
)
|
||||
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := pipeline(config)()
|
||||
|
||||
assert.Equal(t, "complete", result)
|
||||
assert.True(t, executed, "original ReaderIO should be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("executes ReaderIO with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerIOWithSideEffect := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
sideEffectOccurred = true
|
||||
return 42
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MapTo[ReaderTestConfig, int](true)(readerIOWithSideEffect)
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, true, result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur")
|
||||
})
|
||||
|
||||
t.Run("executes complex computation with side effects", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReaderIO := func(c ReaderTestConfig) G.IO[string] {
|
||||
return func() string {
|
||||
computationExecuted = true
|
||||
return "complex result"
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MapTo[ReaderTestConfig, string](99)(complexReaderIO)
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, 99, result)
|
||||
assert.True(t, computationExecuted, "complex computation should be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapToExecutesSideEffects(t *testing.T) {
|
||||
t.Run("executes original ReaderIO and returns constant value", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReaderIO := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
executed = true
|
||||
return 42
|
||||
}
|
||||
}
|
||||
|
||||
// Apply MonadMapTo
|
||||
resultReaderIO := MonadMapTo(originalReaderIO, "done")
|
||||
|
||||
// Execute the resulting ReaderIO
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, "done", result)
|
||||
// Verify the original ReaderIO WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original ReaderIO should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes complex computation with side effects", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReaderIO := func(c ReaderTestConfig) G.IO[string] {
|
||||
return func() string {
|
||||
computationExecuted = true
|
||||
return "complex result"
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MonadMapTo(complexReaderIO, 42)
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
assert.True(t, computationExecuted, "complex computation should be executed")
|
||||
})
|
||||
|
||||
t.Run("executes ReaderIO with logging side effect", func(t *testing.T) {
|
||||
logged := []string{}
|
||||
loggingReaderIO := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
logged = append(logged, "computation executed")
|
||||
return c.Value * 2
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MonadMapTo(loggingReaderIO, "result")
|
||||
config := ReaderTestConfig{Value: 5, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, "result", result)
|
||||
assert.Equal(t, []string{"computation executed"}, logged)
|
||||
})
|
||||
|
||||
t.Run("executes ReaderIO accessing environment", func(t *testing.T) {
|
||||
accessedEnv := false
|
||||
envReaderIO := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
accessedEnv = true
|
||||
return c.Value + 10
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MonadMapTo(envReaderIO, []int{1, 2, 3})
|
||||
config := ReaderTestConfig{Value: 20, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
assert.True(t, accessedEnv, "ReaderIO should access environment during execution")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of[ReaderTestConfig](42)
|
||||
|
||||
Reference in New Issue
Block a user