1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-07 23:03:15 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Carsten Leue
f652a94c3a fix: add template based logger
Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
2025-11-28 10:11:08 +01:00
Dr. Carsten Leue
774db88ca5 fix: add name to prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-27 13:26:36 +01:00
Dr. Carsten Leue
62a3365b20 fix: add conversion prisms for numbers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-27 13:12:18 +01:00
Dr. Carsten Leue
d9a16a6771 fix: add reduce operations to readerioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 17:00:10 +01:00
Dr. Carsten Leue
8949cc7dca fix: expose stats
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 13:44:40 +01:00
12 changed files with 1844 additions and 40 deletions

View File

@@ -8,5 +8,5 @@ import (
// BuilderPrism createa a [Prism] that converts between a builder and its type
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
return prism.MakePrism(F.Flow2(B.Build, result.ToOption[T]), creator)
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
}

View File

@@ -18,6 +18,10 @@ package io
import (
"fmt"
"log"
"os"
"strings"
"sync"
"text/template"
L "github.com/IBM/fp-go/v2/logging"
)
@@ -32,13 +36,14 @@ import (
// io.ChainFirst(io.Logger[User]()("Fetched user")),
// processUser,
// )
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, A] {
_, right := L.LoggingCallbacks(loggers...)
return func(prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
return func(prefix string) Kleisli[A, A] {
return func(a A) IO[A] {
return func() A {
right("%s: %v", prefix, a)
})
return a
}
}
}
}
@@ -53,11 +58,12 @@ func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
// io.ChainFirst(io.Logf[User]("User: %+v")),
// processUser,
// )
func Logf[A any](prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
func Logf[A any](prefix string) Kleisli[A, A] {
return func(a A) IO[A] {
return func() A {
log.Printf(prefix, a)
})
return a
}
}
}
@@ -72,10 +78,102 @@ func Logf[A any](prefix string) Kleisli[A, any] {
// io.ChainFirst(io.Printf[User]("User: %+v\n")),
// processUser,
// )
func Printf[A any](prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
func Printf[A any](prefix string) Kleisli[A, A] {
return func(a A) IO[A] {
return func() A {
fmt.Printf(prefix, a)
})
return a
}
}
}
// handleLogging is a helper function that creates a Kleisli arrow for logging/printing
// values using Go template syntax. It lazily compiles the template on first use and
// executes it with the provided value as data.
//
// Parameters:
// - onSuccess: callback function to handle successfully formatted output
// - onError: callback function to handle template parsing or execution errors
// - prefix: Go template string to format the value
//
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
// The function always returns the original value unchanged, making it suitable for
// use with ChainFirst or similar operations.
func handleLogging[A any](onSuccess func(string), onError func(error), prefix string) Kleisli[A, A] {
var tmp *template.Template
var err error
var once sync.Once
init := func() {
tmp, err = template.New("").Parse(prefix)
}
return func(a A) IO[A] {
return func() A {
// make sure to compile lazily
once.Do(init)
if err == nil {
var buffer strings.Builder
tmpErr := tmp.Execute(&buffer, a)
if tmpErr != nil {
onError(tmpErr)
onSuccess(fmt.Sprintf("%v", a))
} else {
onSuccess(buffer.String())
}
} else {
onError(err)
onSuccess(fmt.Sprintf("%v", a))
}
// in any case return the original value
return a
}
}
}
// LogGo constructs a logger function using Go template syntax for formatting.
// The prefix string is parsed as a Go template and executed with the value as data.
// Both successful output and template errors are logged using log.Println.
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
// result := pipe.Pipe2(
// fetchUser(),
// io.ChainFirst(io.LogGo[User]("User: {{.Name}}, Age: {{.Age}}")),
// processUser,
// )
func LogGo[A any](prefix string) Kleisli[A, A] {
return handleLogging[A](func(value string) {
log.Println(value)
}, func(err error) {
log.Println(err)
}, prefix)
}
// PrintGo constructs a printer function using Go template syntax for formatting.
// The prefix string is parsed as a Go template and executed with the value as data.
// Successful output is printed to stdout using fmt.Println, while template errors
// are printed to stderr using fmt.Fprintln.
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
// result := pipe.Pipe2(
// fetchUser(),
// io.ChainFirst(io.PrintGo[User]("User: {{.Name}}, Age: {{.Age}}")),
// processUser,
// )
func PrintGo[A any](prefix string) Kleisli[A, A] {
return handleLogging[A](func(value string) {
fmt.Println(value)
}, func(err error) {
fmt.Fprintln(os.Stderr, err)
}, prefix)
}

View File

@@ -16,25 +16,206 @@
package io
import (
"bytes"
"log"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestLogger(t *testing.T) {
l := Logger[int]()
lio := l("out")
assert.NotPanics(t, func() { lio(10)() })
}
func TestLoggerWithCustomLogger(t *testing.T) {
var buf bytes.Buffer
customLogger := log.New(&buf, "", 0)
l := Logger[int](customLogger)
lio := l("test value")
result := lio(42)()
assert.Equal(t, 42, result)
assert.Contains(t, buf.String(), "test value")
assert.Contains(t, buf.String(), "42")
}
func TestLoggerReturnsOriginalValue(t *testing.T) {
type TestStruct struct {
Name string
Value int
}
l := Logger[TestStruct]()
lio := l("test")
input := TestStruct{Name: "test", Value: 100}
result := lio(input)()
assert.Equal(t, input, result)
}
func TestLogf(t *testing.T) {
l := Logf[int]
lio := l("Value is %d")
assert.NotPanics(t, func() { lio(10)() })
}
func TestLogfReturnsOriginalValue(t *testing.T) {
l := Logf[string]
lio := l("String: %s")
input := "hello"
result := lio(input)()
assert.Equal(t, input, result)
}
func TestPrintfLogger(t *testing.T) {
l := Printf[int]
lio := l("Value: %d\n")
assert.NotPanics(t, func() { lio(10)() })
}
func TestPrintfLoggerReturnsOriginalValue(t *testing.T) {
l := Printf[float64]
lio := l("Number: %.2f\n")
input := 3.14159
result := lio(input)()
assert.Equal(t, input, result)
}
func TestLogGo(t *testing.T) {
type User struct {
Name string
Age int
}
l := LogGo[User]
lio := l("User: {{.Name}}, Age: {{.Age}}")
input := User{Name: "Alice", Age: 30}
assert.NotPanics(t, func() { lio(input)() })
}
func TestLogGoReturnsOriginalValue(t *testing.T) {
type Product struct {
ID int
Name string
Price float64
}
l := LogGo[Product]
lio := l("Product: {{.Name}} ({{.ID}})")
input := Product{ID: 123, Name: "Widget", Price: 19.99}
result := lio(input)()
assert.Equal(t, input, result)
}
func TestLogGoWithInvalidTemplate(t *testing.T) {
l := LogGo[int]
// Invalid template syntax
lio := l("Value: {{.MissingField")
// Should not panic even with invalid template
assert.NotPanics(t, func() { lio(42)() })
}
func TestLogGoWithComplexTemplate(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
l := LogGo[Person]
lio := l("Person: {{.Name}} from {{.Address.City}}")
input := Person{
Name: "Bob",
Address: Address{Street: "Main St", City: "NYC"},
}
result := lio(input)()
assert.Equal(t, input, result)
}
func TestPrintGo(t *testing.T) {
type User struct {
Name string
Age int
}
l := PrintGo[User]
lio := l("User: {{.Name}}, Age: {{.Age}}")
input := User{Name: "Charlie", Age: 25}
assert.NotPanics(t, func() { lio(input)() })
}
func TestPrintGoReturnsOriginalValue(t *testing.T) {
type Score struct {
Player string
Points int
}
l := PrintGo[Score]
lio := l("{{.Player}}: {{.Points}} points")
input := Score{Player: "Alice", Points: 100}
result := lio(input)()
assert.Equal(t, input, result)
}
func TestPrintGoWithInvalidTemplate(t *testing.T) {
l := PrintGo[string]
// Invalid template syntax
lio := l("Value: {{.}")
// Should not panic even with invalid template
assert.NotPanics(t, func() { lio("test")() })
}
func TestLogGoInPipeline(t *testing.T) {
type Data struct {
Value int
}
input := Data{Value: 10}
result := F.Pipe2(
Of(input),
ChainFirst(LogGo[Data]("Processing: {{.Value}}")),
Map(func(d Data) Data {
return Data{Value: d.Value * 2}
}),
)()
assert.Equal(t, 20, result.Value)
}
func TestPrintGoInPipeline(t *testing.T) {
input := "hello"
result := F.Pipe2(
Of(input),
ChainFirst(PrintGo[string]("Input: {{.}}")),
Map(func(s string) string {
return s + " world"
}),
)()
assert.Equal(t, "hello world", result)
}

View File

@@ -29,6 +29,48 @@ var (
Create = ioeither.Eitherize1(os.Create)
// ReadFile reads the context of a file
ReadFile = ioeither.Eitherize1(os.ReadFile)
// Stat returns [FileInfo] object
Stat = ioeither.Eitherize1(os.Stat)
// UserCacheDir returns an [IOEither] that resolves to the default root directory
// to use for user-specific cached data. Users should create their own application-specific
// subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CACHE_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.cache.
// On Darwin, it returns $HOME/Library/Caches.
// On Windows, it returns %LocalAppData%.
// On Plan 9, it returns $home/lib/cache.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [E.Left].
UserCacheDir = ioeither.Eitherize0(os.UserCacheDir)()
// UserConfigDir returns an [IOEither] that resolves to the default root directory
// to use for user-specific configuration data. Users should create their own
// application-specific subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CONFIG_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.config.
// On Darwin, it returns $HOME/Library/Application Support.
// On Windows, it returns %AppData%.
// On Plan 9, it returns $home/lib.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [E.Left].
UserConfigDir = ioeither.Eitherize0(os.UserConfigDir)()
// UserHomeDir returns an [IOEither] that resolves to the current user's home directory.
//
// On Unix, including macOS, it returns the $HOME environment variable.
// On Windows, it returns %USERPROFILE%.
// On Plan 9, it returns the $home environment variable.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [E.Left].
UserHomeDir = ioeither.Eitherize0(os.UserHomeDir)()
)
// WriteFile writes a data blob to a file

View File

@@ -29,6 +29,48 @@ var (
Create = file.Create
// ReadFile reads the context of a file
ReadFile = file.ReadFile
// Stat returns [FileInfo] object
Stat = file.Stat
// UserCacheDir returns an [IOResult] that resolves to the default root directory
// to use for user-specific cached data. Users should create their own application-specific
// subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CACHE_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.cache.
// On Darwin, it returns $HOME/Library/Caches.
// On Windows, it returns %LocalAppData%.
// On Plan 9, it returns $home/lib/cache.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [Err].
UserCacheDir = file.UserCacheDir
// UserConfigDir returns an [IOResult] that resolves to the default root directory
// to use for user-specific configuration data. Users should create their own
// application-specific subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CONFIG_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.config.
// On Darwin, it returns $HOME/Library/Application Support.
// On Windows, it returns %AppData%.
// On Plan 9, it returns $home/lib.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [Err].
UserConfigDir = file.UserConfigDir
// UserHomeDir returns an [IOResult] that resolves to the current user's home directory.
//
// On Unix, including macOS, it returns the $HOME environment variable.
// On Windows, it returns %USERPROFILE%.
// On Plan 9, it returns the $home environment variable.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error wrapped in [Err].
UserHomeDir = file.UserHomeDir
)
// WriteFile writes a data blob to a file

View File

@@ -33,7 +33,7 @@ func AsOptional[S, A any](sa P.Prism[S, A]) OPT.Optional[S, A] {
}
func PrismSome[A any]() P.Prism[O.Option[A], A] {
return P.MakePrism(F.Identity[O.Option[A]], O.Some[A])
return P.MakePrismWithName(F.Identity[O.Option[A]], O.Some[A], "PrismSome")
}
// Some returns a `Optional` from a `Optional` focused on the `Some` of a `Option` type.

View File

@@ -78,8 +78,15 @@ type (
// func(opt Option[int]) Option[int] { return opt },
// func(n int) Option[int] { return Some(n) },
// )
//
//go:inline
func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
return Prism[S, A]{get, rev, "GenericPrism"}
return MakePrismWithName(get, rev, "GenericPrism")
}
//go:inline
func MakePrismWithName[S, A any](get func(S) Option[A], rev func(A) S, name string) Prism[S, A] {
return Prism[S, A]{get, rev, name}
}
// Id returns an identity prism that focuses on the entire value.
@@ -94,7 +101,7 @@ func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
// value := idPrism.GetOption(42) // Some(42)
// result := idPrism.ReverseGet(42) // 42
func Id[S any]() Prism[S, S] {
return MakePrism(O.Some[S], F.Identity[S])
return MakePrismWithName(O.Some[S], F.Identity[S], "PrismIdentity")
}
// FromPredicate creates a prism that matches values satisfying a predicate.
@@ -113,7 +120,7 @@ func Id[S any]() Prism[S, S] {
// value := positivePrism.GetOption(42) // Some(42)
// value = positivePrism.GetOption(-5) // None[int]
func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
return MakePrism(O.FromPredicate(pred), F.Identity[S])
return MakePrismWithName(O.FromPredicate(pred), F.Identity[S], "PrismWithPredicate")
}
// Compose composes two prisms to create a prism that focuses deeper into a structure.
@@ -137,13 +144,15 @@ func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
// composed := Compose[Outer](innerPrism)(outerPrism) // Prism[Outer, Value]
func Compose[S, A, B any](ab Prism[A, B]) func(Prism[S, A]) Prism[S, B] {
return func(sa Prism[S, A]) Prism[S, B] {
return MakePrism(F.Flow2(
return MakePrismWithName(F.Flow2(
sa.GetOption,
O.Chain(ab.GetOption),
), F.Flow2(
ab.ReverseGet,
sa.ReverseGet,
))
),
fmt.Sprintf("PrismCompose[%s x %s]", ab, sa),
)
}
}
@@ -201,7 +210,7 @@ func Set[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
// prismSome creates a prism that focuses on the Some variant of an Option.
// This is an internal helper used by the Some function.
func prismSome[A any]() Prism[Option[A], A] {
return MakePrism(F.Identity[Option[A]], O.Some[A])
return MakePrismWithName(F.Identity[Option[A]], O.Some[A], "PrismSome")
}
// Some creates a prism that focuses on the Some variant of an Option within a structure.
@@ -230,9 +239,10 @@ func Some[S, A any](soa Prism[S, Option[A]]) Prism[S, A] {
// imap is an internal helper that bidirectionally maps a prism's focus type.
func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB, ba BA) Prism[S, B] {
return MakePrism(
return MakePrismWithName(
F.Flow2(sa.GetOption, O.Map(ab)),
F.Flow2(ba, sa.ReverseGet),
fmt.Sprintf("PrismIMap[%s]", sa),
)
}

View File

@@ -17,8 +17,10 @@ package prism
import (
"encoding/base64"
"fmt"
"net/url"
"regexp"
"strconv"
"time"
"github.com/IBM/fp-go/v2/either"
@@ -67,10 +69,12 @@ import (
// - Validating and transforming base64 data in pipelines
// - Using different encodings (Standard, URL-safe, RawStd, RawURL)
func FromEncoding(enc *base64.Encoding) Prism[string, []byte] {
return MakePrism(F.Flow2(
return MakePrismWithName(F.Flow2(
either.Eitherize1(enc.DecodeString),
either.Fold(F.Ignore1of1[error](option.None[[]byte]), option.Some),
), enc.EncodeToString)
), enc.EncodeToString,
"PrismFromEncoding",
)
}
// ParseURL creates a prism for parsing and formatting URLs.
@@ -114,10 +118,12 @@ func FromEncoding(enc *base64.Encoding) Prism[string, []byte] {
// - Transforming URL strings in data pipelines
// - Extracting and modifying URL components safely
func ParseURL() Prism[string, *url.URL] {
return MakePrism(F.Flow2(
return MakePrismWithName(F.Flow2(
either.Eitherize1(url.Parse),
either.Fold(F.Ignore1of1[error](option.None[*url.URL]), option.Some),
), (*url.URL).String)
), (*url.URL).String,
"PrismParseURL",
)
}
// InstanceOf creates a prism for type assertions on interface{}/any values.
@@ -161,7 +167,8 @@ func ParseURL() Prism[string, *url.URL] {
// - Type-safe deserialization and validation
// - Pattern matching on interface{} values
func InstanceOf[T any]() Prism[any, T] {
return MakePrism(option.ToType[T], F.ToAny[T])
var t T
return MakePrismWithName(option.ToType[T], F.ToAny[T], fmt.Sprintf("PrismInstanceOf[%T]", t))
}
// ParseDate creates a prism for parsing and formatting dates with a specific layout.
@@ -212,10 +219,12 @@ func InstanceOf[T any]() Prism[any, T] {
// - Converting between date formats
// - Safely handling user-provided date inputs
func ParseDate(layout string) Prism[string, time.Time] {
return MakePrism(F.Flow2(
return MakePrismWithName(F.Flow2(
F.Bind1st(either.Eitherize2(time.Parse), layout),
either.Fold(F.Ignore1of1[error](option.None[time.Time]), option.Some),
), F.Bind2nd(time.Time.Format, layout))
), F.Bind2nd(time.Time.Format, layout),
"PrismParseDate",
)
}
// Deref creates a prism for safely dereferencing pointers.
@@ -263,7 +272,7 @@ func ParseDate(layout string) Prism[string, time.Time] {
// - Filtering out nil values in data pipelines
// - Working with database nullable columns
func Deref[T any]() Prism[*T, *T] {
return MakePrism(option.FromNillable[T], F.Identity[*T])
return MakePrismWithName(option.FromNillable[T], F.Identity[*T], "PrismDeref")
}
// FromEither creates a prism for extracting Right values from Either types.
@@ -309,7 +318,7 @@ func Deref[T any]() Prism[*T, *T] {
// - Working with fallible operations
// - Composing with other prisms for complex error handling
func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrism(either.ToOption[E, T], either.Of[E, T])
return MakePrismWithName(either.ToOption[E, T], either.Of[E, T], "PrismFromEither")
}
// FromZero creates a prism that matches zero values of comparable types.
@@ -352,11 +361,50 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
// - Working with optional fields that use zero as "not set"
// - Replacing zero values with defaults
func FromZero[T comparable]() Prism[T, T] {
return MakePrism(option.FromZero[T](), F.Identity[T])
return MakePrismWithName(option.FromZero[T](), F.Identity[T], "PrismFromZero")
}
// FromNonZero creates a prism that matches non-zero values of comparable types.
// It provides a safe way to work with non-zero values, handling zero values
// gracefully through the Option type.
//
// The prism's GetOption returns Some(t) if the value is not equal to the zero value
// of type T; otherwise, it returns None.
//
// The prism's ReverseGet is the identity function, returning the value unchanged.
//
// Type Parameters:
// - T: A comparable type (must support == and != operators)
//
// Returns:
// - A Prism[T, T] that matches non-zero values
//
// Example:
//
// // Create a prism for non-zero integers
// nonZeroPrism := FromNonZero[int]()
//
// // Match non-zero value
// result := nonZeroPrism.GetOption(42) // Some(42)
//
// // Zero returns None
// result = nonZeroPrism.GetOption(0) // None[int]()
//
// // ReverseGet is identity
// value := nonZeroPrism.ReverseGet(42) // 42
//
// // Use with Set to update non-zero values
// setter := Set[int, int](100)
// result := setter(nonZeroPrism)(42) // 100
// result = setter(nonZeroPrism)(0) // 0 (unchanged)
//
// Common use cases:
// - Validating that values are non-zero/non-default
// - Filtering non-zero values in data pipelines
// - Working with required fields that shouldn't be zero
// - Replacing non-zero values with new values
func FromNonZero[T comparable]() Prism[T, T] {
return MakePrism(option.FromNonZero[T](), F.Identity[T])
return MakePrismWithName(option.FromNonZero[T](), F.Identity[T], "PrismFromNonZero")
}
// Match represents a regex match result with full reconstruction capability.
@@ -495,7 +543,7 @@ func (m Match) Group(n int) string {
func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
noMatch := option.None[Match]()
return MakePrism(
return MakePrismWithName(
// String -> Option[Match]
func(s string) Option[Match] {
loc := re.FindStringSubmatchIndex(s)
@@ -522,6 +570,7 @@ func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
return option.Some(match)
},
Match.Reconstruct,
fmt.Sprintf("PrismRegex[%s]", re),
)
}
@@ -660,3 +709,259 @@ func RegexNamedMatcher(re *regexp.Regexp) Prism[string, NamedMatch] {
NamedMatch.Reconstruct,
)
}
func getFromEither[A, B any](f func(A) (B, error)) func(A) Option[B] {
return func(a A) Option[B] {
b, err := f(a)
if err != nil {
return option.None[B]()
}
return option.Of(b)
}
}
func atoi64(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
}
func itoa64(i int64) string {
return strconv.FormatInt(i, 10)
}
// ParseInt creates a prism for parsing and formatting integers.
// It provides a safe way to convert between string and int, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into an int.
// If parsing succeeds, it returns Some(int); if it fails (e.g., invalid
// number format), it returns None.
//
// The prism's ReverseGet always succeeds, converting an int to its string representation.
//
// Returns:
// - A Prism[string, int] that safely handles int parsing/formatting
//
// Example:
//
// // Create an int parsing prism
// intPrism := ParseInt()
//
// // Parse valid integer
// parsed := intPrism.GetOption("42") // Some(42)
//
// // Parse invalid integer
// invalid := intPrism.GetOption("not-a-number") // None[int]()
//
// // Format int to string
// str := intPrism.ReverseGet(42) // "42"
//
// // Use with Set to update integer values
// setter := Set[string, int](100)
// result := setter(intPrism)("42") // "100"
//
// Common use cases:
// - Parsing integer configuration values
// - Validating numeric user input
// - Converting between string and int in data pipelines
// - Working with numeric API parameters
//
//go:inline
func ParseInt() Prism[string, int] {
return MakePrismWithName(getFromEither(strconv.Atoi), strconv.Itoa, "PrismParseInt")
}
// ParseInt64 creates a prism for parsing and formatting 64-bit integers.
// It provides a safe way to convert between string and int64, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into an int64.
// If parsing succeeds, it returns Some(int64); if it fails (e.g., invalid
// number format or overflow), it returns None.
//
// The prism's ReverseGet always succeeds, converting an int64 to its string representation.
//
// Returns:
// - A Prism[string, int64] that safely handles int64 parsing/formatting
//
// Example:
//
// // Create an int64 parsing prism
// int64Prism := ParseInt64()
//
// // Parse valid 64-bit integer
// parsed := int64Prism.GetOption("9223372036854775807") // Some(9223372036854775807)
//
// // Parse invalid integer
// invalid := int64Prism.GetOption("not-a-number") // None[int64]()
//
// // Format int64 to string
// str := int64Prism.ReverseGet(int64(42)) // "42"
//
// // Use with Set to update int64 values
// setter := Set[string, int64](int64(100))
// result := setter(int64Prism)("42") // "100"
//
// Common use cases:
// - Parsing large integer values (timestamps, IDs)
// - Working with database integer columns
// - Handling 64-bit numeric API parameters
// - Converting between string and int64 in data pipelines
//
//go:inline
func ParseInt64() Prism[string, int64] {
return MakePrismWithName(getFromEither(atoi64), itoa64, "PrismParseInt64")
}
// ParseBool creates a prism for parsing and formatting boolean values.
// It provides a safe way to convert between string and bool, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into a bool.
// It accepts "1", "t", "T", "TRUE", "true", "True", "0", "f", "F", "FALSE", "false", "False".
// If parsing succeeds, it returns Some(bool); if it fails, it returns None.
//
// The prism's ReverseGet always succeeds, converting a bool to "true" or "false".
//
// Returns:
// - A Prism[string, bool] that safely handles bool parsing/formatting
//
// Example:
//
// // Create a bool parsing prism
// boolPrism := ParseBool()
//
// // Parse valid boolean strings
// parsed := boolPrism.GetOption("true") // Some(true)
// parsed = boolPrism.GetOption("1") // Some(true)
// parsed = boolPrism.GetOption("false") // Some(false)
// parsed = boolPrism.GetOption("0") // Some(false)
//
// // Parse invalid boolean
// invalid := boolPrism.GetOption("maybe") // None[bool]()
//
// // Format bool to string
// str := boolPrism.ReverseGet(true) // "true"
// str = boolPrism.ReverseGet(false) // "false"
//
// // Use with Set to update boolean values
// setter := Set[string, bool](true)
// result := setter(boolPrism)("false") // "true"
//
// Common use cases:
// - Parsing boolean configuration values
// - Validating boolean user input
// - Converting between string and bool in data pipelines
// - Working with boolean API parameters or flags
//
//go:inline
func ParseBool() Prism[string, bool] {
return MakePrismWithName(getFromEither(strconv.ParseBool), strconv.FormatBool, "PrismParseBool")
}
func atof64(s string) (float64, error) {
return strconv.ParseFloat(s, 64)
}
func atof32(s string) (float32, error) {
f32, err := strconv.ParseFloat(s, 32)
if err != nil {
return 0, err
}
return float32(f32), nil
}
func f32toa(f float32) string {
return strconv.FormatFloat(float64(f), 'g', -1, 32)
}
func f64toa(f float64) string {
return strconv.FormatFloat(f, 'g', -1, 64)
}
// ParseFloat32 creates a prism for parsing and formatting 32-bit floating-point numbers.
// It provides a safe way to convert between string and float32, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into a float32.
// If parsing succeeds, it returns Some(float32); if it fails (e.g., invalid
// number format or overflow), it returns None.
//
// The prism's ReverseGet always succeeds, converting a float32 to its string representation
// using the 'g' format (shortest representation).
//
// Returns:
// - A Prism[string, float32] that safely handles float32 parsing/formatting
//
// Example:
//
// // Create a float32 parsing prism
// float32Prism := ParseFloat32()
//
// // Parse valid float
// parsed := float32Prism.GetOption("3.14") // Some(3.14)
// parsed = float32Prism.GetOption("1.5e10") // Some(1.5e10)
//
// // Parse invalid float
// invalid := float32Prism.GetOption("not-a-number") // None[float32]()
//
// // Format float32 to string
// str := float32Prism.ReverseGet(float32(3.14)) // "3.14"
//
// // Use with Set to update float32 values
// setter := Set[string, float32](float32(2.71))
// result := setter(float32Prism)("3.14") // "2.71"
//
// Common use cases:
// - Parsing floating-point configuration values
// - Working with scientific notation
// - Converting between string and float32 in data pipelines
// - Handling numeric API parameters with decimal precision
//
//go:inline
func ParseFloat32() Prism[string, float32] {
return MakePrismWithName(getFromEither(atof32), f32toa, "ParseFloat32")
}
// ParseFloat64 creates a prism for parsing and formatting 64-bit floating-point numbers.
// It provides a safe way to convert between string and float64, handling
// parsing errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into a float64.
// If parsing succeeds, it returns Some(float64); if it fails (e.g., invalid
// number format or overflow), it returns None.
//
// The prism's ReverseGet always succeeds, converting a float64 to its string representation
// using the 'g' format (shortest representation).
//
// Returns:
// - A Prism[string, float64] that safely handles float64 parsing/formatting
//
// Example:
//
// // Create a float64 parsing prism
// float64Prism := ParseFloat64()
//
// // Parse valid float
// parsed := float64Prism.GetOption("3.141592653589793") // Some(3.141592653589793)
// parsed = float64Prism.GetOption("1.5e100") // Some(1.5e100)
//
// // Parse invalid float
// invalid := float64Prism.GetOption("not-a-number") // None[float64]()
//
// // Format float64 to string
// str := float64Prism.ReverseGet(3.141592653589793) // "3.141592653589793"
//
// // Use with Set to update float64 values
// setter := Set[string, float64](2.718281828459045)
// result := setter(float64Prism)("3.14") // "2.718281828459045"
//
// Common use cases:
// - Parsing high-precision floating-point values
// - Working with scientific notation and large numbers
// - Converting between string and float64 in data pipelines
// - Handling precise numeric API parameters
//
//go:inline
func ParseFloat64() Prism[string, float64] {
return MakePrismWithName(getFromEither(atof64), f64toa, "PrismParseFloat64")
}

View File

@@ -532,3 +532,411 @@ func TestRegexNamedMatcherWithSet(t *testing.T) {
assert.Equal(t, original, result)
})
}
// TestFromNonZero tests the FromNonZero prism with various comparable types
func TestFromNonZero(t *testing.T) {
t.Run("int - match non-zero", func(t *testing.T) {
prism := FromNonZero[int]()
result := prism.GetOption(42)
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("int - zero returns None", func(t *testing.T) {
prism := FromNonZero[int]()
result := prism.GetOption(0)
assert.True(t, O.IsNone(result))
})
t.Run("string - match non-empty string", func(t *testing.T) {
prism := FromNonZero[string]()
result := prism.GetOption("hello")
assert.True(t, O.IsSome(result))
assert.Equal(t, "hello", O.GetOrElse(F.Constant("default"))(result))
})
t.Run("string - empty returns None", func(t *testing.T) {
prism := FromNonZero[string]()
result := prism.GetOption("")
assert.True(t, O.IsNone(result))
})
t.Run("bool - match true", func(t *testing.T) {
prism := FromNonZero[bool]()
result := prism.GetOption(true)
assert.True(t, O.IsSome(result))
assert.True(t, O.GetOrElse(F.Constant(false))(result))
})
t.Run("bool - false returns None", func(t *testing.T) {
prism := FromNonZero[bool]()
result := prism.GetOption(false)
assert.True(t, O.IsNone(result))
})
t.Run("float64 - match non-zero", func(t *testing.T) {
prism := FromNonZero[float64]()
result := prism.GetOption(3.14)
assert.True(t, O.IsSome(result))
assert.Equal(t, 3.14, O.GetOrElse(F.Constant(-1.0))(result))
})
t.Run("float64 - zero returns None", func(t *testing.T) {
prism := FromNonZero[float64]()
result := prism.GetOption(0.0)
assert.True(t, O.IsNone(result))
})
t.Run("pointer - match non-nil", func(t *testing.T) {
prism := FromNonZero[*int]()
value := 42
result := prism.GetOption(&value)
assert.True(t, O.IsSome(result))
})
t.Run("pointer - nil returns None", func(t *testing.T) {
prism := FromNonZero[*int]()
var nilPtr *int
result := prism.GetOption(nilPtr)
assert.True(t, O.IsNone(result))
})
t.Run("reverse get is identity", func(t *testing.T) {
prism := FromNonZero[int]()
assert.Equal(t, 0, prism.ReverseGet(0))
assert.Equal(t, 42, prism.ReverseGet(42))
})
}
// TestFromNonZeroWithSet tests using Set with FromNonZero prism
func TestFromNonZeroWithSet(t *testing.T) {
t.Run("set on non-zero value", func(t *testing.T) {
prism := FromNonZero[int]()
setter := Set[int](100)
result := setter(prism)(42)
assert.Equal(t, 100, result)
})
t.Run("set on zero returns original", func(t *testing.T) {
prism := FromNonZero[int]()
setter := Set[int](100)
result := setter(prism)(0)
assert.Equal(t, 0, result)
})
}
// TestParseInt tests the ParseInt prism
func TestParseInt(t *testing.T) {
prism := ParseInt()
t.Run("parse valid positive integer", func(t *testing.T) {
result := prism.GetOption("42")
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("parse valid negative integer", func(t *testing.T) {
result := prism.GetOption("-123")
assert.True(t, O.IsSome(result))
assert.Equal(t, -123, O.GetOrElse(F.Constant(0))(result))
})
t.Run("parse zero", func(t *testing.T) {
result := prism.GetOption("0")
assert.True(t, O.IsSome(result))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("parse invalid integer", func(t *testing.T) {
result := prism.GetOption("not-a-number")
assert.True(t, O.IsNone(result))
})
t.Run("parse float as integer fails", func(t *testing.T) {
result := prism.GetOption("3.14")
assert.True(t, O.IsNone(result))
})
t.Run("parse empty string fails", func(t *testing.T) {
result := prism.GetOption("")
assert.True(t, O.IsNone(result))
})
t.Run("reverse get formats integer", func(t *testing.T) {
assert.Equal(t, "42", prism.ReverseGet(42))
assert.Equal(t, "-123", prism.ReverseGet(-123))
assert.Equal(t, "0", prism.ReverseGet(0))
})
t.Run("round trip", func(t *testing.T) {
original := "12345"
result := prism.GetOption(original)
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(0))(result)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, original, reconstructed)
}
})
}
// TestParseInt64 tests the ParseInt64 prism
func TestParseInt64(t *testing.T) {
prism := ParseInt64()
t.Run("parse valid int64", func(t *testing.T) {
result := prism.GetOption("9223372036854775807")
assert.True(t, O.IsSome(result))
assert.Equal(t, int64(9223372036854775807), O.GetOrElse(F.Constant(int64(-1)))(result))
})
t.Run("parse negative int64", func(t *testing.T) {
result := prism.GetOption("-9223372036854775808")
assert.True(t, O.IsSome(result))
assert.Equal(t, int64(-9223372036854775808), O.GetOrElse(F.Constant(int64(0)))(result))
})
t.Run("parse invalid int64", func(t *testing.T) {
result := prism.GetOption("not-a-number")
assert.True(t, O.IsNone(result))
})
t.Run("reverse get formats int64", func(t *testing.T) {
assert.Equal(t, "42", prism.ReverseGet(int64(42)))
assert.Equal(t, "9223372036854775807", prism.ReverseGet(int64(9223372036854775807)))
})
t.Run("round trip", func(t *testing.T) {
original := "1234567890123456789"
result := prism.GetOption(original)
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(int64(0)))(result)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, original, reconstructed)
}
})
}
// TestParseBool tests the ParseBool prism
func TestParseBool(t *testing.T) {
prism := ParseBool()
t.Run("parse true variations", func(t *testing.T) {
trueValues := []string{"true", "True", "TRUE", "t", "T", "1"}
for _, val := range trueValues {
result := prism.GetOption(val)
assert.True(t, O.IsSome(result), "Should parse: %s", val)
assert.True(t, O.GetOrElse(F.Constant(false))(result), "Should be true: %s", val)
}
})
t.Run("parse false variations", func(t *testing.T) {
falseValues := []string{"false", "False", "FALSE", "f", "F", "0"}
for _, val := range falseValues {
result := prism.GetOption(val)
assert.True(t, O.IsSome(result), "Should parse: %s", val)
assert.False(t, O.GetOrElse(F.Constant(true))(result), "Should be false: %s", val)
}
})
t.Run("parse invalid bool", func(t *testing.T) {
invalidValues := []string{"maybe", "yes", "no", "2", ""}
for _, val := range invalidValues {
result := prism.GetOption(val)
assert.True(t, O.IsNone(result), "Should not parse: %s", val)
}
})
t.Run("reverse get formats bool", func(t *testing.T) {
assert.Equal(t, "true", prism.ReverseGet(true))
assert.Equal(t, "false", prism.ReverseGet(false))
})
t.Run("round trip with true", func(t *testing.T) {
result := prism.GetOption("true")
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(false))(result)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, "true", reconstructed)
}
})
t.Run("round trip with false", func(t *testing.T) {
result := prism.GetOption("false")
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(true))(result)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, "false", reconstructed)
}
})
}
// TestParseFloat32 tests the ParseFloat32 prism
func TestParseFloat32(t *testing.T) {
prism := ParseFloat32()
t.Run("parse valid float32", func(t *testing.T) {
result := prism.GetOption("3.14")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(float32(0)))(result)
assert.InDelta(t, float32(3.14), value, 0.0001)
})
t.Run("parse negative float32", func(t *testing.T) {
result := prism.GetOption("-2.71")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(float32(0)))(result)
assert.InDelta(t, float32(-2.71), value, 0.0001)
})
t.Run("parse scientific notation", func(t *testing.T) {
result := prism.GetOption("1.5e10")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(float32(0)))(result)
assert.InDelta(t, float32(1.5e10), value, 1e6)
})
t.Run("parse integer as float", func(t *testing.T) {
result := prism.GetOption("42")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(float32(0)))(result)
assert.Equal(t, float32(42), value)
})
t.Run("parse invalid float", func(t *testing.T) {
result := prism.GetOption("not-a-number")
assert.True(t, O.IsNone(result))
})
t.Run("reverse get formats float32", func(t *testing.T) {
str := prism.ReverseGet(float32(3.14))
assert.Contains(t, str, "3.14")
})
t.Run("round trip", func(t *testing.T) {
original := "3.14159"
result := prism.GetOption(original)
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(float32(0)))(result)
reconstructed := prism.ReverseGet(value)
// Parse both to compare as floats due to precision
origFloat := F.Pipe1(original, prism.GetOption)
reconFloat := F.Pipe1(reconstructed, prism.GetOption)
if O.IsSome(origFloat) && O.IsSome(reconFloat) {
assert.InDelta(t,
O.GetOrElse(F.Constant(float32(0)))(origFloat),
O.GetOrElse(F.Constant(float32(0)))(reconFloat),
0.0001)
}
}
})
}
// TestParseFloat64 tests the ParseFloat64 prism
func TestParseFloat64(t *testing.T) {
prism := ParseFloat64()
t.Run("parse valid float64", func(t *testing.T) {
result := prism.GetOption("3.141592653589793")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0.0))(result)
assert.InDelta(t, 3.141592653589793, value, 1e-15)
})
t.Run("parse negative float64", func(t *testing.T) {
result := prism.GetOption("-2.718281828459045")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0.0))(result)
assert.InDelta(t, -2.718281828459045, value, 1e-15)
})
t.Run("parse scientific notation", func(t *testing.T) {
result := prism.GetOption("1.5e100")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0.0))(result)
assert.InDelta(t, 1.5e100, value, 1e85)
})
t.Run("parse integer as float", func(t *testing.T) {
result := prism.GetOption("42")
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0.0))(result)
assert.Equal(t, 42.0, value)
})
t.Run("parse invalid float", func(t *testing.T) {
result := prism.GetOption("not-a-number")
assert.True(t, O.IsNone(result))
})
t.Run("reverse get formats float64", func(t *testing.T) {
str := prism.ReverseGet(3.141592653589793)
assert.Contains(t, str, "3.14159")
})
t.Run("round trip", func(t *testing.T) {
original := "3.141592653589793"
result := prism.GetOption(original)
if O.IsSome(result) {
value := O.GetOrElse(F.Constant(0.0))(result)
reconstructed := prism.ReverseGet(value)
// Parse both to compare as floats
origFloat := F.Pipe1(original, prism.GetOption)
reconFloat := F.Pipe1(reconstructed, prism.GetOption)
if O.IsSome(origFloat) && O.IsSome(reconFloat) {
assert.InDelta(t,
O.GetOrElse(F.Constant(0.0))(origFloat),
O.GetOrElse(F.Constant(0.0))(reconFloat),
1e-15)
}
}
})
}
// TestParseIntWithSet tests using Set with ParseInt prism
func TestParseIntWithSet(t *testing.T) {
prism := ParseInt()
t.Run("set on valid integer string", func(t *testing.T) {
setter := Set[string](100)
result := setter(prism)("42")
assert.Equal(t, "100", result)
})
t.Run("set on invalid string returns original", func(t *testing.T) {
setter := Set[string](100)
result := setter(prism)("not-a-number")
assert.Equal(t, "not-a-number", result)
})
}
// TestParseBoolWithSet tests using Set with ParseBool prism
func TestParseBoolWithSet(t *testing.T) {
prism := ParseBool()
t.Run("set on valid bool string", func(t *testing.T) {
setter := Set[string](true)
result := setter(prism)("false")
assert.Equal(t, "true", result)
})
t.Run("set on invalid string returns original", func(t *testing.T) {
setter := Set[string](true)
result := setter(prism)("maybe")
assert.Equal(t, "maybe", result)
})
}

View File

@@ -0,0 +1,83 @@
package readerioeither
import (
"github.com/IBM/fp-go/v2/function"
RA "github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/monoid"
)
//go:inline
func MonadReduceArray[R, E, A, B any](as []ReaderIOEither[R, E, A], reduce func(B, A) B, initial B) ReaderIOEither[R, E, B] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
function.Identity[ReaderIOEither[R, E, A]],
reduce,
initial,
)
}
//go:inline
func ReduceArray[R, E, A, B any](reduce func(B, A) B, initial B) Kleisli[R, E, []ReaderIOEither[R, E, A], B] {
return RA.TraverseReduce[[]ReaderIOEither[R, E, A]](
Of,
Map,
Ap,
function.Identity[ReaderIOEither[R, E, A]],
reduce,
initial,
)
}
//go:inline
func MonadReduceArrayM[R, E, A any](as []ReaderIOEither[R, E, A], m monoid.Monoid[A]) ReaderIOEither[R, E, A] {
return MonadReduceArray(as, m.Concat, m.Empty())
}
//go:inline
func ReduceArrayM[R, E, A any](m monoid.Monoid[A]) Kleisli[R, E, []ReaderIOEither[R, E, A], A] {
return ReduceArray[R, E](m.Concat, m.Empty())
}
//go:inline
func MonadTraverseReduceArray[R, E, A, B, C any](as []A, trfrm Kleisli[R, E, A, B], reduce func(C, B) C, initial C) ReaderIOEither[R, E, C] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
trfrm,
reduce,
initial,
)
}
//go:inline
func TraverseReduceArray[R, E, A, B, C any](trfrm Kleisli[R, E, A, B], reduce func(C, B) C, initial C) Kleisli[R, E, []A, C] {
return RA.TraverseReduce[[]A](
Of,
Map,
Ap,
trfrm,
reduce,
initial,
)
}
//go:inline
func MonadTraverseReduceArrayM[R, E, A, B any](as []A, trfrm Kleisli[R, E, A, B], m monoid.Monoid[B]) ReaderIOEither[R, E, B] {
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
}
//go:inline
func TraverseReduceArrayM[R, E, A, B any](trfrm Kleisli[R, E, A, B], m monoid.Monoid[B]) Kleisli[R, E, []A, B] {
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
}

296
v2/readerioresult/array.go Normal file
View File

@@ -0,0 +1,296 @@
// 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 readerioresult
import (
"github.com/IBM/fp-go/v2/function"
RA "github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/monoid"
)
// MonadReduceArray reduces an array of ReaderIOResults to a single ReaderIOResult by applying a reduction function.
// This is the monadic version that takes the array of ReaderIOResults as the first parameter.
//
// Each ReaderIOResult is evaluated with the same environment R, and the results are accumulated using
// the provided reduce function starting from the initial value. If any ReaderIOResult fails, the entire
// operation fails with that error.
//
// Parameters:
// - as: Array of ReaderIOResults to reduce
// - reduce: Binary function that combines accumulated value with each ReaderIOResult's result
// - initial: Starting value for the reduction
//
// Example:
//
// type Config struct { Base int }
// readers := []readerioresult.ReaderIOResult[Config, int]{
// readerioresult.Of[Config](func(c Config) int { return c.Base + 1 }),
// readerioresult.Of[Config](func(c Config) int { return c.Base + 2 }),
// readerioresult.Of[Config](func(c Config) int { return c.Base + 3 }),
// }
// sum := func(acc, val int) int { return acc + val }
// r := readerioresult.MonadReduceArray(readers, sum, 0)
// result := r(Config{Base: 10})() // result.Of(36) (11 + 12 + 13)
//
//go:inline
func MonadReduceArray[R, A, B any](as []ReaderIOResult[R, A], reduce func(B, A) B, initial B) ReaderIOResult[R, B] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
function.Identity[ReaderIOResult[R, A]],
reduce,
initial,
)
}
// ReduceArray returns a curried function that reduces an array of ReaderIOResults to a single ReaderIOResult.
// This is the curried version where the reduction function and initial value are provided first,
// returning a function that takes the array of ReaderIOResults.
//
// Parameters:
// - reduce: Binary function that combines accumulated value with each ReaderIOResult's result
// - initial: Starting value for the reduction
//
// Returns:
// - A function that takes an array of ReaderIOResults and returns a ReaderIOResult of the reduced result
//
// Example:
//
// type Config struct { Multiplier int }
// product := func(acc, val int) int { return acc * val }
// reducer := readerioresult.ReduceArray[Config](product, 1)
// readers := []readerioresult.ReaderIOResult[Config, int]{
// readerioresult.Of[Config](func(c Config) int { return c.Multiplier * 2 }),
// readerioresult.Of[Config](func(c Config) int { return c.Multiplier * 3 }),
// }
// r := reducer(readers)
// result := r(Config{Multiplier: 5})() // result.Of(150) (10 * 15)
//
//go:inline
func ReduceArray[R, A, B any](reduce func(B, A) B, initial B) Kleisli[R, []ReaderIOResult[R, A], B] {
return RA.TraverseReduce[[]ReaderIOResult[R, A]](
Of,
Map,
Ap,
function.Identity[ReaderIOResult[R, A]],
reduce,
initial,
)
}
// MonadReduceArrayM reduces an array of ReaderIOResults using a Monoid to combine the results.
// This is the monadic version that takes the array of ReaderIOResults as the first parameter.
//
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
// for the reduction, making it convenient when working with monoidal types. If any ReaderIOResult
// fails, the entire operation fails with that error.
//
// Parameters:
// - as: Array of ReaderIOResults to reduce
// - m: Monoid that defines how to combine the ReaderIOResult results
//
// Example:
//
// type Config struct { Factor int }
// readers := []readerioresult.ReaderIOResult[Config, int]{
// readerioresult.Of[Config](func(c Config) int { return c.Factor }),
// readerioresult.Of[Config](func(c Config) int { return c.Factor * 2 }),
// readerioresult.Of[Config](func(c Config) int { return c.Factor * 3 }),
// }
// intAddMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
// r := readerioresult.MonadReduceArrayM(readers, intAddMonoid)
// result := r(Config{Factor: 5})() // result.Of(30) (5 + 10 + 15)
//
//go:inline
func MonadReduceArrayM[R, A any](as []ReaderIOResult[R, A], m monoid.Monoid[A]) ReaderIOResult[R, A] {
return MonadReduceArray(as, m.Concat, m.Empty())
}
// ReduceArrayM returns a curried function that reduces an array of ReaderIOResults using a Monoid.
// This is the curried version where the Monoid is provided first, returning a function
// that takes the array of ReaderIOResults.
//
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
// for the reduction.
//
// Parameters:
// - m: Monoid that defines how to combine the ReaderIOResult results
//
// Returns:
// - A function that takes an array of ReaderIOResults and returns a ReaderIOResult of the reduced result
//
// Example:
//
// type Config struct { Scale int }
// intMultMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
// reducer := readerioresult.ReduceArrayM[Config](intMultMonoid)
// readers := []readerioresult.ReaderIOResult[Config, int]{
// readerioresult.Of[Config](func(c Config) int { return c.Scale }),
// readerioresult.Of[Config](func(c Config) int { return c.Scale * 2 }),
// }
// r := reducer(readers)
// result := r(Config{Scale: 3})() // result.Of(18) (3 * 6)
//
//go:inline
func ReduceArrayM[R, A any](m monoid.Monoid[A]) Kleisli[R, []ReaderIOResult[R, A], A] {
return ReduceArray[R](m.Concat, m.Empty())
}
// MonadTraverseReduceArray transforms and reduces an array in one operation.
// This is the monadic version that takes the array as the first parameter.
//
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
// Then, the ReaderIOResult results are reduced using the provided reduction function.
// If any transformation fails, the entire operation fails with that error.
//
// This is more efficient than calling TraverseArray followed by a separate reduce operation,
// as it combines both operations into a single traversal.
//
// Parameters:
// - as: Array of elements to transform and reduce
// - trfrm: Function that transforms each element into a ReaderIOResult
// - reduce: Binary function that combines accumulated value with each transformed result
// - initial: Starting value for the reduction
//
// Example:
//
// type Config struct { Multiplier int }
// numbers := []int{1, 2, 3, 4}
// multiply := func(n int) readerioresult.ReaderIOResult[Config, int] {
// return readerioresult.Of[Config](func(c Config) int { return n * c.Multiplier })
// }
// sum := func(acc, val int) int { return acc + val }
// r := readerioresult.MonadTraverseReduceArray(numbers, multiply, sum, 0)
// result := r(Config{Multiplier: 10})() // result.Of(100) (10 + 20 + 30 + 40)
//
//go:inline
func MonadTraverseReduceArray[R, A, B, C any](as []A, trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) ReaderIOResult[R, C] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
trfrm,
reduce,
initial,
)
}
// TraverseReduceArray returns a curried function that transforms and reduces an array.
// This is the curried version where the transformation function, reduce function, and initial value
// are provided first, returning a function that takes the array.
//
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
// Then, the ReaderIOResult results are reduced using the provided reduction function.
//
// Parameters:
// - trfrm: Function that transforms each element into a ReaderIOResult
// - reduce: Binary function that combines accumulated value with each transformed result
// - initial: Starting value for the reduction
//
// Returns:
// - A function that takes an array and returns a ReaderIOResult of the reduced result
//
// Example:
//
// type Config struct { Base int }
// addBase := func(n int) readerioresult.ReaderIOResult[Config, int] {
// return readerioresult.Of[Config](func(c Config) int { return n + c.Base })
// }
// product := func(acc, val int) int { return acc * val }
// transformer := readerioresult.TraverseReduceArray(addBase, product, 1)
// r := transformer([]int{2, 3, 4})
// result := r(Config{Base: 10})() // result.Of(2184) (12 * 13 * 14)
//
//go:inline
func TraverseReduceArray[R, A, B, C any](trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Kleisli[R, []A, C] {
return RA.TraverseReduce[[]A](
Of,
Map,
Ap,
trfrm,
reduce,
initial,
)
}
// MonadTraverseReduceArrayM transforms and reduces an array using a Monoid.
// This is the monadic version that takes the array as the first parameter.
//
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
// Then, the ReaderIOResult results are reduced using the Monoid's binary operation and identity element.
// If any transformation fails, the entire operation fails with that error.
//
// This combines transformation and monoidal reduction in a single efficient operation.
//
// Parameters:
// - as: Array of elements to transform and reduce
// - trfrm: Function that transforms each element into a ReaderIOResult
// - m: Monoid that defines how to combine the transformed results
//
// Example:
//
// type Config struct { Offset int }
// numbers := []int{1, 2, 3}
// addOffset := func(n int) readerioresult.ReaderIOResult[Config, int] {
// return readerioresult.Of[Config](func(c Config) int { return n + c.Offset })
// }
// intSumMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
// r := readerioresult.MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
// result := r(Config{Offset: 100})() // result.Of(306) (101 + 102 + 103)
//
//go:inline
func MonadTraverseReduceArrayM[R, A, B any](as []A, trfrm Kleisli[R, A, B], m monoid.Monoid[B]) ReaderIOResult[R, B] {
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
}
// TraverseReduceArrayM returns a curried function that transforms and reduces an array using a Monoid.
// This is the curried version where the transformation function and Monoid are provided first,
// returning a function that takes the array.
//
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
// Then, the ReaderIOResult results are reduced using the Monoid's binary operation and identity element.
//
// Parameters:
// - trfrm: Function that transforms each element into a ReaderIOResult
// - m: Monoid that defines how to combine the transformed results
//
// Returns:
// - A function that takes an array and returns a ReaderIOResult of the reduced result
//
// Example:
//
// type Config struct { Factor int }
// scale := func(n int) readerioresult.ReaderIOResult[Config, int] {
// return readerioresult.Of[Config](func(c Config) int { return n * c.Factor })
// }
// intProdMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
// transformer := readerioresult.TraverseReduceArrayM(scale, intProdMonoid)
// r := transformer([]int{2, 3, 4})
// result := r(Config{Factor: 5})() // result.Of(3000) (10 * 15 * 20)
//
//go:inline
func TraverseReduceArrayM[R, A, B any](trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Kleisli[R, []A, B] {
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
}

View File

@@ -24,6 +24,7 @@ import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
TST "github.com/IBM/fp-go/v2/internal/testing"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -73,3 +74,341 @@ func TestSequenceArrayError(t *testing.T) {
// run across four bits
s(4)(t)
}
func TestMonadReduceArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
readers := []ReaderIOResult[Config, int]{
Of[Config](11),
Of[Config](12),
Of[Config](13),
}
sum := func(acc, val int) int { return acc + val }
r := MonadReduceArray(readers, sum, 0)
res := r(config)()
assert.Equal(t, result.Of(36), res) // 11 + 12 + 13
}
func TestMonadReduceArrayWithError(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
testErr := errors.New("test error")
readers := []ReaderIOResult[Config, int]{
Of[Config](11),
Left[Config, int](testErr),
Of[Config](13),
}
sum := func(acc, val int) int { return acc + val }
r := MonadReduceArray(readers, sum, 0)
res := r(config)()
assert.True(t, result.IsLeft(res))
val, err := result.Unwrap(res)
assert.Equal(t, 0, val)
assert.Equal(t, testErr, err)
}
func TestReduceArray(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 5}
product := func(acc, val int) int { return acc * val }
reducer := ReduceArray[Config](product, 1)
readers := []ReaderIOResult[Config, int]{
Of[Config](10),
Of[Config](15),
}
r := reducer(readers)
res := r(config)()
assert.Equal(t, result.Of(150), res) // 10 * 15
}
func TestReduceArrayWithError(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 5}
testErr := errors.New("multiplication error")
product := func(acc, val int) int { return acc * val }
reducer := ReduceArray[Config](product, 1)
readers := []ReaderIOResult[Config, int]{
Of[Config](10),
Left[Config, int](testErr),
}
r := reducer(readers)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestMonadReduceArrayM(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
readers := []ReaderIOResult[Config, int]{
Of[Config](5),
Of[Config](10),
Of[Config](15),
}
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadReduceArrayM(readers, intAddMonoid)
res := r(config)()
assert.Equal(t, result.Of(30), res) // 5 + 10 + 15
}
func TestMonadReduceArrayMWithError(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
testErr := errors.New("monoid error")
readers := []ReaderIOResult[Config, int]{
Of[Config](5),
Left[Config, int](testErr),
Of[Config](15),
}
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadReduceArrayM(readers, intAddMonoid)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestReduceArrayM(t *testing.T) {
type Config struct{ Scale int }
config := Config{Scale: 3}
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
reducer := ReduceArrayM[Config](intMultMonoid)
readers := []ReaderIOResult[Config, int]{
Of[Config](3),
Of[Config](6),
}
r := reducer(readers)
res := r(config)()
assert.Equal(t, result.Of(18), res) // 3 * 6
}
func TestReduceArrayMWithError(t *testing.T) {
type Config struct{ Scale int }
config := Config{Scale: 3}
testErr := errors.New("scale error")
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
reducer := ReduceArrayM[Config](intMultMonoid)
readers := []ReaderIOResult[Config, int]{
Of[Config](3),
Left[Config, int](testErr),
}
r := reducer(readers)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestMonadTraverseReduceArray(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 10}
numbers := []int{1, 2, 3, 4}
multiply := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n * 10)
}
sum := func(acc, val int) int { return acc + val }
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
res := r(config)()
assert.Equal(t, result.Of(100), res) // 10 + 20 + 30 + 40
}
func TestMonadTraverseReduceArrayWithError(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 10}
testErr := errors.New("transform error")
numbers := []int{1, 2, 3, 4}
multiply := func(n int) ReaderIOResult[Config, int] {
if n == 3 {
return Left[Config, int](testErr)
}
return Of[Config](n * 10)
}
sum := func(acc, val int) int { return acc + val }
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestTraverseReduceArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
addBase := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n + 10)
}
product := func(acc, val int) int { return acc * val }
transformer := TraverseReduceArray(addBase, product, 1)
r := transformer([]int{2, 3, 4})
res := r(config)()
assert.Equal(t, result.Of(2184), res) // 12 * 13 * 14
}
func TestTraverseReduceArrayWithError(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
testErr := errors.New("addition error")
addBase := func(n int) ReaderIOResult[Config, int] {
if n == 3 {
return Left[Config, int](testErr)
}
return Of[Config](n + 10)
}
product := func(acc, val int) int { return acc * val }
transformer := TraverseReduceArray(addBase, product, 1)
r := transformer([]int{2, 3, 4})
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestMonadTraverseReduceArrayM(t *testing.T) {
type Config struct{ Offset int }
config := Config{Offset: 100}
numbers := []int{1, 2, 3}
addOffset := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n + 100)
}
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
res := r(config)()
assert.Equal(t, result.Of(306), res) // 101 + 102 + 103
}
func TestMonadTraverseReduceArrayMWithError(t *testing.T) {
type Config struct{ Offset int }
config := Config{Offset: 100}
testErr := errors.New("offset error")
numbers := []int{1, 2, 3}
addOffset := func(n int) ReaderIOResult[Config, int] {
if n == 2 {
return Left[Config, int](testErr)
}
return Of[Config](n + 100)
}
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestTraverseReduceArrayM(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
scale := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n * 5)
}
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
transformer := TraverseReduceArrayM(scale, intProdMonoid)
r := transformer([]int{2, 3, 4})
res := r(config)()
assert.Equal(t, result.Of(3000), res) // 10 * 15 * 20
}
func TestTraverseReduceArrayMWithError(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
testErr := errors.New("scaling error")
scale := func(n int) ReaderIOResult[Config, int] {
if n == 3 {
return Left[Config, int](testErr)
}
return Of[Config](n * 5)
}
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
transformer := TraverseReduceArrayM(scale, intProdMonoid)
r := transformer([]int{2, 3, 4})
res := r(config)()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Equal(t, testErr, err)
}
func TestReduceArrayEmptyArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
sum := func(acc, val int) int { return acc + val }
reducer := ReduceArray[Config](sum, 100)
readers := []ReaderIOResult[Config, int]{}
r := reducer(readers)
res := r(config)()
assert.Equal(t, result.Of(100), res) // Should return initial value
}
func TestTraverseReduceArrayEmptyArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
addBase := func(n int) ReaderIOResult[Config, int] {
return Of[Config](n + 10)
}
sum := func(acc, val int) int { return acc + val }
transformer := TraverseReduceArray(addBase, sum, 50)
r := transformer([]int{})
res := r(config)()
assert.Equal(t, result.Of(50), res) // Should return initial value
}