mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-07 23:03:15 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f652a94c3a | ||
|
|
774db88ca5 | ||
|
|
62a3365b20 | ||
|
|
d9a16a6771 | ||
|
|
8949cc7dca |
@@ -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")
|
||||
}
|
||||
|
||||
124
v2/io/logging.go
124
v2/io/logging.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
83
v2/readerioeither/array.go
Normal file
83
v2/readerioeither/array.go
Normal 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
296
v2/readerioresult/array.go
Normal 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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user