1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-19 23:42:05 +02:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Dr. Carsten Leue
d3c466bfb7 fix: some cleanup
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-19 13:18:49 +01:00
Dr. Carsten Leue
a6c6ea804f fix: overhaul record
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-18 18:32:45 +01:00
Dr. Carsten Leue
31ff98901e fix: latest doc fixes
BREAKING CHANGE: new v2

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-18 16:59:23 +01:00
Dr. Carsten Leue
255cf4353c fix: better formatting
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-18 16:07:26 +01:00
Dr. Carsten Leue
4dfc1b5a44 fix: better doc and implementation of retry
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-17 16:28:28 +01:00
Dr. Carsten Leue
20398e67a9 fix: better doc and implementation of retry
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-17 15:58:11 +01:00
Dr. Carsten Leue
fceda15701 doc: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-17 10:11:58 +01:00
118 changed files with 12837 additions and 1271 deletions

View File

@@ -452,17 +452,27 @@ func process() IOResult[string] {
### Core Modules
#### Standard Packages (Struct-based)
- **Option** - Represent optional values without nil
- **Either** - Type-safe error handling with left/right values
- **Result** - Simplified Either with error as left type
- **Result** - Simplified Either with error as left type (recommended for error handling)
- **IO** - Lazy evaluation and side effect management
- **IOEither** - Combine IO with error handling
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
- **Reader** - Dependency injection pattern
- **ReaderIOEither** - Combine Reader, IO, and Either for complex workflows
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
#### Idiomatic Packages (Tuple-based, High Performance)
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples
- **idiomatic/result** - Result monad using native Go `(value, error)` tuples
- **idiomatic/ioresult** - IOResult monad using `func() (value, error)` for IO operations
- **idiomatic/readerresult** - Reader monad combined with Result pattern
- **idiomatic/readerioresult** - Reader monad combined with IOResult pattern
The idiomatic packages offer 2-10x performance improvements and zero allocations by using Go's native tuple patterns instead of struct wrappers. Use them for performance-critical code or when you prefer Go's native error handling style.
## 🤔 Should I Migrate?
**Migrate to V2 if:**

View File

@@ -368,5 +368,3 @@ func TestToNonEmptyArrayUseCases(t *testing.T) {
assert.Equal(t, "default", result2)
})
}
// Made with Bob

View File

@@ -76,6 +76,7 @@ type fieldInfo struct {
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag
IsComparable bool // true if the type is comparable (can use ==)
IsEmbedded bool // true if this field comes from an embedded struct
}
// templateData holds data for template rendering
@@ -110,6 +111,17 @@ type {{.Name}}RefLenses{{.TypeParams}} struct {
{{- if .IsComparable}}
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
// prisms
{{- range .Fields}}
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
type {{.Name}}Prisms{{.TypeParams}} struct {
{{- range .Fields}}
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
`
@@ -182,6 +194,47 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
{{- end}}
}
}
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
{{- range .Fields}}
{{- if .IsComparable}}
_fromNonZero{{.Name}} := __option.FromNonZero[{{.TypeName}}]()
_prism{{.Name}} := __prism.MakePrismWithName(
func(s {{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return _fromNonZero{{.Name}}(s.{{.Name}}) },
func(v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} {
{{- if .IsEmbedded}}
var result {{$.Name}}{{$.TypeParamNames}}
result.{{.Name}} = v
return result
{{- else}}
return {{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
{{- end}}
},
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
)
{{- else}}
_prism{{.Name}} := __prism.MakePrismWithName(
func(s {{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return __option.Some(s.{{.Name}}) },
func(v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} {
{{- if .IsEmbedded}}
var result {{$.Name}}{{$.TypeParamNames}}
result.{{.Name}} = v
return result
{{- else}}
return {{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
{{- end}}
},
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
)
{{- end}}
{{- end}}
return {{.Name}}Prisms{{.TypeParamNames}} {
{{- range .Fields}}
{{.Name}}: _prism{{.Name}},
{{- end}}
}
}
`
var (
@@ -506,6 +559,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
BaseType: baseType,
IsOptional: isOptional,
IsComparable: isComparable,
IsEmbedded: true,
},
fieldType: field.Type,
})
@@ -833,6 +887,8 @@ func generateLensFile(absDir, filename, packageName string, structs []structInfo
f.WriteString("import (\n")
// Standard fp-go imports always needed
f.WriteString("\t__lens \"github.com/IBM/fp-go/v2/optics/lens\"\n")
f.WriteString("\t__option \"github.com/IBM/fp-go/v2/option\"\n")
f.WriteString("\t__prism \"github.com/IBM/fp-go/v2/optics/prism\"\n")
f.WriteString("\t__lens_option \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
f.WriteString("\t__iso_option \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")

View File

@@ -753,3 +753,17 @@ func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return context.WithDeadline(ctx, deadline)
})
}
// Delay creates an operation that passes in the value after some delay
//
//go:inline
func Delay[A any](delay time.Duration) Operator[A, A] {
return RIO.Delay[context.Context, A](delay)
}
// After creates an operation that passes after the given [time.Time]
//
//go:inline
func After[R, E, A any](timestamp time.Time) Operator[A, A] {
return RIO.After[context.Context, A](timestamp)
}

View File

@@ -20,6 +20,6 @@ import (
)
//go:inline
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
return readerio.TailRec(f)
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerio
import (
"github.com/IBM/fp-go/v2/retry"
RG "github.com/IBM/fp-go/v2/retry/generic"
)
//go:inline
func Retrying[A any](
policy retry.RetryPolicy,
action Kleisli[retry.RetryStatus, A],
check func(A) bool,
) ReaderIO[A] {
// get an implementation for the types
return RG.Retrying(
Chain[A, A],
Chain[retry.RetryStatus, A],
Of[A],
Of[retry.RetryStatus],
Delay[retry.RetryStatus],
policy,
action,
check,
)
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/tailrec"
)
type (
@@ -72,4 +73,6 @@ type (
Consumer[A any] = consumer.Consumer[A]
Either[E, A any] = either.Either[E, A]
Trampoline[B, L any] = tailrec.Trampoline[B, L]
)

View File

@@ -24,7 +24,6 @@ import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
@@ -96,7 +95,7 @@ func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return RIOR.Bind(setter, F.Flow2(f, WithContext))
return RIOR.Bind(setter, WithContextK(f))
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
@@ -128,6 +127,13 @@ func BindTo[S1, T any](
return RIOR.BindTo[context.Context](setter)
}
//go:inline
func BindToP[S1, T any](
setter Prism[S1, T],
) Operator[T, S1] {
return BindTo(setter.ReverseGet)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other.
@@ -214,7 +220,7 @@ func ApS[S1, S2, T any](
//
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa ReaderIOResult[T],
) Operator[S, S] {
return ApS(lens.Set, fa)
@@ -253,10 +259,10 @@ func ApSL[S, T any](
//
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return RIOR.BindL(lens, F.Flow2(f, WithContext))
return RIOR.BindL(lens, WithContextK(f))
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
@@ -289,7 +295,7 @@ func BindL[S, T any](
//
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f Endomorphism[T],
) Operator[S, S] {
return RIOR.LetL[context.Context](lens, f)
@@ -322,7 +328,7 @@ func LetL[S, T any](
//
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
b T,
) Operator[S, S] {
return RIOR.LetToL[context.Context](lens, b)
@@ -443,7 +449,7 @@ func BindResultK[S1, S2, T any](
//
//go:inline
func BindIOEitherKL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f ioresult.Kleisli[T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[T]))
@@ -458,7 +464,7 @@ func BindIOEitherKL[S, T any](
//
//go:inline
func BindIOResultKL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f ioresult.Kleisli[T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[T]))
@@ -474,7 +480,7 @@ func BindIOResultKL[S, T any](
//
//go:inline
func BindIOKL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f io.Kleisli[T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIO[T]))
@@ -490,7 +496,7 @@ func BindIOKL[S, T any](
//
//go:inline
func BindReaderKL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f reader.Kleisli[context.Context, T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromReader[T]))
@@ -506,7 +512,7 @@ func BindReaderKL[S, T any](
//
//go:inline
func BindReaderIOKL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f readerio.Kleisli[T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
@@ -627,7 +633,7 @@ func ApResultS[S1, S2, T any](
//
//go:inline
func ApIOEitherSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa IOResult[T],
) Operator[S, S] {
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
@@ -642,7 +648,7 @@ func ApIOEitherSL[S, T any](
//
//go:inline
func ApIOResultSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa IOResult[T],
) Operator[S, S] {
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
@@ -657,7 +663,7 @@ func ApIOResultSL[S, T any](
//
//go:inline
func ApIOSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa IO[T],
) Operator[S, S] {
return ApSL(lens, FromIO(fa))
@@ -672,7 +678,7 @@ func ApIOSL[S, T any](
//
//go:inline
func ApReaderSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa Reader[context.Context, T],
) Operator[S, S] {
return ApSL(lens, FromReader(fa))
@@ -687,7 +693,7 @@ func ApReaderSL[S, T any](
//
//go:inline
func ApReaderIOSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa ReaderIO[T],
) Operator[S, S] {
return ApSL(lens, FromReaderIO(fa))
@@ -702,7 +708,7 @@ func ApReaderIOSL[S, T any](
//
//go:inline
func ApEitherSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa Result[T],
) Operator[S, S] {
return ApSL(lens, FromEither(fa))
@@ -717,7 +723,7 @@ func ApEitherSL[S, T any](
//
//go:inline
func ApResultSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa Result[T],
) Operator[S, S] {
return ApSL(lens, FromResult(fa))

View File

@@ -19,6 +19,7 @@ import (
"context"
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
)
@@ -40,3 +41,11 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
return CIOE.WithContext(ctx, ma(ctx))
}
}
//go:inline
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
return F.Flow2(
f,
WithContext,
)
}

View File

@@ -152,7 +152,7 @@ func MapTo[A, B any](b B) Operator[A, B] {
//
//go:inline
func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChain(ma, function.Flow2(f, WithContext))
return RIOR.MonadChain(ma, WithContextK(f))
}
// Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
@@ -165,7 +165,7 @@ func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return RIOR.Chain(function.Flow2(f, WithContext))
return RIOR.Chain(WithContextK(f))
}
// MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
@@ -179,12 +179,12 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
//
//go:inline
func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirst(ma, function.Flow2(f, WithContext))
return RIOR.MonadChainFirst(ma, WithContextK(f))
}
//go:inline
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTap(ma, function.Flow2(f, WithContext))
return RIOR.MonadTap(ma, WithContextK(f))
}
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
@@ -197,12 +197,12 @@ func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A]
//
//go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirst(function.Flow2(f, WithContext))
return RIOR.ChainFirst(WithContextK(f))
}
//go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.Tap(function.Flow2(f, WithContext))
return RIOR.Tap(WithContextK(f))
}
// Of creates a [ReaderIOResult] that always succeeds with the given value.
@@ -401,6 +401,11 @@ func ChainEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
return RIOR.ChainEitherK[context.Context](f)
}
//go:inline
func ChainResultK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
return RIOR.ChainEitherK[context.Context](f)
}
// MonadChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
// The Either-returning function is executed for its validation/side effects only.
//
@@ -915,7 +920,7 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
//
//go:inline
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
return RIOR.MonadChainLeft(fa, function.Flow2(f, WithContext))
return RIOR.MonadChainLeft(fa, WithContextK(f))
}
// ChainLeft is the curried version of [MonadChainLeft].
@@ -923,7 +928,7 @@ func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIORe
//
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
return RIOR.ChainLeft(function.Flow2(f, WithContext))
return RIOR.ChainLeft(WithContextK(f))
}
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
@@ -936,12 +941,12 @@ func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
//
//go:inline
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstLeft(ma, function.Flow2(f, WithContext))
return RIOR.MonadChainFirstLeft(ma, WithContextK(f))
}
//go:inline
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadTapLeft(ma, function.Flow2(f, WithContext))
return RIOR.MonadTapLeft(ma, WithContextK(f))
}
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
@@ -953,12 +958,12 @@ func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOR
//
//go:inline
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.ChainFirstLeft[A](function.Flow2(f, WithContext))
return RIOR.ChainFirstLeft[A](WithContextK(f))
}
//go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.TapLeft[A](function.Flow2(f, WithContext))
return RIOR.TapLeft[A](WithContextK(f))
}
// Local transforms the context.Context environment before passing it to a ReaderIOResult computation.

View File

@@ -16,7 +16,6 @@
package readerioresult
import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
@@ -36,9 +35,9 @@ import (
//
// # How It Works
//
// TailRec takes a Kleisli arrow that returns Either[A, B]:
// - Left(A): Continue recursion with the new state A
// - Right(B): Terminate recursion successfully and return the final result B
// TailRec takes a Kleisli arrow that returns Trampoline[A, B]:
// - Bounce(A): Continue recursion with the new state A
// - Land(B): Terminate recursion successfully and return the final result B
//
// The function wraps each iteration with [WithContext] to ensure context cancellation
// is checked before each recursive step. If the context is cancelled, the recursion
@@ -51,11 +50,11 @@ import (
//
// # Parameters
//
// - f: A Kleisli arrow (A => ReaderIOResult[Either[A, B]]) that:
// - f: A Kleisli arrow (A => ReaderIOResult[Trampoline[A, B]]) that:
// - Takes the current state A
// - Returns a ReaderIOResult that depends on [context.Context]
// - Can fail with error (Left in the outer Either)
// - Produces Either[A, B] to control recursion flow (Right in the outer Either)
// - Produces Trampoline[A, B] to control recursion flow (Right in the outer Either)
//
// # Returns
//
@@ -93,15 +92,15 @@ import (
//
// # Example: Cancellable Countdown
//
// countdownStep := func(n int) readerioresult.ReaderIOResult[either.Either[int, string]] {
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[int, string]] {
// return func() either.Either[error, either.Either[int, string]] {
// countdownStep := func(n int) readerioresult.ReaderIOResult[tailrec.Trampoline[int, string]] {
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[int, string]] {
// return func() either.Either[error, tailrec.Trampoline[int, string]] {
// if n <= 0 {
// return either.Right[error](either.Right[int]("Done!"))
// return either.Right[error](tailrec.Land[int]("Done!"))
// }
// // Simulate some work
// time.Sleep(100 * time.Millisecond)
// return either.Right[error](either.Left[string](n - 1))
// return either.Right[error](tailrec.Bounce[string](n - 1))
// }
// }
// }
@@ -120,20 +119,20 @@ import (
// processed []string
// }
//
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[either.Either[ProcessState, []string]] {
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[ProcessState, []string]] {
// return func() either.Either[error, either.Either[ProcessState, []string]] {
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[tailrec.Trampoline[ProcessState, []string]] {
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[ProcessState, []string]] {
// return func() either.Either[error, tailrec.Trampoline[ProcessState, []string]] {
// if len(state.files) == 0 {
// return either.Right[error](either.Right[ProcessState](state.processed))
// return either.Right[error](tailrec.Land[ProcessState](state.processed))
// }
//
// file := state.files[0]
// // Process file (this could be cancelled via context)
// if err := processFileWithContext(ctx, file); err != nil {
// return either.Left[either.Either[ProcessState, []string]](err)
// return either.Left[tailrec.Trampoline[ProcessState, []string]](err)
// }
//
// return either.Right[error](either.Left[[]string](ProcessState{
// return either.Right[error](tailrec.Bounce[[]string](ProcessState{
// files: state.files[1:],
// processed: append(state.processed, file),
// }))
@@ -179,6 +178,6 @@ import (
// - [Left]/[Right]: For creating error/success values
//
//go:inline
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
return RIOR.TailRec(F.Flow2(f, WithContext))
}

View File

@@ -25,19 +25,20 @@ import (
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTailRec_BasicRecursion(t *testing.T) {
// Test basic countdown recursion
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
@@ -55,13 +56,13 @@ func TestTailRec_FactorialRecursion(t *testing.T) {
acc int
}
factorialStep := func(state FactorialState) ReaderIOResult[E.Either[FactorialState, int]] {
return func(ctx context.Context) IOEither[E.Either[FactorialState, int]] {
return func() Either[E.Either[FactorialState, int]] {
factorialStep := func(state FactorialState) ReaderIOResult[Trampoline[FactorialState, int]] {
return func(ctx context.Context) IOEither[Trampoline[FactorialState, int]] {
return func() Either[Trampoline[FactorialState, int]] {
if state.n <= 1 {
return E.Right[error](E.Right[FactorialState](state.acc))
return E.Right[error](tailrec.Land[FactorialState](state.acc))
}
return E.Right[error](E.Left[int](FactorialState{
return E.Right[error](tailrec.Bounce[int](FactorialState{
n: state.n - 1,
acc: state.acc * state.n,
}))
@@ -79,16 +80,16 @@ func TestTailRec_ErrorHandling(t *testing.T) {
// Test that errors are properly propagated
testErr := errors.New("computation error")
errorStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
errorStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
if n == 3 {
return E.Left[E.Either[int, string]](testErr)
return E.Left[Trampoline[int, string]](testErr)
}
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
@@ -105,18 +106,18 @@ func TestTailRec_ContextCancellation(t *testing.T) {
// Test that recursion gets cancelled early when context is canceled
var iterationCount int32
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
atomic.AddInt32(&iterationCount, 1)
// Simulate some work
time.Sleep(50 * time.Millisecond)
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
@@ -144,13 +145,13 @@ func TestTailRec_ContextCancellation(t *testing.T) {
func TestTailRec_ImmediateCancellation(t *testing.T) {
// Test with an already cancelled context
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
@@ -173,13 +174,13 @@ func TestTailRec_StackSafety(t *testing.T) {
// Test that deep recursion doesn't cause stack overflow
const largeN = 10000
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
return func(ctx context.Context) IOEither[E.Either[int, int]] {
return func() Either[E.Either[int, int]] {
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
return func() Either[Trampoline[int, int]] {
if n <= 0 {
return E.Right[error](E.Right[int](0))
return E.Right[error](tailrec.Land[int](0))
}
return E.Right[error](E.Left[int](n - 1))
return E.Right[error](tailrec.Bounce[int](n - 1))
}
}
}
@@ -195,9 +196,9 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
const largeN = 100000
var iterationCount int32
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
return func(ctx context.Context) IOEither[E.Either[int, int]] {
return func() Either[E.Either[int, int]] {
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
return func() Either[Trampoline[int, int]] {
atomic.AddInt32(&iterationCount, 1)
// Add a small delay every 1000 iterations to make cancellation more likely
@@ -206,9 +207,9 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
}
if n <= 0 {
return E.Right[error](E.Right[int](0))
return E.Right[error](tailrec.Land[int](0))
}
return E.Right[error](E.Left[int](n - 1))
return E.Right[error](tailrec.Bounce[int](n - 1))
}
}
}
@@ -240,22 +241,22 @@ func TestTailRec_ComplexState(t *testing.T) {
errors []error
}
processStep := func(state ProcessState) ReaderIOResult[E.Either[ProcessState, []string]] {
return func(ctx context.Context) IOEither[E.Either[ProcessState, []string]] {
return func() Either[E.Either[ProcessState, []string]] {
processStep := func(state ProcessState) ReaderIOResult[Trampoline[ProcessState, []string]] {
return func(ctx context.Context) IOEither[Trampoline[ProcessState, []string]] {
return func() Either[Trampoline[ProcessState, []string]] {
if A.IsEmpty(state.items) {
return E.Right[error](E.Right[ProcessState](state.processed))
return E.Right[error](tailrec.Land[ProcessState](state.processed))
}
item := state.items[0]
// Simulate processing that might fail for certain items
if item == "error-item" {
return E.Left[E.Either[ProcessState, []string]](
return E.Left[Trampoline[ProcessState, []string]](
fmt.Errorf("failed to process item: %s", item))
}
return E.Right[error](E.Left[[]string](ProcessState{
return E.Right[error](tailrec.Bounce[[]string](ProcessState{
items: state.items[1:],
processed: append(state.processed, item),
errors: state.errors,
@@ -302,18 +303,18 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
var processedCount int32
processFileStep := func(state FileProcessState) ReaderIOResult[E.Either[FileProcessState, int]] {
return func(ctx context.Context) IOEither[E.Either[FileProcessState, int]] {
return func() Either[E.Either[FileProcessState, int]] {
processFileStep := func(state FileProcessState) ReaderIOResult[Trampoline[FileProcessState, int]] {
return func(ctx context.Context) IOEither[Trampoline[FileProcessState, int]] {
return func() Either[Trampoline[FileProcessState, int]] {
if A.IsEmpty(state.files) {
return E.Right[error](E.Right[FileProcessState](state.processed))
return E.Right[error](tailrec.Land[FileProcessState](state.processed))
}
// Simulate file processing time
time.Sleep(20 * time.Millisecond)
atomic.AddInt32(&processedCount, 1)
return E.Right[error](E.Left[int](FileProcessState{
return E.Right[error](tailrec.Bounce[int](FileProcessState{
files: state.files[1:],
processed: state.processed + 1,
}))
@@ -356,10 +357,10 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
func TestTailRec_ZeroIterations(t *testing.T) {
// Test case where recursion terminates immediately
immediateStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
return E.Right[error](E.Right[int]("immediate"))
immediateStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
return E.Right[error](tailrec.Land[int]("immediate"))
}
}
}
@@ -374,16 +375,16 @@ func TestTailRec_ContextWithDeadline(t *testing.T) {
// Test with context deadline
var iterationCount int32
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
atomic.AddInt32(&iterationCount, 1)
time.Sleep(30 * time.Millisecond)
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
@@ -410,17 +411,17 @@ func TestTailRec_ContextWithValue(t *testing.T) {
type contextKey string
const testKey contextKey = "test"
valueStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
valueStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
value := ctx.Value(testKey)
require.NotNil(t, value)
assert.Equal(t, "test-value", value.(string))
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}

View File

@@ -0,0 +1,179 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache LicensVersion 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 (
"context"
"time"
RIO "github.com/IBM/fp-go/v2/context/readerio"
R "github.com/IBM/fp-go/v2/retry"
RG "github.com/IBM/fp-go/v2/retry/generic"
)
// Retrying retries a ReaderIOResult computation according to a retry policy with context awareness.
//
// This function implements a retry mechanism for operations that depend on a [context.Context],
// perform side effects (IO), and can fail (Result). It respects context cancellation, meaning
// that if the context is cancelled during retry delays, the operation will stop immediately
// and return the cancellation error.
//
// The retry loop will continue until one of the following occurs:
// - The action succeeds and the check function returns false (no retry needed)
// - The retry policy returns None (retry limit reached)
// - The check function returns false (indicating success or a non-retryable failure)
// - The context is cancelled (returns context.Canceled or context.DeadlineExceeded)
//
// Parameters:
//
// - policy: A RetryPolicy that determines when and how long to wait between retries.
// The policy receives a RetryStatus on each iteration and returns an optional delay.
// If it returns None, retrying stops. Common policies include LimitRetries,
// ExponentialBackoff, and CapDelay from the retry package.
//
// - action: A Kleisli arrow that takes a RetryStatus and returns a ReaderIOResult[A].
// This function is called on each retry attempt and receives information about the
// current retry state (iteration number, cumulative delay, etc.). The action depends
// on a context.Context and produces a Result[A]. The context passed to the action
// will be the same context used for retry delays, so cancellation is properly propagated.
//
// - check: A predicate function that examines the Result[A] and returns true if the
// operation should be retried, or false if it should stop. This allows you to
// distinguish between retryable failures (e.g., network timeouts) and permanent
// failures (e.g., invalid input). Note that context cancellation errors will
// automatically stop retrying regardless of this function's return value.
//
// Returns:
//
// A ReaderIOResult[A] that, when executed with a context, will perform the retry
// logic with context cancellation support and return the final result.
//
// Type Parameters:
// - A: The type of the success value
//
// Context Cancellation:
//
// The retry mechanism respects context cancellation in two ways:
// 1. During retry delays: If the context is cancelled while waiting between retries,
// the operation stops immediately and returns the context error.
// 2. During action execution: If the action itself checks the context and returns
// an error due to cancellation, the retry loop will stop (assuming the check
// function doesn't force a retry on context errors).
//
// Example:
//
// // Create a retry policy: exponential backoff with a cap, limited to 5 retries
// policy := M.Concat(
// retry.LimitRetries(5),
// retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
// )(retry.Monoid)
//
// // Action that fetches data, with retry status information
// fetchData := func(status retry.RetryStatus) ReaderIOResult[string] {
// return func(ctx context.Context) IOResult[string] {
// return func() Result[string] {
// // Check if context is cancelled
// if ctx.Err() != nil {
// return result.Left[string](ctx.Err())
// }
// // Simulate an HTTP request that might fail
// if status.IterNumber < 3 {
// return result.Left[string](fmt.Errorf("temporary error"))
// }
// return result.Of("success")
// }
// }
// }
//
// // Check function: retry on any error except context cancellation
// shouldRetry := func(r Result[string]) bool {
// return result.IsLeft(r) && !errors.Is(result.GetLeft(r), context.Canceled)
// }
//
// // Create the retrying computation
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a cancellable context
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// ioResult := retryingFetch(ctx)
// finalResult := ioResult()
//
// See also:
// - retry.RetryPolicy for available retry policies
// - retry.RetryStatus for information passed to the action
// - context.Context for context cancellation semantics
//
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(Result[A]) bool,
) ReaderIOResult[A] {
// delayWithCancel implements a context-aware delay mechanism for retry operations.
// It creates a timeout context that will be cancelled when either:
// 1. The delay duration expires (normal case), or
// 2. The parent context is cancelled (early termination)
//
// The function waits on timeoutCtx.Done(), which will be signaled in either case:
// - If the delay expires, timeoutCtx is cancelled by the timeout
// - If the parent ctx is cancelled, timeoutCtx inherits the cancellation
//
// After the wait completes, we dispatch to the next action by calling ri(ctx)().
// This works correctly because the action is wrapped in WithContextK, which handles
// context cancellation by checking ctx.Err() and returning an appropriate error
// (context.Canceled or context.DeadlineExceeded) when the context is cancelled.
//
// This design ensures that:
// - Retry delays respect context cancellation and terminate immediately
// - The cancellation error propagates correctly through the retry chain
// - No unnecessary delays occur when the context is already cancelled
delayWithCancel := func(delay time.Duration) RIO.Operator[R.RetryStatus, R.RetryStatus] {
return func(ri ReaderIO[R.RetryStatus]) ReaderIO[R.RetryStatus] {
return func(ctx context.Context) IO[R.RetryStatus] {
return func() R.RetryStatus {
// Create a timeout context that will be cancelled when either:
// - The delay duration expires, or
// - The parent context is cancelled
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, delay)
defer cancelTimeout()
// Wait for either the timeout or parent context cancellation
<-timeoutCtx.Done()
// Dispatch to the next action with the original context.
// WithContextK will handle context cancellation correctly.
return ri(ctx)()
}
}
}
}
// get an implementation for the types
return RG.Retrying(
RIO.Chain[Result[A], Result[A]],
RIO.Chain[R.RetryStatus, Result[A]],
RIO.Of[Result[A]],
RIO.Of[R.RetryStatus],
delayWithCancel,
policy,
WithContextK(action),
check,
)
}

View File

@@ -0,0 +1,511 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/IBM/fp-go/v2/result"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
// Helper function to create a test retry policy
func testRetryPolicy() R.RetryPolicy {
return R.Monoid.Concat(
R.LimitRetries(5),
R.CapDelay(1*time.Second, R.ExponentialBackoff(10*time.Millisecond)),
)
}
// TestRetrying_SuccessOnFirstAttempt tests that Retrying succeeds immediately
// when the action succeeds on the first attempt.
func TestRetrying_SuccessOnFirstAttempt(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of("success")
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.Equal(t, result.Of("success"), res)
}
// TestRetrying_SuccessAfterRetries tests that Retrying eventually succeeds
// after a few failed attempts.
func TestRetrying_SuccessAfterRetries(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
// Fail on first 3 attempts, succeed on 4th
if status.IterNumber < 3 {
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
return result.Of(fmt.Sprintf("success on attempt %d", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.Equal(t, result.Of("success on attempt 3"), res)
}
// TestRetrying_ExhaustsRetries tests that Retrying stops after the retry limit
// is reached and returns the last error.
func TestRetrying_ExhaustsRetries(t *testing.T) {
policy := R.LimitRetries(3)
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.True(t, result.IsLeft(res))
assert.Equal(t, result.Left[string](fmt.Errorf("attempt 3 failed")), res)
}
// TestRetrying_ActionChecksContextCancellation tests that actions can check
// the context and return early if it's cancelled.
func TestRetrying_ActionChecksContextCancellation(t *testing.T) {
policy := R.LimitRetries(10)
attemptCount := 0
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
attemptCount++
// Check context at the start of the action
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
// Simulate work that might take time
time.Sleep(10 * time.Millisecond)
// Check context again after work
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
// Always fail to trigger retries
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
// Don't retry on context errors
val, err := result.Unwrap(r)
_ = val
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return false
}
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create a context that we'll cancel after a short time
ctx, cancel := context.WithCancel(t.Context())
// Start the retry operation in a goroutine
resultChan := make(chan Result[string], 1)
go func() {
res := retrying(ctx)()
resultChan <- res
}()
// Cancel the context after allowing a couple attempts
time.Sleep(50 * time.Millisecond)
cancel()
// Wait for the result
res := <-resultChan
// Should have stopped due to context cancellation
assert.True(t, result.IsLeft(res))
// Should have stopped early (not all 10 attempts)
assert.Less(t, attemptCount, 10, "Should stop retrying when action detects context cancellation")
// The error should be related to context cancellation or an early attempt
val, err := result.Unwrap(res)
_ = val
assert.Error(t, err)
}
// TestRetrying_ContextCancelledBeforeStart tests that if the context is already
// cancelled before starting, the operation fails immediately.
func TestRetrying_ContextCancelledBeforeStart(t *testing.T) {
policy := testRetryPolicy()
attemptCount := 0
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
attemptCount++
// Check context before doing work
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
// Don't retry on context errors
val, err := result.Unwrap(r)
_ = val
if err != nil && errors.Is(err, context.Canceled) {
return false
}
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create an already-cancelled context
ctx, cancel := context.WithCancel(t.Context())
cancel()
res := retrying(ctx)()
assert.True(t, result.IsLeft(res))
val, err := result.Unwrap(res)
_ = val
assert.True(t, errors.Is(err, context.Canceled))
// Should have attempted at most once
assert.LessOrEqual(t, attemptCount, 1)
}
// TestRetrying_ContextTimeoutInAction tests that actions respect context deadlines.
func TestRetrying_ContextTimeoutInAction(t *testing.T) {
policy := R.LimitRetries(10)
attemptCount := 0
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
attemptCount++
// Check context before doing work
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
// Simulate some work
time.Sleep(50 * time.Millisecond)
// Check context after work
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
// Always fail to trigger retries
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
// Don't retry on context errors
val, err := result.Unwrap(r)
_ = val
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return false
}
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create a context with a short timeout
ctx, cancel := context.WithTimeout(t.Context(), 150*time.Millisecond)
defer cancel()
startTime := time.Now()
res := retrying(ctx)()
elapsed := time.Since(startTime)
assert.True(t, result.IsLeft(res))
// Should have stopped before completing all 10 retries
assert.Less(t, attemptCount, 10, "Should stop retrying when action detects context timeout")
// Should have stopped around the timeout duration
assert.Less(t, elapsed, 500*time.Millisecond, "Should stop soon after timeout")
}
// TestRetrying_CheckFunctionStopsRetry tests that the check function can
// stop retrying even when errors occur.
func TestRetrying_CheckFunctionStopsRetry(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
if status.IterNumber == 0 {
return result.Left[string](fmt.Errorf("retryable error"))
}
return result.Left[string](fmt.Errorf("permanent error"))
}
}
}
// Only retry on "retryable error"
check := func(r Result[string]) bool {
return result.IsLeft(r) && result.Fold(
func(err error) bool { return err.Error() == "retryable error" },
func(string) bool { return false },
)(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.Equal(t, result.Left[string](fmt.Errorf("permanent error")), res)
}
// TestRetrying_ExponentialBackoff tests that exponential backoff is applied.
func TestRetrying_ExponentialBackoff(t *testing.T) {
// Use a policy with measurable delays
policy := R.Monoid.Concat(
R.LimitRetries(3),
R.ExponentialBackoff(50*time.Millisecond),
)
startTime := time.Now()
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
if status.IterNumber < 2 {
return result.Left[string](fmt.Errorf("retry"))
}
return result.Of("success")
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
elapsed := time.Since(startTime)
assert.Equal(t, result.Of("success"), res)
// With exponential backoff starting at 50ms:
// Iteration 0: no delay
// Iteration 1: 50ms delay
// Iteration 2: 100ms delay
// Total should be at least 150ms
assert.GreaterOrEqual(t, elapsed, 150*time.Millisecond)
}
// TestRetrying_ContextValuePropagation tests that context values are properly
// propagated through the retry mechanism.
func TestRetrying_ContextValuePropagation(t *testing.T) {
policy := R.LimitRetries(2)
type contextKey string
const requestIDKey contextKey = "requestID"
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
// Extract value from context
requestID, ok := ctx.Value(requestIDKey).(string)
if !ok {
return result.Left[string](fmt.Errorf("missing request ID"))
}
if status.IterNumber < 1 {
return result.Left[string](fmt.Errorf("retry needed"))
}
return result.Of(fmt.Sprintf("processed request %s", requestID))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create context with a value
ctx := context.WithValue(t.Context(), requestIDKey, "12345")
res := retrying(ctx)()
assert.Equal(t, result.Of("processed request 12345"), res)
}
// TestRetrying_RetryStatusProgression tests that the RetryStatus is properly
// updated on each iteration.
func TestRetrying_RetryStatusProgression(t *testing.T) {
policy := testRetryPolicy()
var iterations []uint
action := func(status R.RetryStatus) ReaderIOResult[int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
iterations = append(iterations, status.IterNumber)
if status.IterNumber < 3 {
return result.Left[int](fmt.Errorf("retry"))
}
return result.Of(int(status.IterNumber))
}
}
}
check := func(r Result[int]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.Equal(t, result.Of(3), res)
// Should have attempted iterations 0, 1, 2, 3
assert.Equal(t, []uint{0, 1, 2, 3}, iterations)
}
// TestRetrying_ContextCancelledDuringDelay tests that the retry operation
// stops immediately when the context is cancelled during a retry delay,
// even if there are still retries remaining according to the policy.
func TestRetrying_ContextCancelledDuringDelay(t *testing.T) {
// Use a policy with significant delays to ensure we can cancel during the delay
policy := R.Monoid.Concat(
R.LimitRetries(10),
R.ConstantDelay(200*time.Millisecond),
)
attemptCount := 0
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
attemptCount++
// Always fail to trigger retries
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
// Always retry on errors (don't check for context cancellation in check function)
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create a context that we'll cancel during the retry delay
ctx, cancel := context.WithCancel(t.Context())
// Start the retry operation in a goroutine
resultChan := make(chan Result[string], 1)
startTime := time.Now()
go func() {
res := retrying(ctx)()
resultChan <- res
}()
// Wait for the first attempt to complete and the delay to start
time.Sleep(50 * time.Millisecond)
// Cancel the context during the retry delay
cancel()
// Wait for the result
res := <-resultChan
elapsed := time.Since(startTime)
// Should have stopped due to context cancellation
assert.True(t, result.IsLeft(res))
// Should have attempted only once or twice (not all 10 attempts)
// because the context was cancelled during the delay
assert.LessOrEqual(t, attemptCount, 2, "Should stop retrying when context is cancelled during delay")
// Should have stopped quickly after cancellation, not waiting for all delays
// With 10 retries and 200ms delays, it would take ~2 seconds without cancellation
// With cancellation during first delay, it should complete in well under 500ms
assert.Less(t, elapsed, 500*time.Millisecond, "Should stop immediately when context is cancelled during delay")
// When context is cancelled during the delay, the retry mechanism
// detects the cancellation and returns a context error
val, err := result.Unwrap(res)
_ = val
assert.Error(t, err)
// The error should be a context cancellation error since cancellation
// happened during the delay between retries
assert.True(t, errors.Is(err, context.Canceled), "Should return context.Canceled when cancelled during delay")
}

View File

@@ -26,6 +26,8 @@ import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
@@ -33,6 +35,7 @@ import (
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/tailrec"
)
type (
@@ -132,4 +135,9 @@ type (
Endomorphism[A any] = endomorphism.Endomorphism[A]
Consumer[A any] = consumer.Consumer[A]
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
Trampoline[B, L any] = tailrec.Trampoline[B, L]
)

View File

@@ -17,7 +17,6 @@ package readerresult
import (
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
G "github.com/IBM/fp-go/v2/readereither/generic"
)
@@ -39,10 +38,18 @@ func Do[S any](
return G.Do[ReaderResult[S]](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// Bind attaches the result of an EFFECTFUL computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access the context.Context from the environment.
//
// IMPORTANT: Bind is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The function parameter takes state and returns a ReaderResult[T], which is effectful because
// it depends on context.Context (can be cancelled, has deadlines, carries values).
//
// For PURE FUNCTIONS (side-effect free), use:
// - BindResultK: For pure functions with errors (State -> (Value, error))
// - Let: For pure functions without errors (State -> Value)
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
@@ -89,7 +96,16 @@ func Bind[S1, S2, T any](
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, F.Flow2(f, WithContext))
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
// Let attaches the result of a PURE computation to a context [S1] to produce a context [S2].
//
// IMPORTANT: Let is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
// The function parameter takes state and returns a value directly, with no errors or effects.
//
// For EFFECTFUL FUNCTIONS (that need context.Context), use:
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
//
// For PURE FUNCTIONS with error handling, use:
// - BindResultK: For pure functions with errors (State -> (Value, error))
//
//go:inline
func Let[S1, S2, T any](
@@ -99,7 +115,8 @@ func Let[S1, S2, T any](
return G.Let[ReaderResult[S1], ReaderResult[S2]](setter, f)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
// This is a PURE operation (side-effect free) that simply sets a field to a constant value.
//
//go:inline
func LetTo[S1, S2, T any](
@@ -114,13 +131,23 @@ func LetTo[S1, S2, T any](
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Kleisli[ReaderResult[T], S1] {
) Operator[T, S1] {
return G.BindTo[ReaderResult[S1], ReaderResult[T]](setter)
}
//go:inline
func BindToP[S1, T any](
setter Prism[S1, T],
) Operator[T, S1] {
return BindTo(setter.ReverseGet)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other.
// This allows independent EFFECTFUL computations to be combined without one depending on the result of the other.
//
// IMPORTANT: ApS is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The ReaderResult parameter is effectful because it depends on context.Context.
//
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
// and can conceptually run in parallel.
@@ -198,16 +225,21 @@ func ApS[S1, S2, T any](
//
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa ReaderResult[T],
) Kleisli[ReaderResult[S], S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
// It combines the lens-based field access with monadic composition, allowing you to:
// It combines the lens-based field access with monadic composition for EFFECTFUL computations.
//
// IMPORTANT: BindL is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The function parameter returns a ReaderResult, which is effectful.
//
// It allows you to:
// 1. Extract a field value using the lens
// 2. Use that value in a computation that may fail
// 2. Use that value in an effectful computation that may fail
// 3. Update the field with the result
//
// Parameters:
@@ -244,14 +276,17 @@ func ApSL[S, T any](
//
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f Kleisli[T, T],
) Kleisli[ReaderResult[S], S] {
return Bind(lens.Set, F.Flow2(lens.Get, F.Flow2(f, WithContext)))
}
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
// It applies a pure transformation to the focused field without any effects.
// It applies a PURE transformation to the focused field without any effects.
//
// IMPORTANT: LetL is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
// The function parameter is a pure endomorphism (T -> T) with no errors or effects.
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
@@ -281,14 +316,14 @@ func BindL[S, T any](
//
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f Endomorphism[T],
) Kleisli[ReaderResult[S], S] {
return Let(lens.Set, F.Flow2(lens.Get, f))
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
// It sets the focused field to a constant value.
// It sets the focused field to a constant value. This is a PURE operation (side-effect free).
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
@@ -317,7 +352,7 @@ func LetL[S, T any](
//
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
b T,
) Kleisli[ReaderResult[S], S] {
return LetTo(lens.Set, b)

View File

@@ -19,6 +19,7 @@ import (
"context"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
)
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
@@ -30,3 +31,11 @@ func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
return ma(ctx)
}
}
//go:inline
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
return F.Flow2(
f,
WithContext,
)
}

View File

@@ -25,8 +25,8 @@ import (
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
//
// TailRec takes a Kleisli function that returns Either[A, B] and converts it into a stack-safe,
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Right value.
// TailRec takes a Kleisli function that returns Trampoline[A, B] and converts it into a stack-safe,
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Land value.
//
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns a
@@ -37,9 +37,9 @@ import (
// - B: The final result type
//
// Parameters:
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Either[A, B].
// When the result is Left[B](a), recursion continues with the new value 'a'.
// When the result is Right[A](b), recursion terminates with the final value 'b'.
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Trampoline[A, B].
// When the result is Bounce(a), recursion continues with the new value 'a'.
// When the result is Land(b), recursion terminates with the final value 'b'.
//
// Returns:
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
@@ -48,8 +48,8 @@ import (
// - On each iteration, checks if the context has been canceled (short circuit)
// - If canceled, returns result.Left[B](context.Cause(ctx))
// - If the step returns Left[B](error), propagates the error
// - If the step returns Right[A](Left[B](a)), continues recursion with new value 'a'
// - If the step returns Right[A](Right[A](b)), terminates with success value 'b'
// - If the step returns Right[A](Bounce(a)), continues recursion with new value 'a'
// - If the step returns Right[A](Land(b)), terminates with success value 'b'
//
// Example - Factorial computation with context:
//
@@ -58,12 +58,12 @@ import (
// acc int
// }
//
// factorialStep := func(state State) ReaderResult[either.Either[State, int]] {
// return func(ctx context.Context) result.Result[either.Either[State, int]] {
// factorialStep := func(state State) ReaderResult[tailrec.Trampoline[State, int]] {
// return func(ctx context.Context) result.Result[tailrec.Trampoline[State, int]] {
// if state.n <= 0 {
// return result.Of(either.Right[State](state.acc))
// return result.Of(tailrec.Land[State](state.acc))
// }
// return result.Of(either.Left[int](State{state.n - 1, state.acc * state.n}))
// return result.Of(tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}))
// }
// }
//
@@ -80,10 +80,10 @@ import (
// // Returns result.Left[B](context.Cause(ctx)) without executing any steps
//
//go:inline
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
return func(a A) ReaderResult[B] {
initialReader := f(a)
return func(ctx context.Context) Result[B] {
return func(ctx context.Context) result.Result[B] {
rdr := initialReader
for {
// short circuit
@@ -95,11 +95,10 @@ func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
if either.IsLeft(current) {
return result.Left[B](e)
}
b, a := either.Unwrap(rec)
if either.IsRight(rec) {
return result.Of(b)
if rec.Landed {
return result.Of(rec.Land)
}
rdr = f(a)
rdr = f(rec.Bounce)
}
}
}

View File

@@ -23,8 +23,8 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
R "github.com/IBM/fp-go/v2/result"
TR "github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
@@ -35,12 +35,12 @@ func TestTailRecFactorial(t *testing.T) {
acc int
}
factorialStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
factorialStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
if state.n <= 0 {
return R.Of(E.Right[State](state.acc))
return R.Of(TR.Land[State](state.acc))
}
return R.Of(E.Left[int](State{state.n - 1, state.acc * state.n}))
return R.Of(TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
}
}
@@ -58,12 +58,12 @@ func TestTailRecFibonacci(t *testing.T) {
curr int
}
fibStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
fibStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
if state.n <= 0 {
return R.Of(E.Right[State](state.curr))
return R.Of(TR.Land[State](state.curr))
}
return R.Of(E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
return R.Of(TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
}
}
@@ -75,12 +75,12 @@ func TestTailRecFibonacci(t *testing.T) {
// TestTailRecCountdown tests countdown computation
func TestTailRecCountdown(t *testing.T) {
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -92,9 +92,9 @@ func TestTailRecCountdown(t *testing.T) {
// TestTailRecImmediateTermination tests immediate termination (Right on first call)
func TestTailRecImmediateTermination(t *testing.T) {
immediateStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
return R.Of(E.Right[int](n * 2))
immediateStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
return R.Of(TR.Land[int](n * 2))
}
}
@@ -106,12 +106,12 @@ func TestTailRecImmediateTermination(t *testing.T) {
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
func TestTailRecStackSafety(t *testing.T) {
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -128,12 +128,12 @@ func TestTailRecSumList(t *testing.T) {
sum int
}
sumStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
sumStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
if A.IsEmpty(state.list) {
return R.Of(E.Right[State](state.sum))
return R.Of(TR.Land[State](state.sum))
}
return R.Of(E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
return R.Of(TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
}
}
@@ -145,15 +145,15 @@ func TestTailRecSumList(t *testing.T) {
// TestTailRecCollatzConjecture tests the Collatz conjecture
func TestTailRecCollatzConjecture(t *testing.T) {
collatzStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
collatzStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
if n <= 1 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
if n%2 == 0 {
return R.Of(E.Left[int](n / 2))
return R.Of(TR.Bounce[int](n / 2))
}
return R.Of(E.Left[int](3*n + 1))
return R.Of(TR.Bounce[int](3*n + 1))
}
}
@@ -170,12 +170,12 @@ func TestTailRecGCD(t *testing.T) {
b int
}
gcdStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
gcdStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
if state.b == 0 {
return R.Of(E.Right[State](state.a))
return R.Of(TR.Land[State](state.a))
}
return R.Of(E.Left[int](State{state.b, state.a % state.b}))
return R.Of(TR.Bounce[int](State{state.b, state.a % state.b}))
}
}
@@ -189,15 +189,15 @@ func TestTailRecGCD(t *testing.T) {
func TestTailRecErrorPropagation(t *testing.T) {
expectedErr := errors.New("computation error")
errorStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
errorStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
if n == 5 {
return R.Left[E.Either[int, int]](expectedErr)
return R.Left[TR.Trampoline[int, int]](expectedErr)
}
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -215,13 +215,13 @@ func TestTailRecContextCancellationImmediate(t *testing.T) {
cancel() // Cancel immediately before execution
stepExecuted := false
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
stepExecuted = true
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -240,17 +240,17 @@ func TestTailRecContextCancellationDuringExecution(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
executionCount++
// Cancel after 3 iterations
if executionCount == 3 {
cancel()
}
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -270,15 +270,15 @@ func TestTailRecContextWithTimeout(t *testing.T) {
defer cancel()
executionCount := 0
slowStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
slowStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
executionCount++
// Simulate slow computation
time.Sleep(20 * time.Millisecond)
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -298,12 +298,12 @@ func TestTailRecContextWithCause(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(customErr)
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -322,16 +322,16 @@ func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
executionCount := 0
maxExecutions := 5
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
executionCount++
if executionCount == maxExecutions {
cancel()
}
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -351,13 +351,13 @@ func TestTailRecContextNotCanceled(t *testing.T) {
ctx := context.Background()
executionCount := 0
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
executionCount++
if n <= 0 {
return R.Of(E.Right[int](n))
return R.Of(TR.Land[int](n))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -376,12 +376,12 @@ func TestTailRecPowerOfTwo(t *testing.T) {
target int
}
powerStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
powerStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
if state.exponent >= state.target {
return R.Of(E.Right[State](state.result))
return R.Of(TR.Land[State](state.result))
}
return R.Of(E.Left[int](State{state.exponent + 1, state.result * 2, state.target}))
return R.Of(TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}))
}
}
@@ -399,15 +399,15 @@ func TestTailRecFindInRange(t *testing.T) {
target int
}
findStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
if state.current >= state.max {
return R.Of(E.Right[State](-1)) // Not found
return R.Of(TR.Land[State](-1)) // Not found
}
if state.current == state.target {
return R.Of(E.Right[State](state.current)) // Found
return R.Of(TR.Land[State](state.current)) // Found
}
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
return R.Of(TR.Bounce[int](State{state.current + 1, state.max, state.target}))
}
}
@@ -425,15 +425,15 @@ func TestTailRecFindNotInRange(t *testing.T) {
target int
}
findStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
if state.current >= state.max {
return R.Of(E.Right[State](-1)) // Not found
return R.Of(TR.Land[State](-1)) // Not found
}
if state.current == state.target {
return R.Of(E.Right[State](state.current)) // Found
return R.Of(TR.Land[State](state.current)) // Found
}
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
return R.Of(TR.Bounce[int](State{state.current + 1, state.max, state.target}))
}
}
@@ -450,13 +450,13 @@ func TestTailRecWithContextValue(t *testing.T) {
ctx := context.WithValue(context.Background(), multiplierKey, 3)
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
if n <= 0 {
multiplier := ctx.Value(multiplierKey).(int)
return R.Of(E.Right[int](n * multiplier))
return R.Of(TR.Land[int](n * multiplier))
}
return R.Of(E.Left[int](n - 1))
return R.Of(TR.Bounce[int](n - 1))
}
}
@@ -475,11 +475,11 @@ func TestTailRecComplexState(t *testing.T) {
completed bool
}
complexStep := func(state ComplexState) ReaderResult[E.Either[ComplexState, string]] {
return func(ctx context.Context) Result[E.Either[ComplexState, string]] {
complexStep := func(state ComplexState) ReaderResult[TR.Trampoline[ComplexState, string]] {
return func(ctx context.Context) Result[TR.Trampoline[ComplexState, string]] {
if state.counter <= 0 || state.completed {
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
return R.Of(E.Right[ComplexState](result))
return R.Of(TR.Land[ComplexState](result))
}
newState := ComplexState{
counter: state.counter - 1,
@@ -487,7 +487,7 @@ func TestTailRecComplexState(t *testing.T) {
product: state.product * state.counter,
completed: state.counter == 1,
}
return R.Of(E.Left[string](newState))
return R.Of(TR.Bounce[string](newState))
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache LicensVersion 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"time"
RD "github.com/IBM/fp-go/v2/reader"
R "github.com/IBM/fp-go/v2/retry"
RG "github.com/IBM/fp-go/v2/retry/generic"
)
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(Result[A]) bool,
) ReaderResult[A] {
// delayWithCancel implements a context-aware delay mechanism for retry operations.
// It creates a timeout context that will be cancelled when either:
// 1. The delay duration expires (normal case), or
// 2. The parent context is cancelled (early termination)
//
// The function waits on timeoutCtx.Done(), which will be signaled in either case:
// - If the delay expires, timeoutCtx is cancelled by the timeout
// - If the parent ctx is cancelled, timeoutCtx inherits the cancellation
//
// After the wait completes, we dispatch to the next action by calling ri(ctx)().
// This works correctly because the action is wrapped in WithContextK, which handles
// context cancellation by checking ctx.Err() and returning an appropriate error
// (context.Canceled or context.DeadlineExceeded) when the context is cancelled.
//
// This design ensures that:
// - Retry delays respect context cancellation and terminate immediately
// - The cancellation error propagates correctly through the retry chain
// - No unnecessary delays occur when the context is already cancelled
delayWithCancel := func(delay time.Duration) RD.Operator[context.Context, R.RetryStatus, R.RetryStatus] {
return func(ri Reader[context.Context, R.RetryStatus]) Reader[context.Context, R.RetryStatus] {
return func(ctx context.Context) R.RetryStatus {
// Create a timeout context that will be cancelled when either:
// - The delay duration expires, or
// - The parent context is cancelled
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, delay)
defer cancelTimeout()
// Wait for either the timeout or parent context cancellation
<-timeoutCtx.Done()
// Dispatch to the next action with the original context.
// WithContextK will handle context cancellation correctly.
return ri(ctx)
}
}
}
// get an implementation for the types
return RG.Retrying(
RD.Chain[context.Context, Result[A], Result[A]],
RD.Chain[context.Context, R.RetryStatus, Result[A]],
RD.Of[context.Context, Result[A]],
RD.Of[context.Context, R.RetryStatus],
delayWithCancel,
policy,
WithContextK(action),
check,
)
}

View File

@@ -13,7 +13,31 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
// Package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error.
//
// # Pure vs Effectful Functions
//
// This package distinguishes between pure (side-effect free) and effectful (side-effectful) functions:
//
// EFFECTFUL FUNCTIONS (depend on context.Context):
// - ReaderResult[A]: func(context.Context) (A, error) - Effectful computation that needs context
// - These functions are effectful because context.Context is effectful (can be cancelled, has deadlines, carries values)
// - Use for: operations that need cancellation, timeouts, context values, or any context-dependent behavior
// - Examples: database queries, HTTP requests, operations that respect cancellation
//
// PURE FUNCTIONS (side-effect free):
// - func(State) (Value, error) - Pure computation that only depends on state, not context
// - func(State) Value - Pure transformation without errors
// - These functions are pure because they only read from their input state and don't depend on external context
// - Use for: parsing, validation, calculations, data transformations that don't need context
// - Examples: JSON parsing, input validation, mathematical computations
//
// The package provides different bind operations for each:
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
// - BindResultK: For pure functions with errors (State -> (Value, error))
// - Let: For pure functions without errors (State -> Value)
// - BindReaderK: For context-dependent pure functions (State -> Reader[Context, Value])
// - BindEitherK: For pure Result/Either values (State -> Result[Value])
package readerresult
import (
@@ -21,10 +45,13 @@ import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/tailrec"
)
type (
@@ -35,7 +62,10 @@ type (
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
Operator[A, B any] = Kleisli[ReaderResult[A], B]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
Operator[A, B any] = Kleisli[ReaderResult[A], B]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
Trampoline[A, B any] = tailrec.Trampoline[A, B]
)

View File

@@ -23,14 +23,15 @@ import (
IOR "github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
R "github.com/IBM/fp-go/v2/record"
T "github.com/IBM/fp-go/v2/tuple"
"sync"
)
func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] {
return T.MakeTuple2(p.Provides().Id(), p.Factory())
func providerToEntry(p Provider) Entry[string, ProviderFactory] {
return pair.MakePair(p.Provides().Id(), p.Factory())
}
func itemProviderToMap(p Provider) map[string][]ProviderFactory {

View File

@@ -4,10 +4,12 @@ import (
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/record"
)
type (
Option[T any] = option.Option[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Option[T any] = option.Option[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Entry[K comparable, V any] = record.Entry[K, V]
)

View File

@@ -4,12 +4,14 @@ import (
"github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/record"
"github.com/IBM/fp-go/v2/result"
)
type (
Option[T any] = option.Option[T]
Result[T any] = result.Result[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Option[T any] = option.Option[T]
Result[T any] = result.Result[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Entry[K comparable, V any] = record.Entry[K, V]
)

View File

@@ -15,10 +15,6 @@
package either
import (
"fmt"
)
type (
// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases
Either[E, A any] struct {
@@ -28,28 +24,6 @@ type (
}
)
// String prints some debug info for the object
//
//go:noinline
func (s Either[E, A]) String() string {
if !s.isLeft {
return fmt.Sprintf("Right[%T](%v)", s.r, s.r)
}
return fmt.Sprintf("Left[%T](%v)", s.l, s.l)
}
// Format prints some debug info for the object
//
//go:noinline
func (s Either[E, A]) Format(f fmt.State, c rune) {
switch c {
case 's':
fmt.Fprint(f, s.String())
default:
fmt.Fprint(f, s.String())
}
}
// IsLeft tests if the Either is a Left value.
// Rather use [Fold] or [MonadFold] if you need to access the values.
// Inverse is [IsRight].

View File

@@ -0,0 +1,149 @@
// 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 either_test
import (
"errors"
"fmt"
"log/slog"
"os"
E "github.com/IBM/fp-go/v2/either"
)
// ExampleEither_String demonstrates the fmt.Stringer interface implementation.
func ExampleEither_String() {
right := E.Right[error](42)
left := E.Left[int](errors.New("something went wrong"))
fmt.Println(right.String())
fmt.Println(left.String())
// Output:
// Right[int](42)
// Left[*errors.errorString](something went wrong)
}
// ExampleEither_GoString demonstrates the fmt.GoStringer interface implementation.
func ExampleEither_GoString() {
right := E.Right[error](42)
left := E.Left[int](errors.New("error"))
fmt.Printf("%#v\n", right)
fmt.Printf("%#v\n", left)
// Output:
// either.Right[error](42)
// either.Left[int](&errors.errorString{s:"error"})
}
// ExampleEither_Format demonstrates the fmt.Formatter interface implementation.
func ExampleEither_Format() {
result := E.Right[error](42)
// Different format verbs
fmt.Printf("%%s: %s\n", result)
fmt.Printf("%%v: %v\n", result)
fmt.Printf("%%+v: %+v\n", result)
fmt.Printf("%%#v: %#v\n", result)
// Output:
// %s: Right[int](42)
// %v: Right[int](42)
// %+v: Right[int](42)
// %#v: either.Right[error](42)
}
// ExampleEither_LogValue demonstrates the slog.LogValuer interface implementation.
func ExampleEither_LogValue() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Remove time for consistent output
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
// Right value
rightResult := E.Right[error](42)
logger.Info("computation succeeded", "result", rightResult)
// Left value
leftResult := E.Left[int](errors.New("computation failed"))
logger.Error("computation failed", "result", leftResult)
// Output:
// level=INFO msg="computation succeeded" result.right=42
// level=ERROR msg="computation failed" result.left="computation failed"
}
// ExampleEither_formatting_comparison demonstrates different formatting options.
func ExampleEither_formatting_comparison() {
type User struct {
ID int
Name string
}
user := User{ID: 123, Name: "Alice"}
result := E.Right[error](user)
fmt.Printf("String(): %s\n", result.String())
fmt.Printf("GoString(): %s\n", result.GoString())
fmt.Printf("%%v: %v\n", result)
fmt.Printf("%%#v: %#v\n", result)
// Output:
// String(): Right[either_test.User]({123 Alice})
// GoString(): either.Right[error](either_test.User{ID:123, Name:"Alice"})
// %v: Right[either_test.User]({123 Alice})
// %#v: either.Right[error](either_test.User{ID:123, Name:"Alice"})
}
// ExampleEither_LogValue_structured demonstrates structured logging with Either.
func ExampleEither_LogValue_structured() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
// Simulate a computation pipeline
compute := func(x int) E.Either[error, int] {
if x < 0 {
return E.Left[int](errors.New("negative input"))
}
return E.Right[error](x * 2)
}
// Log successful computation
result1 := compute(21)
logger.Info("computation", "input", 21, "output", result1)
// Log failed computation
result2 := compute(-5)
logger.Error("computation", "input", -5, "output", result2)
// Output:
// level=INFO msg=computation input=21 output.right=42
// level=ERROR msg=computation input=-5 output.left="negative input"
}

103
v2/either/format.go Normal file
View File

@@ -0,0 +1,103 @@
// 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 either
import (
"fmt"
"log/slog"
"github.com/IBM/fp-go/v2/internal/formatting"
)
const (
leftGoTemplate = "either.Left[%s](%#v)"
rightGoTemplate = "either.Right[%s](%#v)"
leftFmtTemplate = "Left[%T](%v)"
rightFmtTemplate = "Right[%T](%v)"
)
func goString(template string, other, v any) string {
return fmt.Sprintf(template, formatting.TypeInfo(other), v)
}
// String prints some debug info for the object
//
//go:noinline
func (s Either[E, A]) String() string {
if !s.isLeft {
return fmt.Sprintf(rightFmtTemplate, s.r, s.r)
}
return fmt.Sprintf(leftFmtTemplate, s.l, s.l)
}
// Format implements fmt.Formatter for Either.
// Supports all standard format verbs:
// - %s, %v, %+v: uses String() representation
// - %#v: uses GoString() representation
// - %q: quoted String() representation
// - other verbs: uses String() representation
//
// Example:
//
// e := either.Right[error](42)
// fmt.Printf("%s", e) // "Right[int](42)"
// fmt.Printf("%v", e) // "Right[int](42)"
// fmt.Printf("%#v", e) // "either.Right[error](42)"
//
//go:noinline
func (s Either[E, A]) Format(f fmt.State, c rune) {
formatting.FmtString(s, f, c)
}
// GoString implements fmt.GoStringer for Either.
// Returns a Go-syntax representation of the Either value.
//
// Example:
//
// either.Right[error](42).GoString() // "either.Right[error](42)"
// either.Left[int](errors.New("fail")).GoString() // "either.Left[int](error)"
//
//go:noinline
func (s Either[E, A]) GoString() string {
if !s.isLeft {
return goString(rightGoTemplate, new(E), s.r)
}
return goString(leftGoTemplate, new(A), s.l)
}
// LogValue implements slog.LogValuer for Either.
// Returns a slog.Value that represents the Either for structured logging.
// Returns a group value with "right" key for Right values and "left" key for Left values.
//
// Example:
//
// logger := slog.Default()
// result := either.Right[error](42)
// logger.Info("result", "value", result)
// // Logs: {"msg":"result","value":{"right":42}}
//
// err := either.Left[int](errors.New("failed"))
// logger.Error("error", "value", err)
// // Logs: {"msg":"error","value":{"left":"failed"}}
//
//go:noinline
func (s Either[E, A]) LogValue() slog.Value {
if !s.isLeft {
return slog.GroupValue(slog.Any("right", s.r))
}
return slog.GroupValue(slog.Any("left", s.l))
}

311
v2/either/format_test.go Normal file
View File

@@ -0,0 +1,311 @@
// 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 either
import (
"bytes"
"errors"
"fmt"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
)
func TestString(t *testing.T) {
t.Run("Right value", func(t *testing.T) {
e := Right[error](42)
result := e.String()
assert.Equal(t, "Right[int](42)", result)
})
t.Run("Left value", func(t *testing.T) {
e := Left[int](errors.New("test error"))
result := e.String()
assert.Contains(t, result, "Left[*errors.errorString]")
assert.Contains(t, result, "test error")
})
t.Run("Right with string", func(t *testing.T) {
e := Right[error]("hello")
result := e.String()
assert.Equal(t, "Right[string](hello)", result)
})
t.Run("Left with string", func(t *testing.T) {
e := Left[int]("error message")
result := e.String()
assert.Equal(t, "Left[string](error message)", result)
})
}
func TestGoString(t *testing.T) {
t.Run("Right value", func(t *testing.T) {
e := Right[error](42)
result := e.GoString()
assert.Contains(t, result, "either.Right")
assert.Contains(t, result, "42")
})
t.Run("Left value", func(t *testing.T) {
e := Left[int](errors.New("test error"))
result := e.GoString()
assert.Contains(t, result, "either.Left")
assert.Contains(t, result, "test error")
})
t.Run("Right with struct", func(t *testing.T) {
type TestStruct struct {
Name string
Age int
}
e := Right[error](TestStruct{Name: "Alice", Age: 30})
result := e.GoString()
assert.Contains(t, result, "either.Right")
assert.Contains(t, result, "Alice")
assert.Contains(t, result, "30")
})
t.Run("Left with custom error", func(t *testing.T) {
e := Left[string]("custom error")
result := e.GoString()
assert.Contains(t, result, "either.Left")
assert.Contains(t, result, "custom error")
})
}
func TestFormatInterface(t *testing.T) {
t.Run("Right value with %s", func(t *testing.T) {
e := Right[error](42)
result := fmt.Sprintf("%s", e)
assert.Equal(t, "Right[int](42)", result)
})
t.Run("Left value with %s", func(t *testing.T) {
e := Left[int](errors.New("test error"))
result := fmt.Sprintf("%s", e)
assert.Contains(t, result, "Left")
assert.Contains(t, result, "test error")
})
t.Run("Right value with %v", func(t *testing.T) {
e := Right[error](42)
result := fmt.Sprintf("%v", e)
assert.Equal(t, "Right[int](42)", result)
})
t.Run("Left value with %v", func(t *testing.T) {
e := Left[int]("error")
result := fmt.Sprintf("%v", e)
assert.Equal(t, "Left[string](error)", result)
})
t.Run("Right value with %+v", func(t *testing.T) {
e := Right[error](42)
result := fmt.Sprintf("%+v", e)
assert.Contains(t, result, "Right")
assert.Contains(t, result, "42")
})
t.Run("Right value with %#v (GoString)", func(t *testing.T) {
e := Right[error](42)
result := fmt.Sprintf("%#v", e)
assert.Contains(t, result, "either.Right")
assert.Contains(t, result, "42")
})
t.Run("Left value with %#v (GoString)", func(t *testing.T) {
e := Left[int]("error")
result := fmt.Sprintf("%#v", e)
assert.Contains(t, result, "either.Left")
assert.Contains(t, result, "error")
})
t.Run("Right value with %q", func(t *testing.T) {
e := Right[error]("hello")
result := fmt.Sprintf("%q", e)
// Should use String() representation
assert.Contains(t, result, "Right")
})
t.Run("Right value with %T", func(t *testing.T) {
e := Right[error](42)
result := fmt.Sprintf("%T", e)
assert.Contains(t, result, "either.Either")
})
}
func TestLogValue(t *testing.T) {
t.Run("Right value", func(t *testing.T) {
e := Right[error](42)
logValue := e.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 1)
assert.Equal(t, "right", attrs[0].Key)
assert.Equal(t, int64(42), attrs[0].Value.Any())
})
t.Run("Left value", func(t *testing.T) {
e := Left[int](errors.New("test error"))
logValue := e.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 1)
assert.Equal(t, "left", attrs[0].Key)
assert.NotNil(t, attrs[0].Value.Any())
})
t.Run("Right with string", func(t *testing.T) {
e := Right[error]("success")
logValue := e.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 1)
assert.Equal(t, "right", attrs[0].Key)
assert.Equal(t, "success", attrs[0].Value.Any())
})
t.Run("Left with string", func(t *testing.T) {
e := Left[int]("error message")
logValue := e.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 1)
assert.Equal(t, "left", attrs[0].Key)
assert.Equal(t, "error message", attrs[0].Value.Any())
})
t.Run("Integration with slog - Right", func(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
e := Right[error](42)
logger.Info("test message", "result", e)
output := buf.String()
assert.Contains(t, output, "test message")
assert.Contains(t, output, "result")
assert.Contains(t, output, "right")
assert.Contains(t, output, "42")
})
t.Run("Integration with slog - Left", func(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
e := Left[int]("error occurred")
logger.Info("test message", "result", e)
output := buf.String()
assert.Contains(t, output, "test message")
assert.Contains(t, output, "result")
assert.Contains(t, output, "left")
assert.Contains(t, output, "error occurred")
})
}
func TestFormatComprehensive(t *testing.T) {
t.Run("All format verbs for Right", func(t *testing.T) {
e := Right[error](42)
tests := []struct {
verb string
contains []string
}{
{"%s", []string{"Right", "42"}},
{"%v", []string{"Right", "42"}},
{"%+v", []string{"Right", "42"}},
{"%#v", []string{"either.Right", "42"}},
{"%T", []string{"either.Either"}},
}
for _, tt := range tests {
t.Run(tt.verb, func(t *testing.T) {
result := fmt.Sprintf(tt.verb, e)
for _, substr := range tt.contains {
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
}
})
}
})
t.Run("All format verbs for Left", func(t *testing.T) {
e := Left[int]("error")
tests := []struct {
verb string
contains []string
}{
{"%s", []string{"Left", "error"}},
{"%v", []string{"Left", "error"}},
{"%+v", []string{"Left", "error"}},
{"%#v", []string{"either.Left", "error"}},
{"%T", []string{"either.Either"}},
}
for _, tt := range tests {
t.Run(tt.verb, func(t *testing.T) {
result := fmt.Sprintf(tt.verb, e)
for _, substr := range tt.contains {
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
}
})
}
})
}
func TestInterfaceImplementations(t *testing.T) {
t.Run("fmt.Stringer interface", func(t *testing.T) {
var _ fmt.Stringer = Right[error](42)
var _ fmt.Stringer = Left[int](errors.New("error"))
})
t.Run("fmt.GoStringer interface", func(t *testing.T) {
var _ fmt.GoStringer = Right[error](42)
var _ fmt.GoStringer = Left[int](errors.New("error"))
})
t.Run("fmt.Formatter interface", func(t *testing.T) {
var _ fmt.Formatter = Right[error](42)
var _ fmt.Formatter = Left[int](errors.New("error"))
})
t.Run("slog.LogValuer interface", func(t *testing.T) {
var _ slog.LogValuer = Right[error](42)
var _ slog.LogValuer = Left[int](errors.New("error"))
})
}

View File

@@ -15,8 +15,12 @@
package either
import (
"github.com/IBM/fp-go/v2/tailrec"
)
//go:inline
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
return func(a A) Either[E, B] {
current := f(a)
for {
@@ -24,11 +28,10 @@ func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
if IsLeft(current) {
return Left[B](e)
}
b, a := Unwrap(rec)
if IsRight(rec) {
return Right[E](b)
if rec.Landed {
return Right[E](rec.Land)
}
current = f(a)
current = f(rec.Bounce)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,6 @@ import (
"github.com/IBM/fp-go/v2/idiomatic/result"
AP "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
)
@@ -49,7 +48,15 @@ func Do[S any](
return RR.Do[context.Context](empty)
}
// Bind sequences a ReaderResult computation and updates the state with its result.
// Bind sequences an EFFECTFUL ReaderResult computation and updates the state with its result.
//
// IMPORTANT: Bind is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The Kleisli parameter (State -> ReaderResult[T]) is effectful because ReaderResult
// depends on context.Context (can be cancelled, has deadlines, carries values).
//
// For PURE FUNCTIONS (side-effect free), use:
// - BindResultK: For pure functions with errors (State -> (Value, error))
// - Let: For pure functions without errors (State -> Value)
//
// This is the core operation for do-notation, allowing you to chain computations
// where each step can depend on the accumulated state and update it with new values.
@@ -61,7 +68,7 @@ func Do[S any](
//
// Parameters:
// - setter: A function that takes the computation result and returns a state updater
// - f: A Kleisli arrow that produces the next computation based on current state
// - f: A Kleisli arrow that produces the next effectful computation based on current state
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
@@ -79,7 +86,16 @@ func Bind[S1, S2, T any](
)
}
// Let attaches the result of a pure computation to a state.
// Let attaches the result of a PURE computation to a state.
//
// IMPORTANT: Let is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
// The function parameter (State -> Value) is pure - it only reads from state with no effects.
//
// For EFFECTFUL FUNCTIONS (that need context.Context), use:
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
//
// For PURE FUNCTIONS with error handling, use:
// - BindResultK: For pure functions with errors (State -> (Value, error))
//
// Unlike Bind, Let works with pure functions (not ReaderResult computations).
// This is useful for deriving values from the current state without performing
@@ -106,6 +122,7 @@ func Let[S1, S2, T any](
}
// LetTo attaches a constant value to a state.
// This is a PURE operation (side-effect free).
//
// This is a simplified version of Let for when you want to add a constant
// value to the state without computing it.
@@ -152,6 +169,48 @@ func BindTo[S1, T any](
return RR.BindTo[context.Context](setter)
}
// BindToP initializes do-notation by binding a value to a state using a Prism.
//
// This is a variant of BindTo that uses a prism instead of a setter function.
// Prisms are useful for working with sum types and optional values.
//
// Type Parameters:
// - S1: The state type to create
// - T: The type of the initial value
//
// Parameters:
// - setter: A prism that can construct the state from a value
//
// Returns:
// - An Operator that transforms ReaderResult[T] to ReaderResult[S1]
//
//go:inline
func BindToP[S1, T any](
setter Prism[S1, T],
) Operator[T, S1] {
return BindTo(setter.ReverseGet)
}
// ApS attaches a value to a context using applicative style.
//
// IMPORTANT: ApS is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The ReaderResult parameter is effectful because it depends on context.Context.
//
// Unlike Bind (which sequences operations), ApS can be used when operations are
// independent and can conceptually run in parallel.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the computation
//
// Parameters:
// - setter: A function that takes the computation result and returns a state updater
// - fa: An effectful ReaderResult computation
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
@@ -165,38 +224,119 @@ func ApS[S1, S2, T any](
)
}
// ApSL is a variant of ApS that uses a lens to focus on a specific field in the state.
//
// IMPORTANT: ApSL is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The ReaderResult parameter is effectful because it depends on context.Context.
//
// Instead of providing a setter function, you provide a lens that knows how to get and set
// the field. This is more convenient when working with nested structures.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field to update
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - fa: An effectful ReaderResult computation that produces a value of type T
//
// Returns:
// - An Operator that transforms ReaderResult[S] to ReaderResult[S]
//
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa ReaderResult[T],
) Operator[S, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
//
// IMPORTANT: BindL is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The Kleisli parameter returns a ReaderResult, which is effectful.
//
// It combines lens-based field access with monadic composition, allowing you to:
// 1. Extract a field value using the lens
// 2. Use that value in an effectful computation that may fail
// 3. Update the field with the result
//
// Type Parameters:
// - S: The state type
// - T: The type of the field to update
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - f: An effectful Kleisli arrow that transforms the field value
//
// Returns:
// - An Operator that transforms ReaderResult[S] to ReaderResult[S]
//
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return RR.BindL(lens, WithContextK(f))
}
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
//
// IMPORTANT: LetL is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
// The endomorphism parameter is a pure function (T -> T) with no errors or effects.
//
// It applies a pure transformation to the focused field without any effects.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field to update
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - f: A pure endomorphism that transforms the field value
//
// Returns:
// - An Operator that transforms ReaderResult[S] to ReaderResult[S]
//
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f Endomorphism[T],
) Operator[S, S] {
return RR.LetL[context.Context](lens, f)
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
//
// IMPORTANT: LetToL is for setting constant values. This is a PURE operation (side-effect free).
//
// It sets the focused field to a constant value.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field to update
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - b: The constant value to set
//
// Returns:
// - An Operator that transforms ReaderResult[S] to ReaderResult[S]
//
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
b T,
) Operator[S, S] {
return RR.LetToL[context.Context](lens, b)
}
// BindReaderK binds a Reader computation (context-dependent but error-free) into the do-notation chain.
//
// IMPORTANT: This is for functions that depend on context.Context but don't return errors.
// The Reader[Context, T] is effectful because it depends on context.Context.
// Use this when you need context values but the operation cannot fail.
//
//go:inline
func BindReaderK[S1, S2, T any](
setter func(T) func(S1) S2,
@@ -205,6 +345,12 @@ func BindReaderK[S1, S2, T any](
return RR.BindReaderK(setter, f)
}
// BindEitherK binds a Result (Either) computation into the do-notation chain.
//
// IMPORTANT: This is for PURE FUNCTIONS (side-effect free) that return Result[T].
// The function (State -> Result[T]) is pure - it only depends on state, not context.
// Use this for pure error-handling logic that doesn't need context.
//
//go:inline
func BindEitherK[S1, S2, T any](
setter func(T) func(S1) S2,
@@ -213,6 +359,14 @@ func BindEitherK[S1, S2, T any](
return RR.BindEitherK[context.Context](setter, f)
}
// BindResultK binds an idiomatic Go function (returning value and error) into the do-notation chain.
//
// IMPORTANT: This is for PURE FUNCTIONS (side-effect free) that return (Value, error).
// The function (State -> (Value, error)) is pure - it only depends on state, not context.
// Use this for pure computations with error handling that don't need context.
//
// For EFFECTFUL FUNCTIONS (that need context.Context), use Bind instead.
//
//go:inline
func BindResultK[S1, S2, T any](
setter func(T) func(S1) S2,
@@ -221,6 +375,11 @@ func BindResultK[S1, S2, T any](
return RR.BindResultK[context.Context](setter, f)
}
// BindToReader converts a Reader computation into a ReaderResult and binds it to create an initial state.
//
// IMPORTANT: Reader[Context, T] is EFFECTFUL because it depends on context.Context.
// Use this when you have a context-dependent computation that cannot fail.
//
//go:inline
func BindToReader[
S1, T any](
@@ -229,6 +388,11 @@ func BindToReader[
return RR.BindToReader[context.Context](setter)
}
// BindToEither converts a Result (Either) into a ReaderResult and binds it to create an initial state.
//
// IMPORTANT: Result[T] is PURE (side-effect free) - it doesn't depend on context.
// Use this to lift pure error-handling values into the ReaderResult context.
//
//go:inline
func BindToEither[
S1, T any](
@@ -237,6 +401,11 @@ func BindToEither[
return RR.BindToEither[context.Context](setter)
}
// BindToResult converts an idiomatic Go tuple (value, error) into a ReaderResult and binds it to create an initial state.
//
// IMPORTANT: The (Value, error) tuple is PURE (side-effect free) - it doesn't depend on context.
// Use this to lift pure Go error-handling results into the ReaderResult context.
//
//go:inline
func BindToResult[
S1, T any](
@@ -245,6 +414,11 @@ func BindToResult[
return RR.BindToResult[context.Context](setter)
}
// ApReaderS applies a Reader computation in applicative style, combining it with the current state.
//
// IMPORTANT: Reader[Context, T] is EFFECTFUL because it depends on context.Context.
// Use this for context-dependent operations that cannot fail.
//
//go:inline
func ApReaderS[
S1, S2, T any](
@@ -254,6 +428,11 @@ func ApReaderS[
return RR.ApReaderS(setter, fa)
}
// ApResultS applies an idiomatic Go tuple (value, error) in applicative style.
//
// IMPORTANT: The (Value, error) tuple is PURE (side-effect free) - it doesn't depend on context.
// Use this for pure Go error-handling results.
//
//go:inline
func ApResultS[
S1, S2, T any](
@@ -262,6 +441,11 @@ func ApResultS[
return RR.ApResultS[context.Context](setter)
}
// ApEitherS applies a Result (Either) in applicative style, combining it with the current state.
//
// IMPORTANT: Result[T] is PURE (side-effect free) - it doesn't depend on context.
// Use this for pure error-handling values.
//
//go:inline
func ApEitherS[
S1, S2, T any](

View File

@@ -0,0 +1,627 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestDoInit(t *testing.T) {
initial := SimpleState{Value: 42}
result := Do(initial)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, initial, state)
}
func TestBind(t *testing.T) {
t.Run("successful bind", func(t *testing.T) {
// Effectful function that depends on context
fetchValue := func(s SimpleState) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return s.Value * 2, nil
}
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
Bind(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
fetchValue,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("bind with error", func(t *testing.T) {
fetchValue := func(s SimpleState) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return 0, errors.New("fetch failed")
}
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
Bind(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
fetchValue,
),
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, "fetch failed", err.Error())
})
}
func TestLet(t *testing.T) {
// Pure function that doesn't depend on context
double := func(s SimpleState) int {
return s.Value * 2
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
Let(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
double,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestLetTo(t *testing.T) {
result := F.Pipe1(
Do(SimpleState{}),
LetTo(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
100,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, state.Value)
}
func TestBindToInit(t *testing.T) {
getValue := func(ctx context.Context) (int, error) {
return 42, nil
}
result := F.Pipe1(
getValue,
BindTo(func(v int) SimpleState {
return SimpleState{Value: v}
}),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestApS(t *testing.T) {
t.Run("successful ApS", func(t *testing.T) {
getValue := func(ctx context.Context) (int, error) {
return 100, nil
}
result := F.Pipe1(
Do(SimpleState{Value: 42}),
ApS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
getValue,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, state.Value)
})
t.Run("ApS with error", func(t *testing.T) {
getValue := func(ctx context.Context) (int, error) {
return 0, errors.New("failed")
}
result := F.Pipe1(
Do(SimpleState{Value: 42}),
ApS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
getValue,
),
)
_, err := result(context.Background())
assert.Error(t, err)
})
}
func TestApSL(t *testing.T) {
lenses := MakeSimpleStateLenses()
getValue := func(ctx context.Context) (int, error) {
return 100, nil
}
result := F.Pipe1(
Do(SimpleState{Value: 42}),
ApSL(lenses.Value, getValue),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, state.Value)
}
func TestBindL(t *testing.T) {
lenses := MakeSimpleStateLenses()
// Effectful function
increment := func(v int) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return v + 1, nil
}
}
result := F.Pipe1(
Do(SimpleState{Value: 41}),
BindL(lenses.Value, increment),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestLetL(t *testing.T) {
lenses := MakeSimpleStateLenses()
result := F.Pipe1(
Do(SimpleState{Value: 21}),
LetL(lenses.Value, N.Mul(2)),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestLetToL(t *testing.T) {
lenses := MakeSimpleStateLenses()
result := F.Pipe1(
Do(SimpleState{}),
LetToL(lenses.Value, 42),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestBindReaderK(t *testing.T) {
t.Run("successful BindReaderK", func(t *testing.T) {
// Context-dependent function that doesn't return error
getFromContext := func(s SimpleState) reader.Reader[context.Context, int] {
return func(ctx context.Context) int {
if val := ctx.Value("multiplier"); val != nil {
return s.Value * val.(int)
}
return s.Value
}
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindReaderK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
getFromContext,
),
)
ctx := context.WithValue(context.Background(), "multiplier", 2)
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
}
func TestBindEitherK(t *testing.T) {
t.Run("successful BindEitherK", func(t *testing.T) {
// Pure function returning Result
validate := func(s SimpleState) RES.Result[int] {
if s.Value > 0 {
return RES.Of(s.Value * 2)
}
return RES.Left[int](errors.New("value must be positive"))
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindEitherK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
validate,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("BindEitherK with error", func(t *testing.T) {
validate := func(s SimpleState) RES.Result[int] {
return RES.Left[int](errors.New("validation failed"))
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindEitherK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
validate,
),
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, "validation failed", err.Error())
})
}
func TestBindResultK(t *testing.T) {
t.Run("successful BindResultK", func(t *testing.T) {
// Pure function returning (value, error)
parse := func(s SimpleState) (int, error) {
return s.Value * 2, nil
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindResultK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
parse,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("BindResultK with error", func(t *testing.T) {
parse := func(s SimpleState) (int, error) {
return 0, errors.New("parse failed")
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindResultK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
parse,
),
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, "parse failed", err.Error())
})
}
func TestBindToReader(t *testing.T) {
getFromContext := func(ctx context.Context) int {
if val := ctx.Value("value"); val != nil {
return val.(int)
}
return 0
}
result := F.Pipe1(
getFromContext,
BindToReader(func(v int) SimpleState {
return SimpleState{Value: v}
}),
)
ctx := context.WithValue(context.Background(), "value", 42)
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestBindToEither(t *testing.T) {
t.Run("successful BindToEither", func(t *testing.T) {
resultValue := RES.Of(42)
result := F.Pipe1(
resultValue,
BindToEither(func(v int) SimpleState {
return SimpleState{Value: v}
}),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("BindToEither with error", func(t *testing.T) {
resultValue := RES.Left[int](errors.New("failed"))
result := F.Pipe1(
resultValue,
BindToEither(func(v int) SimpleState {
return SimpleState{Value: v}
}),
)
_, err := result(context.Background())
assert.Error(t, err)
})
}
func TestBindToResult(t *testing.T) {
t.Run("successful BindToResult", func(t *testing.T) {
value, err := 42, error(nil)
result := F.Pipe1(
BindToResult(func(v int) SimpleState {
return SimpleState{Value: v}
}),
func(f func(int, error) ReaderResult[SimpleState]) ReaderResult[SimpleState] {
return f(value, err)
},
)
state, resultErr := result(context.Background())
assert.NoError(t, resultErr)
assert.Equal(t, 42, state.Value)
})
t.Run("BindToResult with error", func(t *testing.T) {
value, err := 0, errors.New("failed")
result := F.Pipe1(
BindToResult(func(v int) SimpleState {
return SimpleState{Value: v}
}),
func(f func(int, error) ReaderResult[SimpleState]) ReaderResult[SimpleState] {
return f(value, err)
},
)
_, resultErr := result(context.Background())
assert.Error(t, resultErr)
})
}
func TestApReaderS(t *testing.T) {
getFromContext := func(ctx context.Context) int {
if val := ctx.Value("value"); val != nil {
return val.(int)
}
return 0
}
result := F.Pipe1(
Do(SimpleState{}),
ApReaderS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
getFromContext,
),
)
ctx := context.WithValue(context.Background(), "value", 42)
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestApResultS(t *testing.T) {
t.Run("successful ApResultS", func(t *testing.T) {
value, err := 42, error(nil)
result := F.Pipe1(
Do(SimpleState{}),
func(rr ReaderResult[SimpleState]) ReaderResult[SimpleState] {
return F.Pipe1(
rr,
ApResultS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
)(value, err),
)
},
)
state, resultErr := result(context.Background())
assert.NoError(t, resultErr)
assert.Equal(t, 42, state.Value)
})
t.Run("ApResultS with error", func(t *testing.T) {
value, err := 0, errors.New("failed")
result := F.Pipe1(
Do(SimpleState{}),
func(rr ReaderResult[SimpleState]) ReaderResult[SimpleState] {
return F.Pipe1(
rr,
ApResultS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
)(value, err),
)
},
)
_, resultErr := result(context.Background())
assert.Error(t, resultErr)
})
}
func TestApEitherS(t *testing.T) {
t.Run("successful ApEitherS", func(t *testing.T) {
resultValue := RES.Of(42)
result := F.Pipe1(
Do(SimpleState{}),
ApEitherS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
resultValue,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("ApEitherS with error", func(t *testing.T) {
resultValue := RES.Left[int](errors.New("failed"))
result := F.Pipe1(
Do(SimpleState{}),
ApEitherS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
resultValue,
),
)
_, err := result(context.Background())
assert.Error(t, err)
})
}
func TestComplexPipeline(t *testing.T) {
lenses := MakeSimpleStateLenses()
// Complex pipeline combining multiple operations
result := F.Pipe3(
Do(SimpleState{}),
LetToL(lenses.Value, 10),
LetL(lenses.Value, N.Mul(2)),
BindResultK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
func(s SimpleState) (int, error) {
return s.Value + 22, nil
},
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}

View File

@@ -130,6 +130,8 @@ import (
// }
// },
// )
//
//go:inline
func Bracket[
A, B, ANY any](
@@ -251,6 +253,8 @@ func Bracket[
// }),
// readerresult.Map(formatResult),
// )
//
//go:inline
func WithResource[B, A, ANY any](
onCreate Lazy[ReaderResult[A]],
onRelease Kleisli[A, ANY],
@@ -261,9 +265,9 @@ func WithResource[B, A, ANY any](
// onClose is a helper function that creates a ReaderResult that closes an io.Closer.
// This is used internally by WithCloser to provide automatic cleanup for resources
// that implement the io.Closer interface.
func onClose[A io.Closer](a A) ReaderResult[any] {
return func(_ context.Context) (any, error) {
return nil, a.Close()
func onClose[A io.Closer](a A) ReaderResult[struct{}] {
return func(_ context.Context) (struct{}, error) {
return struct{}{}, a.Close()
}
}
@@ -398,6 +402,8 @@ func onClose[A io.Closer](a A) ReaderResult[any] {
// Note: WithCloser is a convenience wrapper around WithResource that automatically
// provides the Close() cleanup function. For resources that don't implement io.Closer
// or require custom cleanup logic, use WithResource or Bracket instead.
//
//go:inline
func WithCloser[B any, A io.Closer](onCreate Lazy[ReaderResult[A]]) Kleisli[Kleisli[A, B], B] {
return WithResource[B](onCreate, onClose[A])
}

View File

@@ -0,0 +1,630 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"errors"
"io"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
// mockResource simulates a resource that needs cleanup
type mockResource struct {
id int
closed bool
closeMu sync.Mutex
closeErr error
}
func (m *mockResource) Close() error {
m.closeMu.Lock()
defer m.closeMu.Unlock()
m.closed = true
return m.closeErr
}
func (m *mockResource) IsClosed() bool {
m.closeMu.Lock()
defer m.closeMu.Unlock()
return m.closed
}
// mockCloser implements io.Closer for testing WithCloser
type mockCloser struct {
*mockResource
}
func TestBracketExtended(t *testing.T) {
t.Run("successful acquire, use, and release with real resource", func(t *testing.T) {
resource := &mockResource{id: 1}
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 2, nil
}
},
// Release
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
assert.Equal(t, 2, result)
assert.NoError(t, err)
return nil, r.Close()
}
},
)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 2, value)
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
t.Run("acquire fails - release not called", func(t *testing.T) {
acquireErr := errors.New("acquire failed")
released := false
result := Bracket(
// Acquire fails
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return nil, acquireErr
}
},
// Use (should not be called)
func(r *mockResource) ReaderResult[int] {
t.Fatal("use should not be called when acquire fails")
return func(ctx context.Context) (int, error) {
return 0, nil
}
},
// Release (should not be called)
func(r *mockResource, result int, err error) ReaderResult[any] {
released = true
return func(ctx context.Context) (any, error) {
return nil, nil
}
},
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, acquireErr, err)
assert.False(t, released)
})
t.Run("use fails - release still called", func(t *testing.T) {
resource := &mockResource{id: 1}
useErr := errors.New("use failed")
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use fails
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return 0, useErr
}
},
// Release (should still be called)
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
assert.Equal(t, 0, result)
assert.Equal(t, useErr, err)
return nil, r.Close()
}
},
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, useErr, err)
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
t.Run("release fails - error propagated", func(t *testing.T) {
resource := &mockResource{id: 1, closeErr: errors.New("close failed")}
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use succeeds
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 2, nil
}
},
// Release fails
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
return nil, r.Close()
}
},
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, "close failed", err.Error())
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
t.Run("both use and release fail - use error takes precedence", func(t *testing.T) {
resource := &mockResource{id: 1, closeErr: errors.New("close failed")}
useErr := errors.New("use failed")
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use fails
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return 0, useErr
}
},
// Release also fails
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
assert.Equal(t, useErr, err)
return nil, r.Close()
}
},
)
_, err := result(context.Background())
assert.Error(t, err)
// The use error should be returned
assert.Equal(t, useErr, err)
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
t.Run("context cancellation during use", func(t *testing.T) {
resource := &mockResource{id: 1}
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use checks context
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
return r.id * 2, nil
}
}
},
// Release
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
return nil, r.Close()
}
},
)
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
_, err := result(ctx)
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
}
func TestWithResource(t *testing.T) {
t.Run("reusable resource manager - successful operations", func(t *testing.T) {
resource := &mockResource{id: 42}
createCount := 0
releaseCount := 0
withResource := WithResource[int](
// onCreate
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
createCount++
return resource, nil
}
},
// onRelease
func(r *mockResource) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
releaseCount++
return nil, r.Close()
}
},
)
// First operation
operation1 := withResource(func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 2, nil
}
})
result1, err1 := operation1(context.Background())
assert.NoError(t, err1)
assert.Equal(t, 84, result1)
assert.Equal(t, 1, createCount)
assert.Equal(t, 1, releaseCount)
// Reset for second operation
resource.closed = false
// Second operation with same resource manager
operation2 := withResource(func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id + 10, nil
}
})
result2, err2 := operation2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, 52, result2)
assert.Equal(t, 2, createCount)
assert.Equal(t, 2, releaseCount)
})
t.Run("resource manager with failing operation", func(t *testing.T) {
resource := &mockResource{id: 42}
releaseCount := 0
opErr := errors.New("operation failed")
withResource := WithResource[int](
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
func(r *mockResource) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
releaseCount++
return nil, r.Close()
}
},
)
operation := withResource(func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return 0, opErr
}
})
_, err := operation(context.Background())
assert.Error(t, err)
assert.Equal(t, opErr, err)
assert.Equal(t, 1, releaseCount)
assert.True(t, resource.IsClosed())
})
t.Run("nested resource managers", func(t *testing.T) {
resource1 := &mockResource{id: 1}
resource2 := &mockResource{id: 2}
withResource1 := WithResource[int](
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource1, nil
}
},
func(r *mockResource) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, r.Close()
}
},
)
withResource2 := WithResource[int](
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource2, nil
}
},
func(r *mockResource) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, r.Close()
}
},
)
// Nest the resource managers
operation := withResource1(func(r1 *mockResource) ReaderResult[int] {
return withResource2(func(r2 *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r1.id + r2.id, nil
}
})
})
result, err := operation(context.Background())
assert.NoError(t, err)
assert.Equal(t, 3, result)
assert.True(t, resource1.IsClosed())
assert.True(t, resource2.IsClosed())
})
}
func TestWithCloser(t *testing.T) {
t.Run("successful operation with io.Closer", func(t *testing.T) {
resource := &mockCloser{mockResource: &mockResource{id: 100}}
withCloser := WithCloser[string](
func() ReaderResult[*mockCloser] {
return func(ctx context.Context) (*mockCloser, error) {
return resource, nil
}
},
)
operation := withCloser(func(r *mockCloser) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "success", nil
}
})
result, err := operation(context.Background())
assert.NoError(t, err)
assert.Equal(t, "success", result)
assert.True(t, resource.IsClosed())
})
t.Run("operation fails but closer still called", func(t *testing.T) {
resource := &mockCloser{mockResource: &mockResource{id: 100}}
opErr := errors.New("operation failed")
withCloser := WithCloser[string](
func() ReaderResult[*mockCloser] {
return func(ctx context.Context) (*mockCloser, error) {
return resource, nil
}
},
)
operation := withCloser(func(r *mockCloser) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "", opErr
}
})
_, err := operation(context.Background())
assert.Error(t, err)
assert.Equal(t, opErr, err)
assert.True(t, resource.IsClosed())
})
t.Run("closer fails", func(t *testing.T) {
closeErr := errors.New("close failed")
resource := &mockCloser{mockResource: &mockResource{id: 100, closeErr: closeErr}}
withCloser := WithCloser[string](
func() ReaderResult[*mockCloser] {
return func(ctx context.Context) (*mockCloser, error) {
return resource, nil
}
},
)
operation := withCloser(func(r *mockCloser) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "success", nil
}
})
_, err := operation(context.Background())
assert.Error(t, err)
assert.Equal(t, closeErr, err)
assert.True(t, resource.IsClosed())
})
t.Run("with strings.Reader (real io.Closer)", func(t *testing.T) {
content := "Hello, World!"
withReader := WithCloser[string](
func() ReaderResult[io.ReadCloser] {
return func(ctx context.Context) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(content)), nil
}
},
)
operation := withReader(func(r io.ReadCloser) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
data, err := io.ReadAll(r)
return string(data), err
}
})
result, err := operation(context.Background())
assert.NoError(t, err)
assert.Equal(t, content, result)
})
t.Run("multiple operations with same closer", func(t *testing.T) {
createCount := 0
withCloser := WithCloser[int](
func() ReaderResult[*mockCloser] {
return func(ctx context.Context) (*mockCloser, error) {
createCount++
return &mockCloser{mockResource: &mockResource{id: createCount}}, nil
}
},
)
// First operation
op1 := withCloser(func(r *mockCloser) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 10, nil
}
})
result1, err1 := op1(context.Background())
assert.NoError(t, err1)
assert.Equal(t, 10, result1)
// Second operation
op2 := withCloser(func(r *mockCloser) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 20, nil
}
})
result2, err2 := op2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, 40, result2)
assert.Equal(t, 2, createCount)
})
}
func TestOnClose(t *testing.T) {
t.Run("onClose helper function", func(t *testing.T) {
resource := &mockCloser{mockResource: &mockResource{id: 1}}
closeFunc := onClose(resource)
_, err := closeFunc(context.Background())
assert.NoError(t, err)
assert.True(t, resource.IsClosed())
})
t.Run("onClose with error", func(t *testing.T) {
closeErr := errors.New("close error")
resource := &mockCloser{mockResource: &mockResource{id: 1, closeErr: closeErr}}
closeFunc := onClose(resource)
_, err := closeFunc(context.Background())
assert.Error(t, err)
assert.Equal(t, closeErr, err)
assert.True(t, resource.IsClosed())
})
}
// Integration test combining multiple bracket patterns
func TestBracketIntegration(t *testing.T) {
t.Run("complex resource management scenario", func(t *testing.T) {
// Simulate a scenario with multiple resources
db := &mockResource{id: 1}
cache := &mockResource{id: 2}
logger := &mockResource{id: 3}
result := Bracket(
// Acquire DB
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return db, nil
}
},
// Use DB to get cache and logger
func(dbRes *mockResource) ReaderResult[int] {
return Bracket(
// Acquire cache
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return cache, nil
}
},
// Use cache to get logger
func(cacheRes *mockResource) ReaderResult[int] {
return Bracket(
// Acquire logger
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return logger, nil
}
},
// Use all resources
func(logRes *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return dbRes.id + cacheRes.id + logRes.id, nil
}
},
// Release logger
func(logRes *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, logRes.Close()
}
},
)
},
// Release cache
func(cacheRes *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, cacheRes.Close()
}
},
)
},
// Release DB
func(dbRes *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, dbRes.Close()
}
},
)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 6, value) // 1 + 2 + 3
assert.True(t, db.IsClosed())
assert.True(t, cache.IsClosed())
assert.True(t, logger.IsClosed())
})
}

View File

@@ -364,11 +364,12 @@ type UserState struct {
// This is typically used as the first operation after a computation to
// start building up a state structure.
func ExampleBindTo() {
userStatePrisms := MakeUserStatePrisms()
result := F.Pipe1(
getUser(42),
BindTo(func(u User) UserState {
return UserState{User: u}
}),
BindToP(userStatePrisms.User),
)
state, err := result(context.Background())
@@ -385,6 +386,8 @@ type ConfigState struct {
// but error-free) into a ReaderResult do-notation chain.
func ExampleBindReaderK() {
configStateLenses := MakeConfigStateLenses()
// A Reader that extracts a value from context
getConfig := func(ctx context.Context) string {
if val := ctx.Value("config"); val != nil {
@@ -395,14 +398,8 @@ func ExampleBindReaderK() {
result := F.Pipe1(
Do(ConfigState{}),
BindReaderK(
func(cfg string) Endomorphism[ConfigState] {
return func(s ConfigState) ConfigState {
s.Config = cfg
return s
}
},
func(s ConfigState) func(context.Context) string {
BindReaderK(configStateLenses.Config.Set,
func(s ConfigState) Reader[context.Context, string] {
return getConfig
},
),
@@ -423,6 +420,9 @@ type NumberState struct {
// a ReaderResult do-notation chain. This is useful for integrating pure
// error-handling logic that doesn't need context.
func ExampleBindEitherK() {
numberStateLenses := MakeNumberStateLenses()
// A pure function that returns a Result
parseNumber := func(s NumberState) RES.Result[int] {
return RES.Of(42)
@@ -431,12 +431,7 @@ func ExampleBindEitherK() {
result := F.Pipe1(
Do(NumberState{}),
BindEitherK(
func(n int) Endomorphism[NumberState] {
return func(s NumberState) NumberState {
s.Number = n
return s
}
},
numberStateLenses.Number.Set,
parseNumber,
),
)
@@ -452,8 +447,102 @@ type DataState struct {
}
// ExampleBindResultK demonstrates binding an idiomatic Go function (returning
// value and error) into a ReaderResult do-notation chain.
// value and error) into a ReaderResult do-notation chain. This is particularly
// useful for integrating existing Go code that follows the standard (value, error)
// return pattern into functional pipelines.
//
// Step-by-step breakdown:
//
// 1. dataStateLenses := MakeDataStateLenses() - Create lenses for accessing
// DataState fields. This provides functional accessors (getters and setters)
// for the Data field, enabling type-safe, immutable field updates.
//
// 2. fetchData := func(s DataState) (string, error) - Define an idiomatic Go
// function that takes the current state and returns a tuple of (value, error).
//
// IMPORTANT: This function represents a PURE READER COMPOSITION - it reads from
// the state and performs computations that don't require a context.Context.
// This is suitable for:
// - Pure computations that may fail (parsing, validation, calculations)
// - Operations that only depend on the state, not external context
// - Stateless transformations with error handling
// - Synchronous operations that don't need cancellation or timeouts
//
// For EFFECTFUL COMPOSITION (operations that need context), use the full
// ReaderResult type instead: func(context.Context) (Value, error)
// Use ReaderResult when you need:
// - Context cancellation or timeouts
// - Context values (request IDs, trace IDs, etc.)
// - Operations that depend on external context state
// - Async operations that should respect context lifecycle
//
// In this example, fetchData always succeeds with "fetched data", but in real
// code it might perform pure operations like:
// - Parsing or validating data from the state
// - Performing calculations that could fail
// - Calling pure functions from external libraries
// - Data transformations that don't require context
//
// 3. Do(DataState{}) - Initialize the do-notation chain with an empty DataState.
// This creates the initial ReaderResult that will accumulate data through
// subsequent operations.
// Initial state: {Data: ""}
//
// 4. BindResultK(dataStateLenses.Data.Set, fetchData) - Bind the idiomatic Go
// function into the ReaderResult chain.
//
// BindResultK takes two parameters:
//
// a) First parameter: dataStateLenses.Data.Set
// This is a setter function from the lens that will update the Data field
// with the result of the computation. The lens ensures immutable updates.
//
// b) Second parameter: fetchData
// This is the idiomatic Go function (State -> (Value, error)) that will be
// lifted into the ReaderResult context.
//
// The BindResultK operation flow:
// - Takes the current state: {Data: ""}
// - Calls fetchData with the state: fetchData(DataState{})
// - Gets the result tuple: ("fetched data", nil)
// - If error is not nil, short-circuits the chain and returns the error
// - If error is nil, uses the setter to update state.Data with "fetched data"
// - Returns the updated state: {Data: "fetched data"}
// After this step: {Data: "fetched data"}
//
// 5. result(context.Background()) - Execute the computation chain with a context.
// Even though fetchData doesn't use the context, the ReaderResult still needs
// one to maintain the uniform interface. This runs all operations in sequence
// and returns the final state and any error.
//
// Key concepts demonstrated:
// - Integration of idiomatic Go code: BindResultK bridges functional and imperative styles
// - Error propagation: Errors from the Go function automatically propagate through the chain
// - State transformation: The result updates the state using lens-based setters
// - Context independence: The function doesn't need context but still works in ReaderResult
//
// Comparison with other bind operations:
// - BindResultK: For idiomatic Go functions (State -> (Value, error))
// - Bind: For full ReaderResult computations (State -> ReaderResult[Value])
// - BindEitherK: For pure Result/Either values (State -> Result[Value])
// - BindReaderK: For context-dependent functions (State -> Reader[Context, Value])
//
// Use BindResultK when you need to:
// - Integrate existing Go code that returns (value, error)
// - Call functions that may fail but don't need context
// - Perform stateful computations with standard Go error handling
// - Bridge between functional pipelines and imperative Go code
// - Work with libraries that follow Go conventions
//
// Real-world example scenarios:
// - Parsing JSON from a state field: func(s State) (ParsedData, error)
// - Validating user input: func(s State) (ValidatedInput, error)
// - Performing calculations: func(s State) (Result, error)
// - Calling third-party libraries: func(s State) (APIResponse, error)
func ExampleBindResultK() {
dataStateLenses := MakeDataStateLenses()
// An idiomatic Go function returning (value, error)
fetchData := func(s DataState) (string, error) {
return "fetched data", nil
@@ -462,12 +551,7 @@ func ExampleBindResultK() {
result := F.Pipe1(
Do(DataState{}),
BindResultK(
func(data string) Endomorphism[DataState] {
return func(s DataState) DataState {
s.Data = data
return s
}
},
dataStateLenses.Data.Set,
fetchData,
),
)

View File

@@ -2,36 +2,49 @@ package readerresult
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2025-12-16 13:45:59.7460125 +0100 CET m=+0.011815501
// 2025-12-16 15:20:56.2461527 +0100 CET m=+0.014539301
import (
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
__lens "github.com/IBM/fp-go/v2/optics/lens"
__option "github.com/IBM/fp-go/v2/option"
__prism "github.com/IBM/fp-go/v2/optics/prism"
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
)
// PostLenses provides lenses for accessing fields of Post
type PostLenses struct {
// mandatory fields
ID __lens.Lens[Post, int]
ID __lens.Lens[Post, int]
UserID __lens.Lens[Post, int]
Title __lens.Lens[Post, string]
Title __lens.Lens[Post, string]
// optional fields
IDO __lens_option.LensO[Post, int]
IDO __lens_option.LensO[Post, int]
UserIDO __lens_option.LensO[Post, int]
TitleO __lens_option.LensO[Post, string]
TitleO __lens_option.LensO[Post, string]
}
// PostRefLenses provides lenses for accessing fields of Post via a reference to Post
type PostRefLenses struct {
// mandatory fields
ID __lens.Lens[*Post, int]
ID __lens.Lens[*Post, int]
UserID __lens.Lens[*Post, int]
Title __lens.Lens[*Post, string]
Title __lens.Lens[*Post, string]
// optional fields
IDO __lens_option.LensO[*Post, int]
IDO __lens_option.LensO[*Post, int]
UserIDO __lens_option.LensO[*Post, int]
TitleO __lens_option.LensO[*Post, string]
TitleO __lens_option.LensO[*Post, string]
// prisms
IDP __prism.Prism[*Post, int]
UserIDP __prism.Prism[*Post, int]
TitleP __prism.Prism[*Post, string]
}
// PostPrisms provides prisms for accessing fields of Post
type PostPrisms struct {
ID __prism.Prism[Post, int]
UserID __prism.Prism[Post, int]
Title __prism.Prism[Post, string]
}
// MakePostLenses creates a new PostLenses with lenses for all fields
@@ -58,13 +71,13 @@ func MakePostLenses() PostLenses {
lensTitleO := __lens_option.FromIso[Post](__iso_option.FromZero[string]())(lensTitle)
return PostLenses{
// mandatory lenses
ID: lensID,
ID: lensID,
UserID: lensUserID,
Title: lensTitle,
Title: lensTitle,
// optional lenses
IDO: lensIDO,
IDO: lensIDO,
UserIDO: lensUserIDO,
TitleO: lensTitleO,
TitleO: lensTitleO,
}
}
@@ -92,40 +105,80 @@ func MakePostRefLenses() PostRefLenses {
lensTitleO := __lens_option.FromIso[*Post](__iso_option.FromZero[string]())(lensTitle)
return PostRefLenses{
// mandatory lenses
ID: lensID,
ID: lensID,
UserID: lensUserID,
Title: lensTitle,
Title: lensTitle,
// optional lenses
IDO: lensIDO,
IDO: lensIDO,
UserIDO: lensUserIDO,
TitleO: lensTitleO,
TitleO: lensTitleO,
}
}
// MakePostPrisms creates a new PostPrisms with prisms for all fields
func MakePostPrisms() PostPrisms {
_fromNonZeroID := __option.FromNonZero[int]()
_prismID := __prism.MakePrismWithName(
func(s Post) __option.Option[int] { return _fromNonZeroID(s.ID) },
func(v int) Post { return Post{ ID: v } },
"Post.ID",
)
_fromNonZeroUserID := __option.FromNonZero[int]()
_prismUserID := __prism.MakePrismWithName(
func(s Post) __option.Option[int] { return _fromNonZeroUserID(s.UserID) },
func(v int) Post { return Post{ UserID: v } },
"Post.UserID",
)
_fromNonZeroTitle := __option.FromNonZero[string]()
_prismTitle := __prism.MakePrismWithName(
func(s Post) __option.Option[string] { return _fromNonZeroTitle(s.Title) },
func(v string) Post { return Post{ Title: v } },
"Post.Title",
)
return PostPrisms {
ID: _prismID,
UserID: _prismUserID,
Title: _prismTitle,
}
}
// StateLenses provides lenses for accessing fields of State
type StateLenses struct {
// mandatory fields
User __lens.Lens[State, User]
Posts __lens.Lens[State, []Post]
User __lens.Lens[State, User]
Posts __lens.Lens[State, []Post]
FullName __lens.Lens[State, string]
Status __lens.Lens[State, string]
Status __lens.Lens[State, string]
// optional fields
UserO __lens_option.LensO[State, User]
UserO __lens_option.LensO[State, User]
FullNameO __lens_option.LensO[State, string]
StatusO __lens_option.LensO[State, string]
StatusO __lens_option.LensO[State, string]
}
// StateRefLenses provides lenses for accessing fields of State via a reference to State
type StateRefLenses struct {
// mandatory fields
User __lens.Lens[*State, User]
Posts __lens.Lens[*State, []Post]
User __lens.Lens[*State, User]
Posts __lens.Lens[*State, []Post]
FullName __lens.Lens[*State, string]
Status __lens.Lens[*State, string]
Status __lens.Lens[*State, string]
// optional fields
UserO __lens_option.LensO[*State, User]
UserO __lens_option.LensO[*State, User]
FullNameO __lens_option.LensO[*State, string]
StatusO __lens_option.LensO[*State, string]
StatusO __lens_option.LensO[*State, string]
// prisms
UserP __prism.Prism[*State, User]
PostsP __prism.Prism[*State, []Post]
FullNameP __prism.Prism[*State, string]
StatusP __prism.Prism[*State, string]
}
// StatePrisms provides prisms for accessing fields of State
type StatePrisms struct {
User __prism.Prism[State, User]
Posts __prism.Prism[State, []Post]
FullName __prism.Prism[State, string]
Status __prism.Prism[State, string]
}
// MakeStateLenses creates a new StateLenses with lenses for all fields
@@ -157,14 +210,14 @@ func MakeStateLenses() StateLenses {
lensStatusO := __lens_option.FromIso[State](__iso_option.FromZero[string]())(lensStatus)
return StateLenses{
// mandatory lenses
User: lensUser,
Posts: lensPosts,
User: lensUser,
Posts: lensPosts,
FullName: lensFullName,
Status: lensStatus,
Status: lensStatus,
// optional lenses
UserO: lensUserO,
UserO: lensUserO,
FullNameO: lensFullNameO,
StatusO: lensStatusO,
StatusO: lensStatusO,
}
}
@@ -197,14 +250,47 @@ func MakeStateRefLenses() StateRefLenses {
lensStatusO := __lens_option.FromIso[*State](__iso_option.FromZero[string]())(lensStatus)
return StateRefLenses{
// mandatory lenses
User: lensUser,
Posts: lensPosts,
User: lensUser,
Posts: lensPosts,
FullName: lensFullName,
Status: lensStatus,
Status: lensStatus,
// optional lenses
UserO: lensUserO,
UserO: lensUserO,
FullNameO: lensFullNameO,
StatusO: lensStatusO,
StatusO: lensStatusO,
}
}
// MakeStatePrisms creates a new StatePrisms with prisms for all fields
func MakeStatePrisms() StatePrisms {
_fromNonZeroUser := __option.FromNonZero[User]()
_prismUser := __prism.MakePrismWithName(
func(s State) __option.Option[User] { return _fromNonZeroUser(s.User) },
func(v User) State { return State{ User: v } },
"State.User",
)
_prismPosts := __prism.MakePrismWithName(
func(s State) __option.Option[[]Post] { return __option.Some(s.Posts) },
func(v []Post) State { return State{ Posts: v } },
"State.Posts",
)
_fromNonZeroFullName := __option.FromNonZero[string]()
_prismFullName := __prism.MakePrismWithName(
func(s State) __option.Option[string] { return _fromNonZeroFullName(s.FullName) },
func(v string) State { return State{ FullName: v } },
"State.FullName",
)
_fromNonZeroStatus := __option.FromNonZero[string]()
_prismStatus := __prism.MakePrismWithName(
func(s State) __option.Option[string] { return _fromNonZeroStatus(s.Status) },
func(v string) State { return State{ Status: v } },
"State.Status",
)
return StatePrisms {
User: _prismUser,
Posts: _prismPosts,
FullName: _prismFullName,
Status: _prismStatus,
}
}
@@ -222,6 +308,13 @@ type SimpleStateRefLenses struct {
Value __lens.Lens[*SimpleState, int]
// optional fields
ValueO __lens_option.LensO[*SimpleState, int]
// prisms
ValueP __prism.Prism[*SimpleState, int]
}
// SimpleStatePrisms provides prisms for accessing fields of SimpleState
type SimpleStatePrisms struct {
Value __prism.Prism[SimpleState, int]
}
// MakeSimpleStateLenses creates a new SimpleStateLenses with lenses for all fields
@@ -260,28 +353,52 @@ func MakeSimpleStateRefLenses() SimpleStateRefLenses {
}
}
// MakeSimpleStatePrisms creates a new SimpleStatePrisms with prisms for all fields
func MakeSimpleStatePrisms() SimpleStatePrisms {
_fromNonZeroValue := __option.FromNonZero[int]()
_prismValue := __prism.MakePrismWithName(
func(s SimpleState) __option.Option[int] { return _fromNonZeroValue(s.Value) },
func(v int) SimpleState { return SimpleState{ Value: v } },
"SimpleState.Value",
)
return SimpleStatePrisms {
Value: _prismValue,
}
}
// NameStateLenses provides lenses for accessing fields of NameState
type NameStateLenses struct {
// mandatory fields
FirstName __lens.Lens[NameState, string]
LastName __lens.Lens[NameState, string]
FullName __lens.Lens[NameState, string]
LastName __lens.Lens[NameState, string]
FullName __lens.Lens[NameState, string]
// optional fields
FirstNameO __lens_option.LensO[NameState, string]
LastNameO __lens_option.LensO[NameState, string]
FullNameO __lens_option.LensO[NameState, string]
LastNameO __lens_option.LensO[NameState, string]
FullNameO __lens_option.LensO[NameState, string]
}
// NameStateRefLenses provides lenses for accessing fields of NameState via a reference to NameState
type NameStateRefLenses struct {
// mandatory fields
FirstName __lens.Lens[*NameState, string]
LastName __lens.Lens[*NameState, string]
FullName __lens.Lens[*NameState, string]
LastName __lens.Lens[*NameState, string]
FullName __lens.Lens[*NameState, string]
// optional fields
FirstNameO __lens_option.LensO[*NameState, string]
LastNameO __lens_option.LensO[*NameState, string]
FullNameO __lens_option.LensO[*NameState, string]
LastNameO __lens_option.LensO[*NameState, string]
FullNameO __lens_option.LensO[*NameState, string]
// prisms
FirstNameP __prism.Prism[*NameState, string]
LastNameP __prism.Prism[*NameState, string]
FullNameP __prism.Prism[*NameState, string]
}
// NameStatePrisms provides prisms for accessing fields of NameState
type NameStatePrisms struct {
FirstName __prism.Prism[NameState, string]
LastName __prism.Prism[NameState, string]
FullName __prism.Prism[NameState, string]
}
// MakeNameStateLenses creates a new NameStateLenses with lenses for all fields
@@ -309,12 +426,12 @@ func MakeNameStateLenses() NameStateLenses {
return NameStateLenses{
// mandatory lenses
FirstName: lensFirstName,
LastName: lensLastName,
FullName: lensFullName,
LastName: lensLastName,
FullName: lensFullName,
// optional lenses
FirstNameO: lensFirstNameO,
LastNameO: lensLastNameO,
FullNameO: lensFullNameO,
LastNameO: lensLastNameO,
FullNameO: lensFullNameO,
}
}
@@ -343,12 +460,39 @@ func MakeNameStateRefLenses() NameStateRefLenses {
return NameStateRefLenses{
// mandatory lenses
FirstName: lensFirstName,
LastName: lensLastName,
FullName: lensFullName,
LastName: lensLastName,
FullName: lensFullName,
// optional lenses
FirstNameO: lensFirstNameO,
LastNameO: lensLastNameO,
FullNameO: lensFullNameO,
LastNameO: lensLastNameO,
FullNameO: lensFullNameO,
}
}
// MakeNameStatePrisms creates a new NameStatePrisms with prisms for all fields
func MakeNameStatePrisms() NameStatePrisms {
_fromNonZeroFirstName := __option.FromNonZero[string]()
_prismFirstName := __prism.MakePrismWithName(
func(s NameState) __option.Option[string] { return _fromNonZeroFirstName(s.FirstName) },
func(v string) NameState { return NameState{ FirstName: v } },
"NameState.FirstName",
)
_fromNonZeroLastName := __option.FromNonZero[string]()
_prismLastName := __prism.MakePrismWithName(
func(s NameState) __option.Option[string] { return _fromNonZeroLastName(s.LastName) },
func(v string) NameState { return NameState{ LastName: v } },
"NameState.LastName",
)
_fromNonZeroFullName := __option.FromNonZero[string]()
_prismFullName := __prism.MakePrismWithName(
func(s NameState) __option.Option[string] { return _fromNonZeroFullName(s.FullName) },
func(v string) NameState { return NameState{ FullName: v } },
"NameState.FullName",
)
return NameStatePrisms {
FirstName: _prismFirstName,
LastName: _prismLastName,
FullName: _prismFullName,
}
}
@@ -366,6 +510,13 @@ type StatusStateRefLenses struct {
Status __lens.Lens[*StatusState, string]
// optional fields
StatusO __lens_option.LensO[*StatusState, string]
// prisms
StatusP __prism.Prism[*StatusState, string]
}
// StatusStatePrisms provides prisms for accessing fields of StatusState
type StatusStatePrisms struct {
Status __prism.Prism[StatusState, string]
}
// MakeStatusStateLenses creates a new StatusStateLenses with lenses for all fields
@@ -404,6 +555,19 @@ func MakeStatusStateRefLenses() StatusStateRefLenses {
}
}
// MakeStatusStatePrisms creates a new StatusStatePrisms with prisms for all fields
func MakeStatusStatePrisms() StatusStatePrisms {
_fromNonZeroStatus := __option.FromNonZero[string]()
_prismStatus := __prism.MakePrismWithName(
func(s StatusState) __option.Option[string] { return _fromNonZeroStatus(s.Status) },
func(v string) StatusState { return StatusState{ Status: v } },
"StatusState.Status",
)
return StatusStatePrisms {
Status: _prismStatus,
}
}
// UserStateLenses provides lenses for accessing fields of UserState
type UserStateLenses struct {
// mandatory fields
@@ -418,6 +582,13 @@ type UserStateRefLenses struct {
User __lens.Lens[*UserState, User]
// optional fields
UserO __lens_option.LensO[*UserState, User]
// prisms
UserP __prism.Prism[*UserState, User]
}
// UserStatePrisms provides prisms for accessing fields of UserState
type UserStatePrisms struct {
User __prism.Prism[UserState, User]
}
// MakeUserStateLenses creates a new UserStateLenses with lenses for all fields
@@ -456,6 +627,19 @@ func MakeUserStateRefLenses() UserStateRefLenses {
}
}
// MakeUserStatePrisms creates a new UserStatePrisms with prisms for all fields
func MakeUserStatePrisms() UserStatePrisms {
_fromNonZeroUser := __option.FromNonZero[User]()
_prismUser := __prism.MakePrismWithName(
func(s UserState) __option.Option[User] { return _fromNonZeroUser(s.User) },
func(v User) UserState { return UserState{ User: v } },
"UserState.User",
)
return UserStatePrisms {
User: _prismUser,
}
}
// ConfigStateLenses provides lenses for accessing fields of ConfigState
type ConfigStateLenses struct {
// mandatory fields
@@ -470,6 +654,13 @@ type ConfigStateRefLenses struct {
Config __lens.Lens[*ConfigState, string]
// optional fields
ConfigO __lens_option.LensO[*ConfigState, string]
// prisms
ConfigP __prism.Prism[*ConfigState, string]
}
// ConfigStatePrisms provides prisms for accessing fields of ConfigState
type ConfigStatePrisms struct {
Config __prism.Prism[ConfigState, string]
}
// MakeConfigStateLenses creates a new ConfigStateLenses with lenses for all fields
@@ -508,6 +699,19 @@ func MakeConfigStateRefLenses() ConfigStateRefLenses {
}
}
// MakeConfigStatePrisms creates a new ConfigStatePrisms with prisms for all fields
func MakeConfigStatePrisms() ConfigStatePrisms {
_fromNonZeroConfig := __option.FromNonZero[string]()
_prismConfig := __prism.MakePrismWithName(
func(s ConfigState) __option.Option[string] { return _fromNonZeroConfig(s.Config) },
func(v string) ConfigState { return ConfigState{ Config: v } },
"ConfigState.Config",
)
return ConfigStatePrisms {
Config: _prismConfig,
}
}
// NumberStateLenses provides lenses for accessing fields of NumberState
type NumberStateLenses struct {
// mandatory fields
@@ -522,6 +726,13 @@ type NumberStateRefLenses struct {
Number __lens.Lens[*NumberState, int]
// optional fields
NumberO __lens_option.LensO[*NumberState, int]
// prisms
NumberP __prism.Prism[*NumberState, int]
}
// NumberStatePrisms provides prisms for accessing fields of NumberState
type NumberStatePrisms struct {
Number __prism.Prism[NumberState, int]
}
// MakeNumberStateLenses creates a new NumberStateLenses with lenses for all fields
@@ -560,6 +771,19 @@ func MakeNumberStateRefLenses() NumberStateRefLenses {
}
}
// MakeNumberStatePrisms creates a new NumberStatePrisms with prisms for all fields
func MakeNumberStatePrisms() NumberStatePrisms {
_fromNonZeroNumber := __option.FromNonZero[int]()
_prismNumber := __prism.MakePrismWithName(
func(s NumberState) __option.Option[int] { return _fromNonZeroNumber(s.Number) },
func(v int) NumberState { return NumberState{ Number: v } },
"NumberState.Number",
)
return NumberStatePrisms {
Number: _prismNumber,
}
}
// DataStateLenses provides lenses for accessing fields of DataState
type DataStateLenses struct {
// mandatory fields
@@ -574,6 +798,13 @@ type DataStateRefLenses struct {
Data __lens.Lens[*DataState, string]
// optional fields
DataO __lens_option.LensO[*DataState, string]
// prisms
DataP __prism.Prism[*DataState, string]
}
// DataStatePrisms provides prisms for accessing fields of DataState
type DataStatePrisms struct {
Data __prism.Prism[DataState, string]
}
// MakeDataStateLenses creates a new DataStateLenses with lenses for all fields
@@ -612,6 +843,19 @@ func MakeDataStateRefLenses() DataStateRefLenses {
}
}
// MakeDataStatePrisms creates a new DataStatePrisms with prisms for all fields
func MakeDataStatePrisms() DataStatePrisms {
_fromNonZeroData := __option.FromNonZero[string]()
_prismData := __prism.MakePrismWithName(
func(s DataState) __option.Option[string] { return _fromNonZeroData(s.Data) },
func(v string) DataState { return DataState{ Data: v } },
"DataState.Data",
)
return DataStatePrisms {
Data: _prismData,
}
}
// RequestStateLenses provides lenses for accessing fields of RequestState
type RequestStateLenses struct {
// mandatory fields
@@ -626,6 +870,13 @@ type RequestStateRefLenses struct {
RequestID __lens.Lens[*RequestState, string]
// optional fields
RequestIDO __lens_option.LensO[*RequestState, string]
// prisms
RequestIDP __prism.Prism[*RequestState, string]
}
// RequestStatePrisms provides prisms for accessing fields of RequestState
type RequestStatePrisms struct {
RequestID __prism.Prism[RequestState, string]
}
// MakeRequestStateLenses creates a new RequestStateLenses with lenses for all fields
@@ -664,6 +915,19 @@ func MakeRequestStateRefLenses() RequestStateRefLenses {
}
}
// MakeRequestStatePrisms creates a new RequestStatePrisms with prisms for all fields
func MakeRequestStatePrisms() RequestStatePrisms {
_fromNonZeroRequestID := __option.FromNonZero[string]()
_prismRequestID := __prism.MakePrismWithName(
func(s RequestState) __option.Option[string] { return _fromNonZeroRequestID(s.RequestID) },
func(v string) RequestState { return RequestState{ RequestID: v } },
"RequestState.RequestID",
)
return RequestStatePrisms {
RequestID: _prismRequestID,
}
}
// ValueStateLenses provides lenses for accessing fields of ValueState
type ValueStateLenses struct {
// mandatory fields
@@ -678,6 +942,13 @@ type ValueStateRefLenses struct {
Value __lens.Lens[*ValueState, int]
// optional fields
ValueO __lens_option.LensO[*ValueState, int]
// prisms
ValueP __prism.Prism[*ValueState, int]
}
// ValueStatePrisms provides prisms for accessing fields of ValueState
type ValueStatePrisms struct {
Value __prism.Prism[ValueState, int]
}
// MakeValueStateLenses creates a new ValueStateLenses with lenses for all fields
@@ -716,6 +987,19 @@ func MakeValueStateRefLenses() ValueStateRefLenses {
}
}
// MakeValueStatePrisms creates a new ValueStatePrisms with prisms for all fields
func MakeValueStatePrisms() ValueStatePrisms {
_fromNonZeroValue := __option.FromNonZero[int]()
_prismValue := __prism.MakePrismWithName(
func(s ValueState) __option.Option[int] { return _fromNonZeroValue(s.Value) },
func(v int) ValueState { return ValueState{ Value: v } },
"ValueState.Value",
)
return ValueStatePrisms {
Value: _prismValue,
}
}
// ResultStateLenses provides lenses for accessing fields of ResultState
type ResultStateLenses struct {
// mandatory fields
@@ -730,6 +1014,13 @@ type ResultStateRefLenses struct {
Result __lens.Lens[*ResultState, string]
// optional fields
ResultO __lens_option.LensO[*ResultState, string]
// prisms
ResultP __prism.Prism[*ResultState, string]
}
// ResultStatePrisms provides prisms for accessing fields of ResultState
type ResultStatePrisms struct {
Result __prism.Prism[ResultState, string]
}
// MakeResultStateLenses creates a new ResultStateLenses with lenses for all fields
@@ -768,6 +1059,19 @@ func MakeResultStateRefLenses() ResultStateRefLenses {
}
}
// MakeResultStatePrisms creates a new ResultStatePrisms with prisms for all fields
func MakeResultStatePrisms() ResultStatePrisms {
_fromNonZeroResult := __option.FromNonZero[string]()
_prismResult := __prism.MakePrismWithName(
func(s ResultState) __option.Option[string] { return _fromNonZeroResult(s.Result) },
func(v string) ResultState { return ResultState{ Result: v } },
"ResultState.Result",
)
return ResultStatePrisms {
Result: _prismResult,
}
}
// EnvStateLenses provides lenses for accessing fields of EnvState
type EnvStateLenses struct {
// mandatory fields
@@ -782,6 +1086,13 @@ type EnvStateRefLenses struct {
Environment __lens.Lens[*EnvState, string]
// optional fields
EnvironmentO __lens_option.LensO[*EnvState, string]
// prisms
EnvironmentP __prism.Prism[*EnvState, string]
}
// EnvStatePrisms provides prisms for accessing fields of EnvState
type EnvStatePrisms struct {
Environment __prism.Prism[EnvState, string]
}
// MakeEnvStateLenses creates a new EnvStateLenses with lenses for all fields
@@ -820,6 +1131,19 @@ func MakeEnvStateRefLenses() EnvStateRefLenses {
}
}
// MakeEnvStatePrisms creates a new EnvStatePrisms with prisms for all fields
func MakeEnvStatePrisms() EnvStatePrisms {
_fromNonZeroEnvironment := __option.FromNonZero[string]()
_prismEnvironment := __prism.MakePrismWithName(
func(s EnvState) __option.Option[string] { return _fromNonZeroEnvironment(s.Environment) },
func(v string) EnvState { return EnvState{ Environment: v } },
"EnvState.Environment",
)
return EnvStatePrisms {
Environment: _prismEnvironment,
}
}
// ScoreStateLenses provides lenses for accessing fields of ScoreState
type ScoreStateLenses struct {
// mandatory fields
@@ -834,6 +1158,13 @@ type ScoreStateRefLenses struct {
Score __lens.Lens[*ScoreState, int]
// optional fields
ScoreO __lens_option.LensO[*ScoreState, int]
// prisms
ScoreP __prism.Prism[*ScoreState, int]
}
// ScoreStatePrisms provides prisms for accessing fields of ScoreState
type ScoreStatePrisms struct {
Score __prism.Prism[ScoreState, int]
}
// MakeScoreStateLenses creates a new ScoreStateLenses with lenses for all fields
@@ -872,6 +1203,19 @@ func MakeScoreStateRefLenses() ScoreStateRefLenses {
}
}
// MakeScoreStatePrisms creates a new ScoreStatePrisms with prisms for all fields
func MakeScoreStatePrisms() ScoreStatePrisms {
_fromNonZeroScore := __option.FromNonZero[int]()
_prismScore := __prism.MakePrismWithName(
func(s ScoreState) __option.Option[int] { return _fromNonZeroScore(s.Score) },
func(v int) ScoreState { return ScoreState{ Score: v } },
"ScoreState.Score",
)
return ScoreStatePrisms {
Score: _prismScore,
}
}
// MessageStateLenses provides lenses for accessing fields of MessageState
type MessageStateLenses struct {
// mandatory fields
@@ -886,6 +1230,13 @@ type MessageStateRefLenses struct {
Message __lens.Lens[*MessageState, string]
// optional fields
MessageO __lens_option.LensO[*MessageState, string]
// prisms
MessageP __prism.Prism[*MessageState, string]
}
// MessageStatePrisms provides prisms for accessing fields of MessageState
type MessageStatePrisms struct {
Message __prism.Prism[MessageState, string]
}
// MakeMessageStateLenses creates a new MessageStateLenses with lenses for all fields
@@ -924,24 +1275,46 @@ func MakeMessageStateRefLenses() MessageStateRefLenses {
}
}
// MakeMessageStatePrisms creates a new MessageStatePrisms with prisms for all fields
func MakeMessageStatePrisms() MessageStatePrisms {
_fromNonZeroMessage := __option.FromNonZero[string]()
_prismMessage := __prism.MakePrismWithName(
func(s MessageState) __option.Option[string] { return _fromNonZeroMessage(s.Message) },
func(v string) MessageState { return MessageState{ Message: v } },
"MessageState.Message",
)
return MessageStatePrisms {
Message: _prismMessage,
}
}
// UserLenses provides lenses for accessing fields of User
type UserLenses struct {
// mandatory fields
ID __lens.Lens[User, int]
ID __lens.Lens[User, int]
Name __lens.Lens[User, string]
// optional fields
IDO __lens_option.LensO[User, int]
IDO __lens_option.LensO[User, int]
NameO __lens_option.LensO[User, string]
}
// UserRefLenses provides lenses for accessing fields of User via a reference to User
type UserRefLenses struct {
// mandatory fields
ID __lens.Lens[*User, int]
ID __lens.Lens[*User, int]
Name __lens.Lens[*User, string]
// optional fields
IDO __lens_option.LensO[*User, int]
IDO __lens_option.LensO[*User, int]
NameO __lens_option.LensO[*User, string]
// prisms
IDP __prism.Prism[*User, int]
NameP __prism.Prism[*User, string]
}
// UserPrisms provides prisms for accessing fields of User
type UserPrisms struct {
ID __prism.Prism[User, int]
Name __prism.Prism[User, string]
}
// MakeUserLenses creates a new UserLenses with lenses for all fields
@@ -962,10 +1335,10 @@ func MakeUserLenses() UserLenses {
lensNameO := __lens_option.FromIso[User](__iso_option.FromZero[string]())(lensName)
return UserLenses{
// mandatory lenses
ID: lensID,
ID: lensID,
Name: lensName,
// optional lenses
IDO: lensIDO,
IDO: lensIDO,
NameO: lensNameO,
}
}
@@ -988,32 +1361,61 @@ func MakeUserRefLenses() UserRefLenses {
lensNameO := __lens_option.FromIso[*User](__iso_option.FromZero[string]())(lensName)
return UserRefLenses{
// mandatory lenses
ID: lensID,
ID: lensID,
Name: lensName,
// optional lenses
IDO: lensIDO,
IDO: lensIDO,
NameO: lensNameO,
}
}
// MakeUserPrisms creates a new UserPrisms with prisms for all fields
func MakeUserPrisms() UserPrisms {
_fromNonZeroID := __option.FromNonZero[int]()
_prismID := __prism.MakePrismWithName(
func(s User) __option.Option[int] { return _fromNonZeroID(s.ID) },
func(v int) User { return User{ ID: v } },
"User.ID",
)
_fromNonZeroName := __option.FromNonZero[string]()
_prismName := __prism.MakePrismWithName(
func(s User) __option.Option[string] { return _fromNonZeroName(s.Name) },
func(v string) User { return User{ Name: v } },
"User.Name",
)
return UserPrisms {
ID: _prismID,
Name: _prismName,
}
}
// ConfigLenses provides lenses for accessing fields of Config
type ConfigLenses struct {
// mandatory fields
Port __lens.Lens[Config, int]
Port __lens.Lens[Config, int]
DatabaseURL __lens.Lens[Config, string]
// optional fields
PortO __lens_option.LensO[Config, int]
PortO __lens_option.LensO[Config, int]
DatabaseURLO __lens_option.LensO[Config, string]
}
// ConfigRefLenses provides lenses for accessing fields of Config via a reference to Config
type ConfigRefLenses struct {
// mandatory fields
Port __lens.Lens[*Config, int]
Port __lens.Lens[*Config, int]
DatabaseURL __lens.Lens[*Config, string]
// optional fields
PortO __lens_option.LensO[*Config, int]
PortO __lens_option.LensO[*Config, int]
DatabaseURLO __lens_option.LensO[*Config, string]
// prisms
PortP __prism.Prism[*Config, int]
DatabaseURLP __prism.Prism[*Config, string]
}
// ConfigPrisms provides prisms for accessing fields of Config
type ConfigPrisms struct {
Port __prism.Prism[Config, int]
DatabaseURL __prism.Prism[Config, string]
}
// MakeConfigLenses creates a new ConfigLenses with lenses for all fields
@@ -1034,10 +1436,10 @@ func MakeConfigLenses() ConfigLenses {
lensDatabaseURLO := __lens_option.FromIso[Config](__iso_option.FromZero[string]())(lensDatabaseURL)
return ConfigLenses{
// mandatory lenses
Port: lensPort,
Port: lensPort,
DatabaseURL: lensDatabaseURL,
// optional lenses
PortO: lensPortO,
PortO: lensPortO,
DatabaseURLO: lensDatabaseURLO,
}
}
@@ -1060,10 +1462,30 @@ func MakeConfigRefLenses() ConfigRefLenses {
lensDatabaseURLO := __lens_option.FromIso[*Config](__iso_option.FromZero[string]())(lensDatabaseURL)
return ConfigRefLenses{
// mandatory lenses
Port: lensPort,
Port: lensPort,
DatabaseURL: lensDatabaseURL,
// optional lenses
PortO: lensPortO,
PortO: lensPortO,
DatabaseURLO: lensDatabaseURLO,
}
}
// MakeConfigPrisms creates a new ConfigPrisms with prisms for all fields
func MakeConfigPrisms() ConfigPrisms {
_fromNonZeroPort := __option.FromNonZero[int]()
_prismPort := __prism.MakePrismWithName(
func(s Config) __option.Option[int] { return _fromNonZeroPort(s.Port) },
func(v int) Config { return Config{ Port: v } },
"Config.Port",
)
_fromNonZeroDatabaseURL := __option.FromNonZero[string]()
_prismDatabaseURL := __prism.MakePrismWithName(
func(s Config) __option.Option[string] { return _fromNonZeroDatabaseURL(s.DatabaseURL) },
func(v string) Config { return Config{ DatabaseURL: v } },
"Config.DatabaseURL",
)
return ConfigPrisms {
Port: _prismPort,
DatabaseURL: _prismDatabaseURL,
}
}

View File

@@ -20,6 +20,8 @@ import (
"sync"
"time"
RS "github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/option"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
@@ -135,6 +137,20 @@ func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
return RR.FromReader(r)
}
//go:inline
func FromReaderResult[A any](r RS.ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) (A, error) {
return either.Unwrap(r(ctx))
}
}
//go:inline
func ToReaderResult[A any](r ReaderResult[A]) RS.ReaderResult[A] {
return func(ctx context.Context) Result[A] {
return either.TryCatchError(r(ctx))
}
}
// MonadMap transforms the success value of a ReaderResult using the given function.
//
// If the ReaderResult fails, the error is propagated unchanged. This is the

View File

@@ -0,0 +1,146 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache LicensVersion 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
RS "github.com/IBM/fp-go/v2/context/readerresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
R "github.com/IBM/fp-go/v2/retry"
)
// Retrying retries a ReaderResult computation according to a retry policy with context awareness.
//
// This is the idiomatic wrapper around the functional [github.com/IBM/fp-go/v2/context/readerresult.Retrying]
// function. It provides a more Go-friendly API by working with (value, error) tuples instead of Result types.
//
// The function implements a retry mechanism for operations that depend on a [context.Context] and can fail.
// It respects context cancellation, meaning that if the context is cancelled during retry delays, the
// operation will stop immediately and return the cancellation error.
//
// The retry loop will continue until one of the following occurs:
// - The action succeeds and the check function returns false (no retry needed)
// - The retry policy returns None (retry limit reached)
// - The check function returns false (indicating success or a non-retryable failure)
// - The context is cancelled (returns context.Canceled or context.DeadlineExceeded)
//
// Parameters:
//
// - policy: A RetryPolicy that determines when and how long to wait between retries.
// The policy receives a RetryStatus on each iteration and returns an optional delay.
// If it returns None, retrying stops. Common policies include LimitRetries,
// ExponentialBackoff, and CapDelay from the retry package.
//
// - action: A Kleisli arrow that takes a RetryStatus and returns a ReaderResult[A].
// This function is called on each retry attempt and receives information about the
// current retry state (iteration number, cumulative delay, etc.). The action depends
// on a context.Context and produces (A, error). The context passed to the action
// will be the same context used for retry delays, so cancellation is properly propagated.
//
// - check: A predicate function that examines the result value and error, returning true
// if the operation should be retried, or false if it should stop. This allows you to
// distinguish between retryable failures (e.g., network timeouts) and permanent
// failures (e.g., invalid input). The function receives both the value and error from
// the action's result. Note that context cancellation errors will automatically stop
// retrying regardless of this function's return value.
//
// Returns:
//
// A ReaderResult[A] that, when executed with a context, will perform the retry
// logic with context cancellation support and return the final (value, error) tuple.
//
// Type Parameters:
// - A: The type of the success value
//
// Context Cancellation:
//
// The retry mechanism respects context cancellation in two ways:
// 1. During retry delays: If the context is cancelled while waiting between retries,
// the operation stops immediately and returns the context error.
// 2. During action execution: If the action itself checks the context and returns
// an error due to cancellation, the retry loop will stop (assuming the check
// function doesn't force a retry on context errors).
//
// Implementation Details:
//
// This function wraps the functional [github.com/IBM/fp-go/v2/context/readerresult.Retrying]
// by converting between the idiomatic (value, error) tuple representation and the functional
// Result[A] representation. The conversion is handled by ToReaderResult and FromReaderResult,
// ensuring seamless integration with the underlying retry mechanism that uses delayWithCancel
// to properly handle context cancellation during delays.
//
// Example:
//
// // Create a retry policy: exponential backoff with a cap, limited to 5 retries
// policy := retry.Monoid.Concat(
// retry.LimitRetries(5),
// retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
// )
//
// // Action that fetches data, with retry status information
// fetchData := func(status retry.RetryStatus) ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// // Check if context is cancelled
// if ctx.Err() != nil {
// return "", ctx.Err()
// }
// // Simulate an HTTP request that might fail
// if status.IterNumber < 3 {
// return "", fmt.Errorf("temporary error")
// }
// return "success", nil
// }
// }
//
// // Check function: retry on any error except context cancellation
// shouldRetry := func(val string, err error) bool {
// return err != nil && !errors.Is(err, context.Canceled)
// }
//
// // Create the retrying computation
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a cancellable context
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// result, err := retryingFetch(ctx)
//
// See also:
// - retry.RetryPolicy for available retry policies
// - retry.RetryStatus for information passed to the action
// - context.Context for context cancellation semantics
// - github.com/IBM/fp-go/v2/context/readerresult.Retrying for the underlying functional implementation
//
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(A, error) bool,
) ReaderResult[A] {
return F.Pipe1(
RS.Retrying(
policy,
F.Flow2(
action,
ToReaderResult,
),
func(a Result[A]) bool {
return check(result.Unwrap(a))
},
),
FromReaderResult,
)
}

View File

@@ -0,0 +1,482 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
// Helper function to create a test retry policy
func testRetryPolicy() R.RetryPolicy {
return R.Monoid.Concat(
R.LimitRetries(5),
R.CapDelay(1*time.Second, R.ExponentialBackoff(10*time.Millisecond)),
)
}
// TestRetrying_SuccessOnFirstAttempt tests that Retrying succeeds immediately
// when the action succeeds on the first attempt.
func TestRetrying_SuccessOnFirstAttempt(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "success", nil
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
assert.NoError(t, err)
assert.Equal(t, "success", result)
}
// TestRetrying_SuccessAfterRetries tests that Retrying eventually succeeds
// after a few failed attempts.
func TestRetrying_SuccessAfterRetries(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
// Fail on first 3 attempts, succeed on 4th
if status.IterNumber < 3 {
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
return fmt.Sprintf("success on attempt %d", status.IterNumber), nil
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
assert.NoError(t, err)
assert.Equal(t, "success on attempt 3", result)
}
// TestRetrying_ExhaustsRetries tests that Retrying stops after the retry limit
// is reached and returns the last error.
func TestRetrying_ExhaustsRetries(t *testing.T) {
policy := R.LimitRetries(3)
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
assert.Error(t, err)
assert.Equal(t, "", result)
assert.Equal(t, "attempt 3 failed", err.Error())
}
// TestRetrying_ActionChecksContextCancellation tests that actions can check
// the context and return early if it's cancelled.
func TestRetrying_ActionChecksContextCancellation(t *testing.T) {
policy := R.LimitRetries(10)
attemptCount := 0
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
attemptCount++
// Check context at the start of the action
if ctx.Err() != nil {
return "", ctx.Err()
}
// Simulate work that might take time
time.Sleep(10 * time.Millisecond)
// Check context again after work
if ctx.Err() != nil {
return "", ctx.Err()
}
// Always fail to trigger retries
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
check := func(val string, err error) bool {
// Don't retry on context errors
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return false
}
return err != nil
}
retrying := Retrying(policy, action, check)
// Create a context that we'll cancel after a short time
ctx, cancel := context.WithCancel(context.Background())
// Start the retry operation in a goroutine
type result struct {
val string
err error
}
resultChan := make(chan result, 1)
go func() {
val, err := retrying(ctx)
resultChan <- result{val, err}
}()
// Cancel the context after allowing a couple attempts
time.Sleep(50 * time.Millisecond)
cancel()
// Wait for the result
res := <-resultChan
// Should have stopped due to context cancellation
assert.Error(t, res.err)
// Should have stopped early (not all 10 attempts)
assert.Less(t, attemptCount, 10, "Should stop retrying when action detects context cancellation")
}
// TestRetrying_ContextCancelledBeforeStart tests that if the context is already
// cancelled before starting, the operation fails immediately.
func TestRetrying_ContextCancelledBeforeStart(t *testing.T) {
policy := testRetryPolicy()
attemptCount := 0
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
attemptCount++
// Check context before doing work
if ctx.Err() != nil {
return "", ctx.Err()
}
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
check := func(val string, err error) bool {
// Don't retry on context errors
if err != nil && errors.Is(err, context.Canceled) {
return false
}
return err != nil
}
retrying := Retrying(policy, action, check)
// Create an already-cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := retrying(ctx)
assert.Error(t, err)
assert.True(t, errors.Is(err, context.Canceled))
// Should have attempted at most once
assert.LessOrEqual(t, attemptCount, 1)
}
// TestRetrying_ContextTimeoutInAction tests that actions respect context deadlines.
func TestRetrying_ContextTimeoutInAction(t *testing.T) {
policy := R.LimitRetries(10)
attemptCount := 0
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
attemptCount++
// Check context before doing work
if ctx.Err() != nil {
return "", ctx.Err()
}
// Simulate some work
time.Sleep(50 * time.Millisecond)
// Check context after work
if ctx.Err() != nil {
return "", ctx.Err()
}
// Always fail to trigger retries
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
check := func(val string, err error) bool {
// Don't retry on context errors
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return false
}
return err != nil
}
retrying := Retrying(policy, action, check)
// Create a context with a short timeout
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
startTime := time.Now()
_, err := retrying(ctx)
elapsed := time.Since(startTime)
assert.Error(t, err)
// Should have stopped before completing all 10 retries
assert.Less(t, attemptCount, 10, "Should stop retrying when action detects context timeout")
// Should have stopped around the timeout duration
assert.Less(t, elapsed, 500*time.Millisecond, "Should stop soon after timeout")
}
// TestRetrying_CheckFunctionStopsRetry tests that the check function can
// stop retrying even when errors occur.
func TestRetrying_CheckFunctionStopsRetry(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
if status.IterNumber == 0 {
return "", fmt.Errorf("retryable error")
}
return "", fmt.Errorf("permanent error")
}
}
// Only retry on "retryable error"
check := func(val string, err error) bool {
return err != nil && err.Error() == "retryable error"
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
_, err := retrying(ctx)
assert.Error(t, err)
assert.Equal(t, "permanent error", err.Error())
}
// TestRetrying_ExponentialBackoff tests that exponential backoff is applied.
func TestRetrying_ExponentialBackoff(t *testing.T) {
// Use a policy with measurable delays
policy := R.Monoid.Concat(
R.LimitRetries(3),
R.ExponentialBackoff(50*time.Millisecond),
)
startTime := time.Now()
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
if status.IterNumber < 2 {
return "", fmt.Errorf("retry")
}
return "success", nil
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
elapsed := time.Since(startTime)
assert.NoError(t, err)
assert.Equal(t, "success", result)
// With exponential backoff starting at 50ms:
// Iteration 0: no delay
// Iteration 1: 50ms delay
// Iteration 2: 100ms delay
// Total should be at least 150ms
assert.GreaterOrEqual(t, elapsed, 150*time.Millisecond)
}
// TestRetrying_ContextValuePropagation tests that context values are properly
// propagated through the retry mechanism.
func TestRetrying_ContextValuePropagation(t *testing.T) {
policy := R.LimitRetries(2)
type contextKey string
const requestIDKey contextKey = "requestID"
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
// Extract value from context
requestID, ok := ctx.Value(requestIDKey).(string)
if !ok {
return "", fmt.Errorf("missing request ID")
}
if status.IterNumber < 1 {
return "", fmt.Errorf("retry needed")
}
return fmt.Sprintf("processed request %s", requestID), nil
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
// Create context with a value
ctx := context.WithValue(context.Background(), requestIDKey, "12345")
result, err := retrying(ctx)
assert.NoError(t, err)
assert.Equal(t, "processed request 12345", result)
}
// TestRetrying_RetryStatusProgression tests that the RetryStatus is properly
// updated on each iteration.
func TestRetrying_RetryStatusProgression(t *testing.T) {
policy := testRetryPolicy()
var iterations []uint
action := func(status R.RetryStatus) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
iterations = append(iterations, status.IterNumber)
if status.IterNumber < 3 {
return 0, fmt.Errorf("retry")
}
return int(status.IterNumber), nil
}
}
check := func(val int, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
assert.NoError(t, err)
assert.Equal(t, 3, result)
// Should have attempted iterations 0, 1, 2, 3
assert.Equal(t, []uint{0, 1, 2, 3}, iterations)
}
// TestRetrying_ContextCancelledDuringDelay tests that the retry operation
// stops immediately when the context is cancelled during a retry delay,
// even if there are still retries remaining according to the policy.
func TestRetrying_ContextCancelledDuringDelay(t *testing.T) {
// Use a policy with significant delays to ensure we can cancel during the delay
policy := R.Monoid.Concat(
R.LimitRetries(10),
R.ConstantDelay(200*time.Millisecond),
)
attemptCount := 0
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
attemptCount++
// Always fail to trigger retries
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
// Always retry on errors (don't check for context cancellation in check function)
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
// Create a context that we'll cancel during the retry delay
ctx, cancel := context.WithCancel(context.Background())
// Start the retry operation in a goroutine
type result struct {
val string
err error
}
resultChan := make(chan result, 1)
startTime := time.Now()
go func() {
val, err := retrying(ctx)
resultChan <- result{val, err}
}()
// Wait for the first attempt to complete and the delay to start
time.Sleep(50 * time.Millisecond)
// Cancel the context during the retry delay
cancel()
// Wait for the result
res := <-resultChan
elapsed := time.Since(startTime)
// Should have stopped due to context cancellation
assert.Error(t, res.err)
// Should have attempted only once or twice (not all 10 attempts)
// because the context was cancelled during the delay
assert.LessOrEqual(t, attemptCount, 2, "Should stop retrying when context is cancelled during delay")
// Should have stopped quickly after cancellation, not waiting for all delays
// With 10 retries and 200ms delays, it would take ~2 seconds without cancellation
// With cancellation during first delay, it should complete in well under 500ms
assert.Less(t, elapsed, 500*time.Millisecond, "Should stop immediately when context is cancelled during delay")
// When context is cancelled during the delay, the retry mechanism
// detects the cancellation and returns a context error
assert.True(t, errors.Is(res.err, context.Canceled), "Should return context.Canceled when cancelled during delay")
}

View File

@@ -22,6 +22,8 @@ import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
@@ -46,12 +48,24 @@ type (
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
// ReaderResult represents a computation that depends on a context.Context and produces either a value of type A or an error.
// It combines the Reader pattern with Result (error handling), making it suitable for context-aware operations that may fail.
ReaderResult[A any] = func(context.Context) (A, error)
// Monoid represents a monoid structure for ReaderResult values.
Monoid[A any] = monoid.Monoid[ReaderResult[A]]
// Kleisli represents a Kleisli arrow from A to ReaderResult[B].
// It's a function that takes a value of type A and returns a computation that produces B or an error in a context.
Kleisli[A, B any] = Reader[A, ReaderResult[B]]
// Operator represents a Kleisli arrow that operates on ReaderResult values.
// It transforms a ReaderResult[A] into a ReaderResult[B], useful for composing context-aware operations.
Operator[A, B any] = Kleisli[ReaderResult[A], B]
// Lens represents an optic that focuses on a field of type A within a structure of type S.
Lens[S, A any] = lens.Lens[S, A]
// Prism represents an optic that focuses on a case of type A within a sum type S.
Prism[S, A any] = prism.Prism[S, A]
)

View File

@@ -0,0 +1,78 @@
package formatting
import (
"fmt"
"log/slog"
)
type (
// Formattable is a composite interface that combines multiple formatting capabilities
// from the Go standard library. Types implementing this interface can be formatted
// in various contexts including string conversion, custom formatting, Go syntax
// representation, and structured logging.
//
// This interface is particularly useful for types that need to provide consistent
// formatting across different output contexts, such as logging, debugging, and
// user-facing displays.
//
// Embedded Interfaces:
//
// - fmt.Stringer: Provides String() string method for basic string representation
// - fmt.Formatter: Provides Format(f fmt.State, verb rune) for custom formatting with verbs like %v, %s, %+v, etc.
// - fmt.GoStringer: Provides GoString() string method for Go-syntax representation (used with %#v)
// - slog.LogValuer: Provides LogValue() slog.Value for structured logging with the slog package
//
// Example Implementation:
//
// type User struct {
// ID int
// Name string
// }
//
// // String provides a simple string representation
// func (u User) String() string {
// return fmt.Sprintf("User(%s)", u.Name)
// }
//
// // Format provides custom formatting based on the verb
// func (u User) Format(f fmt.State, verb rune) {
// switch verb {
// case 'v':
// if f.Flag('+') {
// fmt.Fprintf(f, "User{ID: %d, Name: %s}", u.ID, u.Name)
// } else {
// fmt.Fprint(f, u.String())
// }
// case 's':
// fmt.Fprint(f, u.String())
// }
// }
//
// // GoString provides Go-syntax representation
// func (u User) GoString() string {
// return fmt.Sprintf("User{ID: %d, Name: %q}", u.ID, u.Name)
// }
//
// // LogValue provides structured logging representation
// func (u User) LogValue() slog.Value {
// return slog.GroupValue(
// slog.Int("id", u.ID),
// slog.String("name", u.Name),
// )
// }
//
// Usage:
//
// user := User{ID: 1, Name: "Alice"}
// fmt.Println(user) // Output: User(Alice)
// fmt.Printf("%+v\n", user) // Output: User{ID: 1, Name: Alice}
// fmt.Printf("%#v\n", user) // Output: User{ID: 1, Name: "Alice"}
// slog.Info("user", "user", user) // Structured log with id and name fields
Formattable interface {
fmt.Stringer
fmt.Formatter
fmt.GoStringer
slog.LogValuer
}
)

View File

@@ -0,0 +1,123 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package formatting
import (
"fmt"
"reflect"
"strings"
)
// FmtString implements the fmt.Formatter interface for Formattable types.
// It handles various format verbs to provide consistent string formatting
// across different output contexts.
//
// Supported format verbs:
// - %v: Uses String() representation (default format)
// - %+v: Uses String() representation (verbose format)
// - %#v: Uses GoString() representation (Go-syntax format)
// - %s: Uses String() representation (string format)
// - %q: Uses quoted String() representation (quoted string format)
// - default: Uses String() representation for any other verb
//
// The function delegates to the appropriate method of the Formattable interface
// based on the format verb and flags provided by fmt.State.
//
// Parameters:
// - stg: The Formattable value to format
// - f: The fmt.State that provides formatting context and flags
// - c: The format verb (rune) being used
//
// Example usage:
//
// type MyType struct {
// value int
// }
//
// func (m MyType) Format(f fmt.State, verb rune) {
// formatting.FmtString(m, f, verb)
// }
//
// func (m MyType) String() string {
// return fmt.Sprintf("MyType(%d)", m.value)
// }
//
// func (m MyType) GoString() string {
// return fmt.Sprintf("MyType{value: %d}", m.value)
// }
//
// // Usage:
// mt := MyType{value: 42}
// fmt.Printf("%v\n", mt) // Output: MyType(42)
// fmt.Printf("%#v\n", mt) // Output: MyType{value: 42}
// fmt.Printf("%s\n", mt) // Output: MyType(42)
// fmt.Printf("%q\n", mt) // Output: "MyType(42)"
func FmtString(stg Formattable, f fmt.State, c rune) {
switch c {
case 'v':
if f.Flag('#') {
// %#v uses GoString representation
fmt.Fprint(f, stg.GoString())
} else {
// %v and %+v use String representation
fmt.Fprint(f, stg.String())
}
case 's':
fmt.Fprint(f, stg.String())
case 'q':
fmt.Fprintf(f, "%q", stg.String())
default:
fmt.Fprint(f, stg.String())
}
}
// TypeInfo returns a string representation of the type of the given value.
// It uses reflection to determine the type and removes the leading asterisk (*)
// from pointer types to provide a cleaner type name.
//
// This function is useful for generating human-readable type information in
// string representations, particularly for generic types where the concrete
// type needs to be displayed.
//
// Parameters:
// - v: The value whose type information should be extracted
//
// Returns:
// - A string representing the type name, with pointer prefix removed
//
// Example usage:
//
// // For non-pointer types
// TypeInfo(42) // Returns: "int"
// TypeInfo("hello") // Returns: "string"
// TypeInfo([]int{1, 2, 3}) // Returns: "[]int"
//
// // For pointer types (asterisk is removed)
// var ptr *int
// TypeInfo(ptr) // Returns: "int" (not "*int")
//
// // For custom types
// type MyStruct struct{ Name string }
// TypeInfo(MyStruct{}) // Returns: "formatting.MyStruct"
// TypeInfo(&MyStruct{}) // Returns: "formatting.MyStruct" (not "*formatting.MyStruct")
//
// // For interface types
// var err error = fmt.Errorf("test")
// TypeInfo(err) // Returns: "errors.errorString"
func TypeInfo(v any) string {
// Remove the leading * from pointer type
return strings.TrimPrefix(reflect.TypeOf(v).String(), "*")
}

View File

@@ -0,0 +1,369 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package formatting
import (
"fmt"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
)
// mockFormattable is a test implementation of the Formattable interface
type mockFormattable struct {
stringValue string
goStringValue string
}
func (m mockFormattable) String() string {
return m.stringValue
}
func (m mockFormattable) GoString() string {
return m.goStringValue
}
func (m mockFormattable) Format(f fmt.State, verb rune) {
FmtString(m, f, verb)
}
func (m mockFormattable) LogValue() slog.Value {
return slog.StringValue(m.stringValue)
}
func TestFmtString(t *testing.T) {
t.Run("format with %v verb", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test value",
goStringValue: "test.GoString",
}
result := fmt.Sprintf("%v", mock)
assert.Equal(t, "test value", result, "Should use String() for %v")
})
t.Run("format with %+v verb", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test value",
goStringValue: "test.GoString",
}
result := fmt.Sprintf("%+v", mock)
assert.Equal(t, "test value", result, "Should use String() for %+v")
})
t.Run("format with %#v verb", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test value",
goStringValue: "test.GoString",
}
result := fmt.Sprintf("%#v", mock)
assert.Equal(t, "test.GoString", result, "Should use GoString() for %#v")
})
t.Run("format with %s verb", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test value",
goStringValue: "test.GoString",
}
result := fmt.Sprintf("%s", mock)
assert.Equal(t, "test value", result, "Should use String() for %s")
})
t.Run("format with %q verb", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test value",
goStringValue: "test.GoString",
}
result := fmt.Sprintf("%q", mock)
assert.Equal(t, `"test value"`, result, "Should use quoted String() for %q")
})
t.Run("format with unsupported verb", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test value",
goStringValue: "test.GoString",
}
// Using %d which is not a typical string verb
result := fmt.Sprintf("%d", mock)
assert.Equal(t, "test value", result, "Should use String() for unsupported verbs")
})
t.Run("format with special characters in string", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test\nvalue\twith\rspecial",
goStringValue: "test.GoString",
}
result := fmt.Sprintf("%s", mock)
assert.Equal(t, "test\nvalue\twith\rspecial", result)
})
t.Run("format with empty string", func(t *testing.T) {
mock := mockFormattable{
stringValue: "",
goStringValue: "",
}
result := fmt.Sprintf("%s", mock)
assert.Equal(t, "", result)
})
t.Run("format with unicode characters", func(t *testing.T) {
mock := mockFormattable{
stringValue: "Hello 世界 🌍",
goStringValue: "test.GoString",
}
result := fmt.Sprintf("%s", mock)
assert.Equal(t, "Hello 世界 🌍", result)
})
t.Run("format with %q and special characters", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test\nvalue",
goStringValue: "test.GoString",
}
result := fmt.Sprintf("%q", mock)
assert.Equal(t, `"test\nvalue"`, result, "Should properly escape special characters in quoted format")
})
}
func TestTypeInfo(t *testing.T) {
t.Run("basic types", func(t *testing.T) {
tests := []struct {
name string
value any
expected string
}{
{"int", 42, "int"},
{"string", "hello", "string"},
{"bool", true, "bool"},
{"float64", 3.14, "float64"},
{"float32", float32(3.14), "float32"},
{"int64", int64(42), "int64"},
{"int32", int32(42), "int32"},
{"uint", uint(42), "uint"},
{"byte", byte(42), "uint8"},
{"rune", rune('a'), "int32"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := TypeInfo(tt.value)
assert.Equal(t, tt.expected, result)
})
}
})
t.Run("pointer types", func(t *testing.T) {
var intPtr *int
result := TypeInfo(intPtr)
assert.Equal(t, "int", result, "Should remove leading * from pointer type")
var strPtr *string
result = TypeInfo(strPtr)
assert.Equal(t, "string", result, "Should remove leading * from pointer type")
})
t.Run("slice types", func(t *testing.T) {
result := TypeInfo([]int{1, 2, 3})
assert.Equal(t, "[]int", result)
result = TypeInfo([]string{"a", "b"})
assert.Equal(t, "[]string", result)
result = TypeInfo([][]int{{1, 2}, {3, 4}})
assert.Equal(t, "[][]int", result)
})
t.Run("map types", func(t *testing.T) {
result := TypeInfo(map[string]int{"a": 1})
assert.Equal(t, "map[string]int", result)
result = TypeInfo(map[int]string{1: "a"})
assert.Equal(t, "map[int]string", result)
})
t.Run("struct types", func(t *testing.T) {
type TestStruct struct {
Name string
Age int
}
result := TypeInfo(TestStruct{})
assert.Equal(t, "formatting.TestStruct", result)
result = TypeInfo(&TestStruct{})
assert.Equal(t, "formatting.TestStruct", result, "Should remove leading * from pointer to struct")
})
t.Run("interface types", func(t *testing.T) {
var err error = fmt.Errorf("test error")
result := TypeInfo(err)
assert.Contains(t, result, "errors", "Should contain package name")
assert.NotContains(t, result, "*", "Should not contain pointer prefix")
})
t.Run("channel types", func(t *testing.T) {
ch := make(chan int)
result := TypeInfo(ch)
assert.Equal(t, "chan int", result)
ch2 := make(chan string, 10)
result = TypeInfo(ch2)
assert.Equal(t, "chan string", result)
})
t.Run("function types", func(t *testing.T) {
fn := func(int) string { return "" }
result := TypeInfo(fn)
assert.Equal(t, "func(int) string", result)
})
t.Run("array types", func(t *testing.T) {
arr := [3]int{1, 2, 3}
result := TypeInfo(arr)
assert.Equal(t, "[3]int", result)
})
t.Run("complex types", func(t *testing.T) {
type ComplexStruct struct {
Data map[string][]int
}
result := TypeInfo(ComplexStruct{})
assert.Equal(t, "formatting.ComplexStruct", result)
})
t.Run("nil pointer", func(t *testing.T) {
var ptr *int
result := TypeInfo(ptr)
assert.Equal(t, "int", result, "Should handle nil pointer correctly")
})
}
func TestTypeInfoWithCustomTypes(t *testing.T) {
t.Run("custom type with methods", func(t *testing.T) {
mock := mockFormattable{
stringValue: "test",
goStringValue: "test.GoString",
}
result := TypeInfo(mock)
assert.Equal(t, "formatting.mockFormattable", result)
})
t.Run("pointer to custom type", func(t *testing.T) {
mock := &mockFormattable{
stringValue: "test",
goStringValue: "test.GoString",
}
result := TypeInfo(mock)
assert.Equal(t, "formatting.mockFormattable", result, "Should remove pointer prefix")
})
}
func TestFmtStringIntegration(t *testing.T) {
t.Run("integration with fmt.Printf", func(t *testing.T) {
mock := mockFormattable{
stringValue: "integration test",
goStringValue: "mock.GoString",
}
// Test various format combinations
tests := []struct {
format string
expected string
}{
{"%v", "integration test"},
{"%+v", "integration test"},
{"%#v", "mock.GoString"},
{"%s", "integration test"},
{"%q", `"integration test"`},
}
for _, tt := range tests {
t.Run(tt.format, func(t *testing.T) {
result := fmt.Sprintf(tt.format, mock)
assert.Equal(t, tt.expected, result)
})
}
})
t.Run("integration with fmt.Fprintf", func(t *testing.T) {
mock := mockFormattable{
stringValue: "buffer test",
goStringValue: "mock.GoString",
}
var buf []byte
n, err := fmt.Fprintf((*mockWriter)(&buf), "%s", mock)
assert.NoError(t, err)
assert.Greater(t, n, 0)
assert.Equal(t, "buffer test", string(buf))
})
}
// mockWriter is a simple writer for testing fmt.Fprintf
type mockWriter []byte
func (m *mockWriter) Write(p []byte) (n int, err error) {
*m = append(*m, p...)
return len(p), nil
}
func BenchmarkFmtString(b *testing.B) {
mock := mockFormattable{
stringValue: "benchmark test value",
goStringValue: "mock.GoString",
}
b.Run("format with %v", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%v", mock)
}
})
b.Run("format with %#v", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%#v", mock)
}
})
b.Run("format with %s", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s", mock)
}
})
b.Run("format with %q", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%q", mock)
}
})
}
func BenchmarkTypeInfo(b *testing.B) {
values := []any{
42,
"string",
[]int{1, 2, 3},
map[string]int{"a": 1},
mockFormattable{},
}
for _, v := range values {
b.Run(fmt.Sprintf("%T", v), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = TypeInfo(v)
}
})
}
}

View File

@@ -17,15 +17,16 @@ package ioeither
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/tailrec"
)
// TailRec creates a tail-recursive computation in the IOEither monad.
// It enables writing recursive algorithms that don't overflow the call stack by using
// trampolining - a technique where recursive calls are converted into iterations.
//
// The function takes a step function that returns either:
// - Left(A): Continue recursion with a new value of type A
// - Right(B): Terminate recursion with a final result of type B
// The function takes a step function that returns a Trampoline:
// - Bounce(A): Continue recursion with a new value of type A
// - Land(B): Terminate recursion with a final result of type B
//
// This is particularly useful for implementing recursive algorithms like:
// - Iterative calculations (factorial, fibonacci, etc.)
@@ -34,7 +35,7 @@ import (
// - Processing collections with early termination
//
// The recursion is stack-safe because each step returns a value that indicates
// whether to continue (Left) or stop (Right), rather than making direct recursive calls.
// whether to continue (Bounce) or stop (Land), rather than making direct recursive calls.
//
// Type Parameters:
// - E: The error type that may occur during computation
@@ -43,7 +44,7 @@ import (
//
// Parameters:
// - f: A step function that takes the current state (A) and returns an IOEither
// containing either Left(A) to continue with a new state, or Right(B) to
// containing either Bounce(A) to continue with a new state, or Land(B) to
// terminate with a final result
//
// Returns:
@@ -57,13 +58,13 @@ import (
// result int
// }
//
// factorial := TailRec(func(state FactState) IOEither[error, Either[FactState, int]] {
// factorial := TailRec(func(state FactState) IOEither[error, tailrec.Trampoline[FactState, int]] {
// if state.n <= 1 {
// // Terminate with final result
// return Of[error](either.Right[FactState](state.result))
// return Of[error](tailrec.Land[FactState](state.result))
// }
// // Continue with next iteration
// return Of[error](either.Left[int](FactState{
// return Of[error](tailrec.Bounce[int](FactState{
// n: state.n - 1,
// result: state.result * state.n,
// }))
@@ -78,36 +79,35 @@ import (
// sum int
// }
//
// processItems := TailRec(func(state ProcessState) IOEither[error, Either[ProcessState, int]] {
// processItems := TailRec(func(state ProcessState) IOEither[error, tailrec.Trampoline[ProcessState, int]] {
// if len(state.items) == 0 {
// return Of[error](either.Right[ProcessState](state.sum))
// return Of[error](tailrec.Land[ProcessState](state.sum))
// }
// val, err := strconv.Atoi(state.items[0])
// if err != nil {
// return Left[Either[ProcessState, int]](err)
// return Left[tailrec.Trampoline[ProcessState, int]](err)
// }
// return Of[error](either.Left[int](ProcessState{
// return Of[error](tailrec.Bounce[int](ProcessState{
// items: state.items[1:],
// sum: state.sum + val,
// }))
// })
//
// result := processItems(ProcessState{items: []string{"1", "2", "3"}, sum: 0})() // Right(6)
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
return func(a A) IOEither[E, B] {
initial := f(a)
return func() Either[E, B] {
return func() either.Either[E, B] {
current := initial()
for {
r, e := either.Unwrap(current)
if either.IsLeft(current) {
return either.Left[B](e)
}
b, a := either.Unwrap(r)
if either.IsRight(r) {
return either.Right[E](b)
if r.Landed {
return either.Right[E](r.Land)
}
current = f(a)()
current = f(r.Bounce)()
}
}
}

View File

@@ -22,6 +22,7 @@ import (
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
TR "github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
@@ -32,13 +33,13 @@ func TestTailRecFactorial(t *testing.T) {
result int
}
factorial := TailRec(func(state FactState) IOEither[error, E.Either[FactState, int]] {
factorial := TailRec(func(state FactState) IOEither[error, TR.Trampoline[FactState, int]] {
if state.n <= 1 {
// Terminate with final result
return Of[error](E.Right[FactState](state.result))
return Of[error](TR.Land[FactState](state.result))
}
// Continue with next iteration
return Of[error](E.Left[int](FactState{
return Of[error](TR.Bounce[int](FactState{
n: state.n - 1,
result: state.result * state.n,
}))
@@ -73,11 +74,11 @@ func TestTailRecFibonacci(t *testing.T) {
curr int
}
fibonacci := TailRec(func(state FibState) IOEither[error, E.Either[FibState, int]] {
fibonacci := TailRec(func(state FibState) IOEither[error, TR.Trampoline[FibState, int]] {
if state.n == 0 {
return Of[error](E.Right[FibState](state.curr))
return Of[error](TR.Land[FibState](state.curr))
}
return Of[error](E.Left[int](FibState{
return Of[error](TR.Bounce[int](FibState{
n: state.n - 1,
prev: state.curr,
curr: state.prev + state.curr,
@@ -107,11 +108,11 @@ func TestTailRecSumList(t *testing.T) {
sum int
}
sumList := TailRec(func(state SumState) IOEither[error, E.Either[SumState, int]] {
sumList := TailRec(func(state SumState) IOEither[error, TR.Trampoline[SumState, int]] {
if A.IsEmpty(state.items) {
return Of[error](E.Right[SumState](state.sum))
return Of[error](TR.Land[SumState](state.sum))
}
return Of[error](E.Left[int](SumState{
return Of[error](TR.Bounce[int](SumState{
items: state.items[1:],
sum: state.sum + state.items[0],
}))
@@ -141,14 +142,14 @@ func TestTailRecWithError(t *testing.T) {
}
// Divide n by 2 repeatedly until it reaches 1, fail if we encounter an odd number > 1
divideByTwo := TailRec(func(state DivState) IOEither[error, E.Either[DivState, int]] {
divideByTwo := TailRec(func(state DivState) IOEither[error, TR.Trampoline[DivState, int]] {
if state.n == 1 {
return Of[error](E.Right[DivState](state.result))
return Of[error](TR.Land[DivState](state.result))
}
if state.n%2 != 0 {
return Left[E.Either[DivState, int]](fmt.Errorf("cannot divide odd number %d", state.n))
return Left[TR.Trampoline[DivState, int]](fmt.Errorf("cannot divide odd number %d", state.n))
}
return Of[error](E.Left[int](DivState{
return Of[error](TR.Bounce[int](DivState{
n: state.n / 2,
result: state.result + 1,
}))
@@ -183,11 +184,11 @@ func TestTailRecWithError(t *testing.T) {
// TestTailRecCountdown tests a simple countdown
func TestTailRecCountdown(t *testing.T) {
countdown := TailRec(func(n int) IOEither[error, E.Either[int, string]] {
countdown := TailRec(func(n int) IOEither[error, TR.Trampoline[int, string]] {
if n <= 0 {
return Of[error](E.Right[int]("Done!"))
return Of[error](TR.Land[int]("Done!"))
}
return Of[error](E.Left[string](n - 1))
return Of[error](TR.Bounce[string](n - 1))
})
t.Run("countdown from 5", func(t *testing.T) {
@@ -209,11 +210,11 @@ func TestTailRecCountdown(t *testing.T) {
// TestTailRecStackSafety tests that TailRec doesn't overflow the stack with large iterations
func TestTailRecStackSafety(t *testing.T) {
// Count down from a large number - this would overflow the stack with regular recursion
largeCountdown := TailRec(func(n int) IOEither[error, E.Either[int, int]] {
largeCountdown := TailRec(func(n int) IOEither[error, TR.Trampoline[int, int]] {
if n <= 0 {
return Of[error](E.Right[int](0))
return Of[error](TR.Land[int](0))
}
return Of[error](E.Left[int](n - 1))
return Of[error](TR.Bounce[int](n - 1))
})
t.Run("large iteration count", func(t *testing.T) {
@@ -231,14 +232,14 @@ func TestTailRecFindInList(t *testing.T) {
index int
}
findInList := TailRec(func(state FindState) IOEither[error, E.Either[FindState, int]] {
findInList := TailRec(func(state FindState) IOEither[error, TR.Trampoline[FindState, int]] {
if A.IsEmpty(state.items) {
return Left[E.Either[FindState, int]](errors.New("not found"))
return Left[TR.Trampoline[FindState, int]](errors.New("not found"))
}
if state.items[0] == state.target {
return Of[error](E.Right[FindState](state.index))
return Of[error](TR.Land[FindState](state.index))
}
return Of[error](E.Left[int](FindState{
return Of[error](TR.Bounce[int](FindState{
items: state.items[1:],
target: state.target,
index: state.index + 1,

View File

@@ -20,7 +20,6 @@ import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type [S] to be used with the [Bind] operation.
@@ -75,7 +74,7 @@ func Do[S any](
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Kleisli[IOOption[S1], S2] {
) Operator[S1, S2] {
return chain.Bind(
Chain[S1, S2],
Map[T, S2],
@@ -88,7 +87,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Kleisli[IOOption[S1], S2] {
) Operator[S1, S2] {
return functor.Let(
Map[S1, S2],
setter,
@@ -100,7 +99,7 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Kleisli[IOOption[S1], S2] {
) Operator[S1, S2] {
return functor.LetTo(
Map[S1, S2],
setter,
@@ -111,13 +110,20 @@ func LetTo[S1, S2, T any](
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any](
setter func(T) S1,
) Kleisli[IOOption[T], S1] {
) Operator[T, S1] {
return chain.BindTo(
Map[T, S1],
setter,
)
}
//go:inline
func BindToP[S1, T any](
setter Prism[S1, T],
) Operator[T, S1] {
return BindTo(setter.ReverseGet)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other.
@@ -154,7 +160,7 @@ func BindTo[S1, T any](
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa IOOption[T],
) Kleisli[IOOption[S1], S2] {
) Operator[S1, S2] {
return apply.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
@@ -187,9 +193,9 @@ func ApS[S1, S2, T any](
// iooption.ApSL(ageLens, iooption.Some(30)),
// )
func ApSL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
fa IOOption[T],
) Kleisli[IOOption[S], S] {
) Operator[S, S] {
return ApS(lens.Set, fa)
}
@@ -222,9 +228,9 @@ func ApSL[S, T any](
// iooption.BindL(valueLens, increment),
// ) // IOOption[Counter{Value: 43}]
func BindL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f Kleisli[T, T],
) Kleisli[IOOption[S], S] {
) Operator[S, S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
}
@@ -255,9 +261,9 @@ func BindL[S, T any](
// iooption.LetL(valueLens, double),
// ) // IOOption[Counter{Value: 42}]
func LetL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
f func(T) T,
) Kleisli[IOOption[S], S] {
) Operator[S, S] {
return Let(lens.Set, F.Flow2(lens.Get, f))
}
@@ -286,8 +292,8 @@ func LetL[S, T any](
// iooption.LetToL(debugLens, false),
// ) // IOOption[Config{Debug: false, Timeout: 30}]
func LetToL[S, T any](
lens L.Lens[S, T],
lens Lens[S, T],
b T,
) Kleisli[IOOption[S], S] {
) Operator[S, S] {
return LetTo(lens.Set, b)
}

View File

@@ -16,18 +16,18 @@
package iooption
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tailrec"
)
// TailRec creates a tail-recursive computation in the IOOption monad.
// It enables writing recursive algorithms that don't overflow the call stack by using
// an iterative loop - a technique where recursive calls are converted into iterations.
//
// The function takes a step function that returns an IOOption containing either:
// The function takes a step function that returns an IOOption containing a Trampoline:
// - None: Terminate recursion with no result
// - Some(Left(A)): Continue recursion with a new value of type A
// - Some(Right(B)): Terminate recursion with a final result of type B
// - Some(Bounce(A)): Continue recursion with a new value of type A
// - Some(Land(B)): Terminate recursion with a final result of type B
//
// This is particularly useful for implementing recursive algorithms that may fail at any step:
// - Iterative calculations that may not produce a result
@@ -45,8 +45,8 @@ import (
//
// Parameters:
// - f: A step function that takes the current state (A) and returns an IOOption
// containing either None (failure), Some(Left(A)) to continue with a new state,
// or Some(Right(B)) to terminate with a final result
// containing either None (failure), Some(Bounce(A)) to continue with a new state,
// or Some(Land(B)) to terminate with a final result
//
// Returns:
// - A Kleisli arrow (function from A to IOOption[B]) that executes the
@@ -59,17 +59,17 @@ import (
// result int
// }
//
// factorial := TailRec[any](func(state FactState) IOOption[Either[FactState, int]] {
// factorial := TailRec[any](func(state FactState) IOOption[tailrec.Trampoline[FactState, int]] {
// if state.n < 0 {
// // Negative numbers have no factorial
// return None[Either[FactState, int]]()
// return None[tailrec.Trampoline[FactState, int]]()
// }
// if state.n <= 1 {
// // Terminate with final result
// return Of(either.Right[FactState](state.result))
// return Of(tailrec.Land[FactState](state.result))
// }
// // Continue with next iteration
// return Of(either.Left[int](FactState{
// return Of(tailrec.Bounce[int](FactState{
// n: state.n - 1,
// result: state.result * state.n,
// }))
@@ -86,14 +86,14 @@ import (
// steps int
// }
//
// safeDivide := TailRec[any](func(state DivState) IOOption[Either[DivState, int]] {
// safeDivide := TailRec[any](func(state DivState) IOOption[tailrec.Trampoline[DivState, int]] {
// if state.denominator == 0 {
// return None[Either[DivState, int]]() // Division by zero
// return None[tailrec.Trampoline[DivState, int]]() // Division by zero
// }
// if state.numerator < state.denominator {
// return Of(either.Right[DivState](state.steps))
// return Of(tailrec.Land[DivState](state.steps))
// }
// return Of(either.Left[int](DivState{
// return Of(tailrec.Bounce[int](DivState{
// numerator: state.numerator - state.denominator,
// denominator: state.denominator,
// steps: state.steps + 1,
@@ -102,21 +102,20 @@ import (
//
// result := safeDivide(DivState{numerator: 10, denominator: 3, steps: 0})() // Some(3)
// result := safeDivide(DivState{numerator: 10, denominator: 0, steps: 0})() // None
func TailRec[E, A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
func TailRec[E, A, B any](f Kleisli[A, tailrec.Trampoline[A, B]]) Kleisli[A, B] {
return func(a A) IOOption[B] {
initial := f(a)
return func() Option[B] {
return func() option.Option[B] {
current := initial()
for {
r, ok := option.Unwrap(current)
if !ok {
return option.None[B]()
}
b, a := either.Unwrap(r)
if either.IsRight(r) {
return option.Some(b)
if r.Landed {
return option.Some(r.Land)
}
current = f(a)()
current = f(r.Bounce)()
}
}
}

View File

@@ -18,8 +18,8 @@ package iooption
import (
"testing"
E "github.com/IBM/fp-go/v2/either"
O "github.com/IBM/fp-go/v2/option"
TR "github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
@@ -30,17 +30,17 @@ func TestTailRecFactorial(t *testing.T) {
result int
}
factorial := TailRec[any](func(state FactState) IOOption[E.Either[FactState, int]] {
factorial := TailRec[any](func(state FactState) IOOption[TR.Trampoline[FactState, int]] {
if state.n < 0 {
// Negative numbers have no factorial
return None[E.Either[FactState, int]]()
return None[TR.Trampoline[FactState, int]]()
}
if state.n <= 1 {
// Terminate with final result
return Of(E.Right[FactState](state.result))
return Of(TR.Land[FactState](state.result))
}
// Continue with next iteration
return Of(E.Left[int](FactState{
return Of(TR.Bounce[int](FactState{
n: state.n - 1,
result: state.result * state.n,
}))
@@ -80,14 +80,14 @@ func TestTailRecSafeDivision(t *testing.T) {
steps int
}
safeDivide := TailRec[any](func(state DivState) IOOption[E.Either[DivState, int]] {
safeDivide := TailRec[any](func(state DivState) IOOption[TR.Trampoline[DivState, int]] {
if state.denominator == 0 {
return None[E.Either[DivState, int]]() // Division by zero
return None[TR.Trampoline[DivState, int]]() // Division by zero
}
if state.numerator < state.denominator {
return Of(E.Right[DivState](state.steps))
return Of(TR.Land[DivState](state.steps))
}
return Of(E.Left[int](DivState{
return Of(TR.Bounce[int](DivState{
numerator: state.numerator - state.denominator,
denominator: state.denominator,
steps: state.steps + 1,
@@ -123,14 +123,14 @@ func TestTailRecFindInRange(t *testing.T) {
max int
}
findInRange := TailRec[any](func(state FindState) IOOption[E.Either[FindState, int]] {
findInRange := TailRec[any](func(state FindState) IOOption[TR.Trampoline[FindState, int]] {
if state.current > state.max {
return None[E.Either[FindState, int]]() // Not found
return None[TR.Trampoline[FindState, int]]() // Not found
}
if state.current == state.target {
return Of(E.Right[FindState](state.current))
return Of(TR.Land[FindState](state.current))
}
return Of(E.Left[int](FindState{
return Of(TR.Bounce[int](FindState{
current: state.current + 1,
target: state.target,
max: state.max,
@@ -166,14 +166,14 @@ func TestTailRecSumUntilLimit(t *testing.T) {
limit int
}
sumUntilLimit := TailRec[any](func(state SumState) IOOption[E.Either[SumState, int]] {
sumUntilLimit := TailRec[any](func(state SumState) IOOption[TR.Trampoline[SumState, int]] {
if state.sum > state.limit {
return None[E.Either[SumState, int]]() // Exceeded limit
return None[TR.Trampoline[SumState, int]]() // Exceeded limit
}
if state.current <= 0 {
return Of(E.Right[SumState](state.sum))
return Of(TR.Land[SumState](state.sum))
}
return Of(E.Left[int](SumState{
return Of(TR.Bounce[int](SumState{
current: state.current - 1,
sum: state.sum + state.current,
limit: state.limit,
@@ -198,14 +198,14 @@ func TestTailRecSumUntilLimit(t *testing.T) {
// TestTailRecCountdown tests a simple countdown with optional result
func TestTailRecCountdown(t *testing.T) {
countdown := TailRec[any](func(n int) IOOption[E.Either[int, string]] {
countdown := TailRec[any](func(n int) IOOption[TR.Trampoline[int, string]] {
if n < 0 {
return None[E.Either[int, string]]() // Negative not allowed
return None[TR.Trampoline[int, string]]() // Negative not allowed
}
if n == 0 {
return Of(E.Right[int]("Done!"))
return Of(TR.Land[int]("Done!"))
}
return Of(E.Left[string](n - 1))
return Of(TR.Bounce[string](n - 1))
})
t.Run("countdown from 5", func(t *testing.T) {
@@ -227,14 +227,14 @@ func TestTailRecCountdown(t *testing.T) {
// TestTailRecStackSafety tests that TailRec doesn't overflow the stack with large iterations
func TestTailRecStackSafety(t *testing.T) {
// Count down from a large number - this would overflow the stack with regular recursion
largeCountdown := TailRec[any](func(n int) IOOption[E.Either[int, int]] {
largeCountdown := TailRec[any](func(n int) IOOption[TR.Trampoline[int, int]] {
if n < 0 {
return None[E.Either[int, int]]()
return None[TR.Trampoline[int, int]]()
}
if n == 0 {
return Of(E.Right[int](0))
return Of(TR.Land[int](0))
}
return Of(E.Left[int](n - 1))
return Of(TR.Bounce[int](n - 1))
})
t.Run("large iteration count", func(t *testing.T) {
@@ -252,14 +252,14 @@ func TestTailRecValidation(t *testing.T) {
}
// Validate all items are positive, return count if valid
validatePositive := TailRec[any](func(state ValidationState) IOOption[E.Either[ValidationState, int]] {
validatePositive := TailRec[any](func(state ValidationState) IOOption[TR.Trampoline[ValidationState, int]] {
if state.index >= len(state.items) {
return Of(E.Right[ValidationState](state.index))
return Of(TR.Land[ValidationState](state.index))
}
if state.items[state.index] <= 0 {
return None[E.Either[ValidationState, int]]() // Invalid item
return None[TR.Trampoline[ValidationState, int]]() // Invalid item
}
return Of(E.Left[int](ValidationState{
return Of(TR.Bounce[int](ValidationState{
items: state.items,
index: state.index + 1,
}))
@@ -294,17 +294,17 @@ func TestTailRecCollatzConjecture(t *testing.T) {
}
// Count steps to reach 1 in Collatz sequence
collatz := TailRec[any](func(state CollatzState) IOOption[E.Either[CollatzState, int]] {
collatz := TailRec[any](func(state CollatzState) IOOption[TR.Trampoline[CollatzState, int]] {
if state.n <= 0 {
return None[E.Either[CollatzState, int]]() // Invalid input
return None[TR.Trampoline[CollatzState, int]]() // Invalid input
}
if state.n == 1 {
return Of(E.Right[CollatzState](state.steps))
return Of(TR.Land[CollatzState](state.steps))
}
if state.n%2 == 0 {
return Of(E.Left[int](CollatzState{n: state.n / 2, steps: state.steps + 1}))
return Of(TR.Bounce[int](CollatzState{n: state.n / 2, steps: state.steps + 1}))
}
return Of(E.Left[int](CollatzState{n: 3*state.n + 1, steps: state.steps + 1}))
return Of(TR.Bounce[int](CollatzState{n: 3*state.n + 1, steps: state.steps + 1}))
})
t.Run("collatz for 1", func(t *testing.T) {

View File

@@ -20,6 +20,8 @@ import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
)
@@ -37,4 +39,7 @@ type (
Kleisli[A, B any] = reader.Reader[A, IOOption[B]]
Operator[A, B any] = Kleisli[IOOption[A], B]
Consumer[A any] = consumer.Consumer[A]
Lens[S, T any] = lens.Lens[S, T]
Prism[S, T any] = prism.Prism[S, T]
)

View File

@@ -17,9 +17,10 @@ package ioresult
import (
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/tailrec"
)
//go:inline
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
func TailRec[A, B any](f Kleisli[A, tailrec.Trampoline[A, B]]) Kleisli[A, B] {
return ioeither.TailRec(f)
}

View File

@@ -22,9 +22,11 @@ import (
"strings"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -42,13 +44,13 @@ func toMap[K comparable, V any](seq Seq2[K, V]) map[K]V {
func TestOf(t *testing.T) {
seq := Of(42)
result := toSlice(seq)
assert.Equal(t, []int{42}, result)
assert.Equal(t, A.Of(42), result)
}
func TestOf2(t *testing.T) {
seq := Of2("key", 100)
result := toMap(seq)
assert.Equal(t, map[string]int{"key": 100}, result)
assert.Equal(t, R.Of("key", 100), result)
}
func TestFrom(t *testing.T) {
@@ -587,3 +589,29 @@ func ExampleMonoid() {
fmt.Println(result)
// Output: [1 2 3 4 5 6]
}
func TestMonadMapToArray(t *testing.T) {
seq := From(1, 2, 3)
result := MonadMapToArray(seq, N.Mul(2))
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMonadMapToArrayEmpty(t *testing.T) {
seq := Empty[int]()
result := MonadMapToArray(seq, N.Mul(2))
assert.Empty(t, result)
}
func TestMapToArray(t *testing.T) {
seq := From(1, 2, 3)
mapper := MapToArray(N.Mul(2))
result := mapper(seq)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMapToArrayIdentity(t *testing.T) {
seq := From("a", "b", "c")
mapper := MapToArray(F.Identity[string])
result := mapper(seq)
assert.Equal(t, []string{"a", "b", "c"}, result)
}

View File

@@ -527,3 +527,127 @@ func BenchmarkMax(b *testing.B) {
_ = Max(i, i+1)
}
}
// Test MoreThan curried function
func TestMoreThan(t *testing.T) {
moreThan10 := MoreThan(10)
tests := []struct {
name string
input int
expected bool
}{
{"greater than threshold", 15, true},
{"less than threshold", 5, false},
{"equal to threshold", 10, false},
{"much greater", 100, true},
{"negative value", -5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := moreThan10(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MoreThan with floats
func TestMoreThan_Float(t *testing.T) {
moreThan5_5 := MoreThan(5.5)
assert.True(t, moreThan5_5(6.0))
assert.False(t, moreThan5_5(5.0))
assert.False(t, moreThan5_5(5.5))
assert.True(t, moreThan5_5(10.5))
assert.False(t, moreThan5_5(5.4))
}
// Test MoreThan with negative numbers
func TestMoreThan_Negative(t *testing.T) {
moreThanNeg5 := MoreThan(-5)
assert.True(t, moreThanNeg5(0))
assert.True(t, moreThanNeg5(-4))
assert.False(t, moreThanNeg5(-5))
assert.False(t, moreThanNeg5(-10))
}
// Test LessThan curried function
func TestLessThan(t *testing.T) {
lessThan10 := LessThan(10)
tests := []struct {
name string
input int
expected bool
}{
{"less than threshold", 5, true},
{"greater than threshold", 15, false},
{"equal to threshold", 10, false},
{"much less", -10, true},
{"zero", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := lessThan10(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Test LessThan with floats
func TestLessThan_Float(t *testing.T) {
lessThan5_5 := LessThan(5.5)
assert.True(t, lessThan5_5(5.0))
assert.False(t, lessThan5_5(6.0))
assert.False(t, lessThan5_5(5.5))
assert.True(t, lessThan5_5(2.5))
assert.True(t, lessThan5_5(5.4))
}
// Test LessThan with negative numbers
func TestLessThan_Negative(t *testing.T) {
lessThanNeg5 := LessThan(-5)
assert.False(t, lessThanNeg5(0))
assert.False(t, lessThanNeg5(-4))
assert.False(t, lessThanNeg5(-5))
assert.True(t, lessThanNeg5(-10))
}
// Test MoreThan and LessThan together for range checking
func TestMoreThanLessThan_Range(t *testing.T) {
// Check if value is in range (10, 20) - exclusive
moreThan10 := MoreThan(10)
lessThan20 := LessThan(20)
inRange := func(x int) bool {
return moreThan10(x) && lessThan20(x)
}
assert.True(t, inRange(15))
assert.False(t, inRange(10))
assert.False(t, inRange(20))
assert.False(t, inRange(5))
assert.False(t, inRange(25))
}
// Benchmark tests for comparison functions
func BenchmarkMoreThan(b *testing.B) {
moreThan10 := MoreThan(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = moreThan10(i)
}
}
func BenchmarkLessThan(b *testing.B) {
lessThan10 := LessThan(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = lessThan10(i)
}
}

View File

@@ -13,50 +13,107 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package number provides utility functions for numeric operations with a functional programming approach.
// It includes curried arithmetic operations, comparison functions, and min/max utilities that work with
// generic numeric types.
package number
import (
C "github.com/IBM/fp-go/v2/constraints"
)
// Number is a constraint that represents all numeric types including integers, floats, and complex numbers.
// It combines the Integer, Float, and Complex constraints from the constraints package.
type Number interface {
C.Integer | C.Float | C.Complex
}
// Add is a curried function used to add two numbers
// Add is a curried function that adds two numbers.
// It takes a right operand and returns a function that takes a left operand,
// returning their sum (left + right).
//
// This curried form is useful for partial application and function composition.
//
// Example:
//
// add5 := Add(5)
// result := add5(10) // returns 15
func Add[T Number](right T) func(T) T {
return func(left T) T {
return left + right
}
}
// Sub is a curried function used to subtract two numbers
// Sub is a curried function that subtracts two numbers.
// It takes a right operand and returns a function that takes a left operand,
// returning their difference (left - right).
//
// This curried form is useful for partial application and function composition.
//
// Example:
//
// sub5 := Sub(5)
// result := sub5(10) // returns 5
func Sub[T Number](right T) func(T) T {
return func(left T) T {
return left - right
}
}
// Mul is a curried function used to multiply two numbers
// Mul is a curried function that multiplies two numbers.
// It takes a right operand and returns a function that takes a left operand,
// returning their product (left * right).
//
// This curried form is useful for partial application and function composition.
//
// Example:
//
// double := Mul(2)
// result := double(10) // returns 20
func Mul[T Number](right T) func(T) T {
return func(left T) T {
return left * right
}
}
// Div is a curried function used to divide two numbers
// Div is a curried function that divides two numbers.
// It takes a right operand (divisor) and returns a function that takes a left operand (dividend),
// returning their quotient (left / right).
//
// Note: Division by zero will cause a panic for integer types or return infinity for float types.
//
// This curried form is useful for partial application and function composition.
//
// Example:
//
// halve := Div(2)
// result := halve(10) // returns 5
func Div[T Number](right T) func(T) T {
return func(left T) T {
return left / right
}
}
// Inc is a function that increments a number
// Inc increments a number by 1.
// It works with any numeric type that satisfies the Number constraint.
//
// Example:
//
// result := Inc(5) // returns 6
func Inc[T Number](value T) T {
return value + 1
}
// Min takes the minimum of two values. If they are considered equal, the first argument is chosen
// Min returns the minimum of two ordered values.
// If the values are considered equal, the first argument is returned.
//
// This function works with any type that satisfies the Ordered constraint,
// including all numeric types and strings.
//
// Example:
//
// result := Min(5, 10) // returns 5
// result := Min(3.14, 2.71) // returns 2.71
func Min[A C.Ordered](a, b A) A {
if a < b {
return a
@@ -64,10 +121,55 @@ func Min[A C.Ordered](a, b A) A {
return b
}
// Max takes the maximum of two values. If they are considered equal, the first argument is chosen
// Max returns the maximum of two ordered values.
// If the values are considered equal, the first argument is returned.
//
// This function works with any type that satisfies the Ordered constraint,
// including all numeric types and strings.
//
// Example:
//
// result := Max(5, 10) // returns 10
// result := Max(3.14, 2.71) // returns 3.14
func Max[A C.Ordered](a, b A) A {
if a > b {
return a
}
return b
}
// MoreThan is a curried comparison function that checks if a value is more than (greater than) another.
// It takes a threshold value 'a' and returns a predicate function that checks if 'a' is less than its argument,
// meaning the argument is more than 'a'.
//
// This curried form is useful for creating reusable predicates and function composition.
//
// Example:
//
// moreThan10 := MoreThan(10)
// result := moreThan10(15) // returns true (15 is more than 10)
// result := moreThan10(10) // returns false (10 is not more than 10)
// result := moreThan10(5) // returns false (5 is not more than 10)
func MoreThan[A C.Ordered](a A) func(A) bool {
return func(b A) bool {
return a < b
}
}
// LessThan is a curried comparison function that checks if a value is less than another.
// It takes a threshold value 'a' and returns a predicate function that checks if 'a' is greater than its argument,
// meaning the argument is less than 'a'.
//
// This curried form is useful for creating reusable predicates and function composition.
//
// Example:
//
// lessThan10 := LessThan(10)
// result := lessThan10(5) // returns true (5 is less than 10)
// result := lessThan10(10) // returns false (10 is not less than 10)
// result := lessThan10(15) // returns false (15 is not less than 10)
func LessThan[A C.Ordered](a A) func(A) bool {
return func(b A) bool {
return a > b
}
}

View File

@@ -1,12 +1,36 @@
# Optics
# 🔍 Optics
Functional optics for composable data access and manipulation in Go.
## Overview
## 📖 Overview
Optics are first-class, composable references to parts of data structures. They provide a uniform interface for reading, writing, and transforming nested immutable data without verbose boilerplate code.
## Quick Start
## ✨ Why Use Optics?
Optics bring powerful benefits to your Go code:
- **🎯 Composability**: Optics naturally compose with each other and with monadic operations, enabling elegant data transformations through function composition
- **🔒 Immutability**: Work with immutable data structures without manual copying and updating
- **🧩 Type Safety**: Leverage Go's type system to catch errors at compile time
- **📦 Reusability**: Define data access patterns once and reuse them throughout your codebase
- **🎨 Expressiveness**: Write declarative code that clearly expresses intent
- **🔄 Bidirectionality**: Read and write through the same abstraction
- **🚀 Productivity**: Eliminate boilerplate for nested data access and updates
- **🧪 Testability**: Optics are pure functions, making them easy to test and reason about
### 🔗 Composition with Monadic Operations
One of the most powerful features of optics is their natural composition with monadic operations. Optics integrate seamlessly with `fp-go`'s monadic types like [`Option`](https://pkg.go.dev/github.com/IBM/fp-go/v2/option), [`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either), [`Result`](https://pkg.go.dev/github.com/IBM/fp-go/v2/result), and [`IO`](https://pkg.go.dev/github.com/IBM/fp-go/v2/io), allowing you to:
- Chain optional field access with [`Option`](https://pkg.go.dev/github.com/IBM/fp-go/v2/option) monads
- Handle errors gracefully with [`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either) or [`Result`](https://pkg.go.dev/github.com/IBM/fp-go/v2/result) monads
- Perform side effects with [`IO`](https://pkg.go.dev/github.com/IBM/fp-go/v2/io) monads
- Combine multiple optics in a single pipeline using [`Pipe`](https://pkg.go.dev/github.com/IBM/fp-go/v2/function#Pipe1)
This composability enables you to build complex data transformations from simple, reusable building blocks.
## 🚀 Quick Start
```go
import (
@@ -38,9 +62,9 @@ updated := nameLens.Set("Bob")(person)
// person.Name is still "Alice", updated.Name is "Bob"
```
## Core Optics Types
## 🛠️ Core Optics Types
### Lens - Product Types (Structs)
### 🔎 [Lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens) - Product Types (Structs)
Focus on a single field within a struct. Provides get and set operations.
**Use when:** Working with struct fields that always exist.
@@ -55,14 +79,19 @@ ageLens := lens.MakeLens(
)
```
### Prism - Sum Types (Variants)
### 🔀 [Prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism) - Sum Types (Variants)
Focus on one variant of a sum type. Provides optional get and definite set.
**Use when:** Working with Either, Result, or custom sum types.
**Use when:** Working with [`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either), [`Result`](https://pkg.go.dev/github.com/IBM/fp-go/v2/result), or custom sum types.
**💡 Important Use Case - Generalized Constructors for Do Notation:**
[Prisms](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism) act as generalized constructors, making them invaluable for `Do` notation workflows. The prism's `ReverseGet` function serves as a constructor that creates a value of the sum type from a specific variant. This is particularly useful when building up complex data structures step-by-step in monadic contexts:
```go
import "github.com/IBM/fp-go/v2/optics/prism"
// Prism for the Success variant
successPrism := prism.MakePrism(
func(r Result) option.Option[int] {
if s, ok := r.(Success); ok {
@@ -70,11 +99,18 @@ successPrism := prism.MakePrism(
}
return option.None[int]()
},
func(v int) Result { return Success{Value: v} },
func(v int) Result { return Success{Value: v} }, // Constructor!
)
// Use in Do notation to construct values
result := F.Pipe2(
computeValue(),
option.Map(func(v int) int { return v * 2 }),
option.Map(successPrism.ReverseGet), // Construct Result from int
)
```
### Iso - Isomorphisms
### 🔄 [Iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso) - Isomorphisms
Bidirectional transformation between equivalent types with no information loss.
**Use when:** Converting between equivalent representations (e.g., Celsius ↔ Fahrenheit).
@@ -88,7 +124,7 @@ celsiusToFahrenheit := iso.MakeIso(
)
```
### Optional - Maybe Values
### ❓ [Optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional) - Maybe Values
Focus on a value that may or may not exist.
**Use when:** Working with nullable fields or values that may be absent.
@@ -107,7 +143,7 @@ timeoutOptional := optional.MakeOptional(
)
```
### Traversal - Multiple Values
### 🔢 [Traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal) - Multiple Values
Focus on multiple values simultaneously, allowing batch operations.
**Use when:** Working with collections or updating multiple fields at once.
@@ -129,7 +165,7 @@ doubled := F.Pipe2(
// Result: [2, 4, 6, 8, 10]
```
## Composition
## 🔗 Composition
The real power of optics comes from composition:
@@ -176,41 +212,100 @@ city := companyCityLens.Get(company) // "NYC"
updated := companyCityLens.Set("Boston")(company)
```
## Optics Hierarchy
## ⚙️ Auto-Generation with [`go generate`](https://go.dev/blog/generate)
Lenses can be automatically generated using the `fp-go` CLI tool and a simple annotation. This eliminates boilerplate and ensures consistency.
### 📝 How to Use
1. **Annotate your struct** with the `fp-go:Lens` comment:
```go
//go:generate go run github.com/IBM/fp-go/v2/main.go lens --dir . --filename gen_lens.go
// fp-go:Lens
type Person struct {
Name string
Age int
Email string
Phone *string // Optional field
}
```
2. **Run `go generate`**:
```bash
go generate ./...
```
3. **Use the generated lenses**:
```go
// Generated code creates PersonLenses, PersonRefLenses, and PersonPrisms
lenses := MakePersonLenses()
person := Person{Name: "Alice", Age: 30, Email: "alice@example.com"}
// Use the generated lenses
updatedPerson := lenses.Age.Set(31)(person)
name := lenses.Name.Get(person)
// Optional lenses for zero-value handling
personWithEmail := lenses.EmailO.Set(option.Some("new@example.com"))(person)
```
### 🎁 What Gets Generated
For each annotated struct, the generator creates:
- **`StructNameLenses`**: Lenses for value types with optional variants (`LensO`) for comparable fields
- **`StructNameRefLenses`**: Lenses for pointer types with prisms for constructing values
- **`StructNamePrisms`**: Prisms for all fields, useful for partial construction
- Constructor functions: `MakeStructNameLenses()`, `MakeStructNameRefLenses()`, `MakeStructNamePrisms()`
The generator supports:
- ✅ Generic types with type parameters
- ✅ Embedded structs (fields are promoted)
- ✅ Optional fields (pointers and `omitempty` tags)
- ✅ Custom package imports
See [samples/lens](../samples/lens) for complete examples.
## 📊 Optics Hierarchy
```
Iso[S, A]
[Iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)[S, A]
Lens[S, A]
[Lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)[S, A]
Optional[S, A]
[Optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)[S, A]
Traversal[S, A]
[Traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)[S, A]
Prism[S, A]
[Prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism)[S, A]
Optional[S, A]
[Optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)[S, A]
Traversal[S, A]
[Traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)[S, A]
```
More specific optics can be converted to more general ones.
## Package Structure
## 📦 Package Structure
- **optics/lens**: Lenses for product types (structs)
- **optics/prism**: Prisms for sum types (Either, Result, etc.)
- **optics/iso**: Isomorphisms for equivalent types
- **optics/optional**: Optional optics for maybe values
- **optics/traversal**: Traversals for multiple values
- **[optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)**: Lenses for product types (structs)
- **[optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism)**: Prisms for sum types ([`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either), [`Result`](https://pkg.go.dev/github.com/IBM/fp-go/v2/result), etc.)
- **[optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)**: Isomorphisms for equivalent types
- **[optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)**: Optional optics for maybe values
- **[optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)**: Traversals for multiple values
Each package includes specialized sub-packages for common patterns:
- **array**: Optics for arrays/slices
- **either**: Optics for Either types
- **option**: Optics for Option types
- **either**: Optics for [`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either) types
- **option**: Optics for [`Option`](https://pkg.go.dev/github.com/IBM/fp-go/v2/option) types
- **record**: Optics for maps
## Documentation
## 📚 Documentation
For detailed documentation on each optic type, see:
- [Main Package Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics)
@@ -220,15 +315,34 @@ For detailed documentation on each optic type, see:
- [Optional Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)
- [Traversal Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)
## Further Reading
## 🌐 Further Reading
For an introduction to functional optics concepts:
- [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe) by Giulio Canti
### Haskell Lens Library
The concepts in this library are inspired by the powerful [Haskell lens library](https://hackage.haskell.org/package/lens), which pioneered many of these abstractions.
## Examples
### Articles and Resources
- [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe) by Giulio Canti - Excellent introduction to optics concepts
- [Lenses in Functional Programming](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial) - Tutorial on lens fundamentals
- [Profunctor Optics: The Categorical View](https://bartoszmilewski.com/2017/07/07/profunctor-optics-the-categorical-view/) by Bartosz Milewski - Deep dive into the theory
- [Why Optics?](https://www.tweag.io/blog/2022-01-06-optics-vs-lenses/) - Discussion of benefits and use cases
See the [samples/lens](../samples/lens) directory for complete working examples.
### Why Functional Optics?
Functional optics solve real problems in software development:
- **Nested Updates**: Eliminate deeply nested field access patterns
- **Immutability**: Make working with immutable data practical and ergonomic
- **Abstraction**: Separate data access patterns from business logic
- **Composition**: Build complex operations from simple, reusable pieces
- **Type Safety**: Catch errors at compile time rather than runtime
## License
## 💡 Examples
See the [samples/lens](../samples/lens) directory for complete working examples, including:
- Basic lens usage
- Lens composition
- Auto-generated lenses
- Prism usage for sum types
- Integration with monadic operations
## 📄 License
Apache License 2.0 - See LICENSE file for details.

View File

@@ -305,6 +305,63 @@ Convenience Functions:
- Unwrap/To: Extract the target value (Get)
- Wrap/From: Wrap into the source value (ReverseGet)
# Useful Iso Implementations
The package provides several ready-to-use isomorphisms for common transformations:
**String and Byte Conversions:**
- UTF8String: []byte ↔ string (UTF-8 encoding)
- Lines: []string ↔ string (newline-separated text)
**Time Conversions:**
- UnixMilli: int64 ↔ time.Time (Unix millisecond timestamps)
**Numeric Operations:**
- Add[T]: T ↔ T (shift by constant addition)
- Sub[T]: T ↔ T (shift by constant subtraction)
**Collection Operations:**
- ReverseArray[A]: []A ↔ []A (reverse slice order, self-inverse)
- Head[A]: A ↔ NonEmptyArray[A] (singleton array conversion)
**Pair and Either Operations:**
- SwapPair[A, B]: Pair[A, B] ↔ Pair[B, A] (swap pair elements, self-inverse)
- SwapEither[E, A]: Either[E, A] ↔ Either[A, E] (swap Either types, self-inverse)
**Option Conversions (optics/iso/option):**
- FromZero[T]: T ↔ Option[T] (zero value ↔ None, non-zero ↔ Some)
**Lens Conversions (optics/iso/lens):**
- IsoAsLens: Convert Iso[S, A] to Lens[S, A]
- IsoAsLensRef: Convert Iso[*S, A] to Lens[*S, A]
Example usage of built-in isomorphisms:
// String/byte conversion
utf8 := UTF8String()
str := utf8.Get([]byte("hello")) // "hello"
// Time conversion
unixTime := UnixMilli()
t := unixTime.Get(1609459200000) // 2021-01-01 00:00:00 UTC
// Numeric shift
addTen := Add(10)
result := addTen.Get(5) // 15
// Array reversal
reverse := ReverseArray[int]()
reversed := reverse.Get([]int{1, 2, 3}) // []int{3, 2, 1}
// Pair swap
swap := SwapPair[string, int]()
swapped := swap.Get(pair.MakePair("a", 1)) // Pair[int, string](1, "a")
// Option conversion
optIso := option.FromZero[int]()
opt := optIso.Get(0) // None
opt = optIso.Get(42) // Some(42)
# Related Packages
- github.com/IBM/fp-go/v2/optics/lens: Lenses for focusing on parts of structures

84
v2/optics/iso/format.go Normal file
View File

@@ -0,0 +1,84 @@
// 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 iso
import (
"fmt"
"log/slog"
"github.com/IBM/fp-go/v2/internal/formatting"
)
// String returns a string representation of the isomorphism.
//
// Example:
//
// tempIso := iso.MakeIso(...)
// fmt.Println(tempIso) // Prints: "Iso"
func (i Iso[S, T]) String() string {
return "Iso"
}
// Format implements fmt.Formatter for Iso.
// Supports all standard format verbs:
// - %s, %v, %+v: uses String() representation
// - %#v: uses GoString() representation
// - %q: quoted String() representation
// - other verbs: uses String() representation
//
// Example:
//
// tempIso := iso.MakeIso(...)
// fmt.Printf("%s", tempIso) // "Iso"
// fmt.Printf("%v", tempIso) // "Iso"
// fmt.Printf("%#v", tempIso) // "iso.Iso[Celsius, Fahrenheit]"
//
//go:noinline
func (i Iso[S, T]) Format(f fmt.State, c rune) {
formatting.FmtString(i, f, c)
}
// GoString implements fmt.GoStringer for Iso.
// Returns a Go-syntax representation of the Iso value.
//
// Example:
//
// tempIso := iso.MakeIso(...)
// tempIso.GoString() // "iso.Iso[Celsius, Fahrenheit]"
//
//go:noinline
func (i Iso[S, T]) GoString() string {
return fmt.Sprintf("iso.Iso[%s, %s]",
formatting.TypeInfo(new(S)),
formatting.TypeInfo(new(T)),
)
}
// LogValue implements slog.LogValuer for Iso.
// Returns a slog.Value that represents the Iso for structured logging.
// Logs the type information as a string value.
//
// Example:
//
// logger := slog.Default()
// tempIso := iso.MakeIso(...)
// logger.Info("using iso", "iso", tempIso)
// // Logs: {"msg":"using iso","iso":"Iso"}
//
//go:noinline
func (i Iso[S, T]) LogValue() slog.Value {
return slog.StringValue("Iso")
}

View File

@@ -17,8 +17,6 @@
package iso
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
)
@@ -405,11 +403,3 @@ func IMap[S, A, B any](ab func(A) B, ba func(B) A) func(Iso[S, A]) Iso[S, B] {
return imap(sa, ab, ba)
}
}
func (l Iso[S, T]) String() string {
return "Iso"
}
func (l Iso[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

85
v2/optics/lens/format.go Normal file
View File

@@ -0,0 +1,85 @@
// 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 lens
import (
"fmt"
"log/slog"
"github.com/IBM/fp-go/v2/internal/formatting"
)
// String returns the name of the lens for debugging and display purposes.
//
// Example:
//
// nameLens := lens.MakeLensWithName(..., "Person.Name")
// fmt.Println(nameLens) // Prints: "Person.Name"
func (l Lens[S, T]) String() string {
return l.name
}
// Format implements fmt.Formatter for Lens.
// Supports all standard format verbs:
// - %s, %v, %+v: uses String() representation (lens name)
// - %#v: uses GoString() representation
// - %q: quoted String() representation
// - other verbs: uses String() representation
//
// Example:
//
// nameLens := lens.MakeLensWithName(..., "Person.Name")
// fmt.Printf("%s", nameLens) // "Person.Name"
// fmt.Printf("%v", nameLens) // "Person.Name"
// fmt.Printf("%#v", nameLens) // "lens.Lens[Person, string]{name: \"Person.Name\"}"
//
//go:noinline
func (l Lens[S, T]) Format(f fmt.State, c rune) {
formatting.FmtString(l, f, c)
}
// GoString implements fmt.GoStringer for Lens.
// Returns a Go-syntax representation of the Lens value.
//
// Example:
//
// nameLens := lens.MakeLensWithName(..., "Person.Name")
// nameLens.GoString() // "lens.Lens[Person, string]{name: \"Person.Name\"}"
//
//go:noinline
func (l Lens[S, T]) GoString() string {
return fmt.Sprintf("lens.Lens[%s, %s]{name: %q}",
formatting.TypeInfo(new(S)),
formatting.TypeInfo(new(T)),
l.name,
)
}
// LogValue implements slog.LogValuer for Lens.
// Returns a slog.Value that represents the Lens for structured logging.
// Logs the lens name as a string value.
//
// Example:
//
// logger := slog.Default()
// nameLens := lens.MakeLensWithName(..., "Person.Name")
// logger.Info("using lens", "lens", nameLens)
// // Logs: {"msg":"using lens","lens":"Person.Name"}
//
//go:noinline
func (l Lens[S, T]) LogValue() slog.Value {
return slog.StringValue(l.name)
}

View File

@@ -979,26 +979,3 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[
// "Person.Name",
// )
// fmt.Println(nameLens) // Prints: "Person.Name"
func (l Lens[S, T]) String() string {
return l.name
}
// Format implements the fmt.Formatter interface for custom formatting of lenses.
//
// This allows lenses to be used with fmt.Printf and related functions with
// various format verbs. All format operations delegate to the String() method,
// which returns the lens name.
//
// Parameters:
// - f: The format state containing formatting options
// - c: The format verb (currently unused, all verbs produce the same output)
//
// Example:
//
// nameLens := lens.MakeLensWithName(..., "Person.Name")
// fmt.Printf("Lens: %v\n", nameLens) // Prints: "Lens: Person.Name"
// fmt.Printf("Lens: %s\n", nameLens) // Prints: "Lens: Person.Name"
// fmt.Printf("Lens: %q\n", nameLens) // Prints: "Lens: Person.Name"
func (l Lens[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -0,0 +1,85 @@
// 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 optional
import (
"fmt"
"log/slog"
"github.com/IBM/fp-go/v2/internal/formatting"
)
// String returns the name of the optional for debugging and display purposes.
//
// Example:
//
// fieldOptional := optional.MakeOptionalWithName(..., "Person.Email")
// fmt.Println(fieldOptional) // Prints: "Person.Email"
func (o Optional[S, T]) String() string {
return o.name
}
// Format implements fmt.Formatter for Optional.
// Supports all standard format verbs:
// - %s, %v, %+v: uses String() representation (optional name)
// - %#v: uses GoString() representation
// - %q: quoted String() representation
// - other verbs: uses String() representation
//
// Example:
//
// fieldOptional := optional.MakeOptionalWithName(..., "Person.Email")
// fmt.Printf("%s", fieldOptional) // "Person.Email"
// fmt.Printf("%v", fieldOptional) // "Person.Email"
// fmt.Printf("%#v", fieldOptional) // "optional.Optional[Person, string]{name: \"Person.Email\"}"
//
//go:noinline
func (o Optional[S, T]) Format(f fmt.State, c rune) {
formatting.FmtString(o, f, c)
}
// GoString implements fmt.GoStringer for Optional.
// Returns a Go-syntax representation of the Optional value.
//
// Example:
//
// fieldOptional := optional.MakeOptionalWithName(..., "Person.Email")
// fieldOptional.GoString() // "optional.Optional[Person, string]{name: \"Person.Email\"}"
//
//go:noinline
func (o Optional[S, T]) GoString() string {
return fmt.Sprintf("optional.Optional[%s, %s]{name: %q}",
formatting.TypeInfo(new(S)),
formatting.TypeInfo(new(T)),
o.name,
)
}
// LogValue implements slog.LogValuer for Optional.
// Returns a slog.Value that represents the Optional for structured logging.
// Logs the optional name as a string value.
//
// Example:
//
// logger := slog.Default()
// fieldOptional := optional.MakeOptionalWithName(..., "Person.Email")
// logger.Info("using optional", "optional", fieldOptional)
// // Logs: {"msg":"using optional","optional":"Person.Email"}
//
//go:noinline
func (o Optional[S, T]) LogValue() slog.Value {
return slog.StringValue(o.name)
}

View File

@@ -18,8 +18,6 @@
package optional
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
@@ -225,11 +223,3 @@ func IChainAny[S, A any]() Operator[S, any, A] {
return ichain(sa, fromAny, toAny)
}
}
func (l Optional[S, T]) String() string {
return l.name
}
func (l Optional[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -409,6 +409,62 @@ Prisms are fully type-safe:
- Generic type parameters ensure correctness
- Composition maintains type relationships
# Built-in Prisms
The package provides many useful prisms for common transformations:
**Type Conversion & Parsing:**
- FromEncoding(enc): Base64 encoding/decoding - Prism[string, []byte]
- ParseURL(): URL parsing/formatting - Prism[string, *url.URL]
- ParseDate(layout): Date parsing with custom layouts - Prism[string, time.Time]
- ParseInt(): Integer string parsing - Prism[string, int]
- ParseInt64(): 64-bit integer parsing - Prism[string, int64]
- ParseBool(): Boolean string parsing - Prism[string, bool]
- ParseFloat32(): 32-bit float parsing - Prism[string, float32]
- ParseFloat64(): 64-bit float parsing - Prism[string, float64]
**Type Assertion & Extraction:**
- InstanceOf[T](): Safe type assertion from any - Prism[any, T]
- Deref[T](): Safe pointer dereferencing (filters nil) - Prism[*T, *T]
**Container/Wrapper Prisms:**
- FromEither[E, T](): Extract Right values - Prism[Either[E, T], T]
ReverseGet wraps into Right (acts as success constructor)
- FromResult[T](): Extract success from Result - Prism[Result[T], T]
ReverseGet wraps into success Result
- FromOption[T](): Extract Some values - Prism[Option[T], T]
ReverseGet wraps into Some (acts as Some constructor)
**Validation Prisms:**
- FromZero[T](): Match only zero/default values - Prism[T, T]
- FromNonZero[T](): Match only non-zero values - Prism[T, T]
**Pattern Matching:**
- RegexMatcher(re): Extract regex matches with groups - Prism[string, Match]
- RegexNamedMatcher(re): Extract named regex groups - Prism[string, NamedMatch]
Example using built-in prisms:
// Parse and validate an integer from a string
intPrism := prism.ParseInt()
value := intPrism.GetOption("42") // Some(42)
invalid := intPrism.GetOption("abc") // None[int]()
// Extract success values from Either
resultPrism := prism.FromEither[error, int]()
success := either.Right[error](100)
value = resultPrism.GetOption(success) // Some(100)
// ReverseGet acts as a constructor
wrapped := resultPrism.ReverseGet(42) // Right(42)
// Compose prisms for complex transformations
// Parse string to int, then wrap in Option
composed := F.Pipe1(
prism.ParseInt(),
prism.Compose[string](prism.FromOption[int]()),
)
# Function Reference
Core Functions:

85
v2/optics/prism/format.go Normal file
View File

@@ -0,0 +1,85 @@
// 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 prism
import (
"fmt"
"log/slog"
"github.com/IBM/fp-go/v2/internal/formatting"
)
// String returns the name of the prism for debugging and display purposes.
//
// Example:
//
// successPrism := prism.MakePrismWithName(..., "Result.Success")
// fmt.Println(successPrism) // Prints: "Result.Success"
func (p Prism[S, T]) String() string {
return p.name
}
// Format implements fmt.Formatter for Prism.
// Supports all standard format verbs:
// - %s, %v, %+v: uses String() representation (prism name)
// - %#v: uses GoString() representation
// - %q: quoted String() representation
// - other verbs: uses String() representation
//
// Example:
//
// successPrism := prism.MakePrismWithName(..., "Result.Success")
// fmt.Printf("%s", successPrism) // "Result.Success"
// fmt.Printf("%v", successPrism) // "Result.Success"
// fmt.Printf("%#v", successPrism) // "prism.Prism[Result, int]{name: \"Result.Success\"}"
//
//go:noinline
func (p Prism[S, T]) Format(f fmt.State, c rune) {
formatting.FmtString(p, f, c)
}
// GoString implements fmt.GoStringer for Prism.
// Returns a Go-syntax representation of the Prism value.
//
// Example:
//
// successPrism := prism.MakePrismWithName(..., "Result.Success")
// successPrism.GoString() // "prism.Prism[Result, int]{name: \"Result.Success\"}"
//
//go:noinline
func (p Prism[S, T]) GoString() string {
return fmt.Sprintf("prism.Prism[%s, %s]{name: %q}",
formatting.TypeInfo(new(S)),
formatting.TypeInfo(new(T)),
p.name,
)
}
// LogValue implements slog.LogValuer for Prism.
// Returns a slog.Value that represents the Prism for structured logging.
// Logs the prism name as a string value.
//
// Example:
//
// logger := slog.Default()
// successPrism := prism.MakePrismWithName(..., "Result.Success")
// logger.Info("using prism", "prism", successPrism)
// // Logs: {"msg":"using prism","prism":"Result.Success"}
//
//go:noinline
func (p Prism[S, T]) LogValue() slog.Value {
return slog.StringValue(p.name)
}

View File

@@ -270,11 +270,3 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[
return imap(sa, ab, ba)
}
}
func (l Prism[S, T]) String() string {
return l.name
}
func (l Prism[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -18,7 +18,6 @@ package option
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
)
@@ -48,38 +47,17 @@ type (
Operator[A, B any] = Kleisli[Option[A], B]
)
// optString prints some debug info for the object
// String implements fmt.Stringer for Option.
// Returns a human-readable string representation.
//
//go:noinline
func optString(isSome bool, value any) string {
if isSome {
return fmt.Sprintf("Some[%T](%v)", value, value)
}
return fmt.Sprintf("None[%T]", value)
}
// optFormat prints some debug info for the object
// Example:
//
//go:noinline
func optFormat(isSome bool, value any, f fmt.State, c rune) {
switch c {
case 's':
fmt.Fprint(f, optString(isSome, value))
default:
fmt.Fprint(f, optString(isSome, value))
}
}
// String prints some debug info for the object
// Some(42).String() // "Some[int](42)"
// None[int]().String() // "None[int]"
func (s Option[A]) String() string {
return optString(s.isSome, s.value)
}
// Format prints some debug info for the object
func (s Option[A]) Format(f fmt.State, c rune) {
optFormat(s.isSome, s.value, f, c)
}
func optMarshalJSON(isSome bool, value any) ([]byte, error) {
if isSome {
return json.Marshal(value)

View File

@@ -48,7 +48,7 @@ func ExampleOption_creation() {
// Output:
// None[int]
// Some[string](value)
// None[*string]
// None[string]
// true
// None[int]
// Some[int](4)

View File

@@ -0,0 +1,164 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package option_test
import (
"fmt"
"log/slog"
"os"
O "github.com/IBM/fp-go/v2/option"
)
// ExampleOption_String demonstrates the fmt.Stringer interface implementation.
func ExampleOption_String() {
some := O.Some(42)
none := O.None[int]()
fmt.Println(some.String())
fmt.Println(none.String())
// Output:
// Some[int](42)
// None[int]
}
// ExampleOption_GoString demonstrates the fmt.GoStringer interface implementation.
func ExampleOption_GoString() {
some := O.Some(42)
none := O.None[int]()
fmt.Printf("%#v\n", some)
fmt.Printf("%#v\n", none)
// Output:
// option.Some[int](42)
// option.None[int]
}
// ExampleOption_Format demonstrates the fmt.Formatter interface implementation.
func ExampleOption_Format() {
result := O.Some(42)
// Different format verbs
fmt.Printf("%%s: %s\n", result)
fmt.Printf("%%v: %v\n", result)
fmt.Printf("%%+v: %+v\n", result)
fmt.Printf("%%#v: %#v\n", result)
// Output:
// %s: Some[int](42)
// %v: Some[int](42)
// %+v: Some[int](42)
// %#v: option.Some[int](42)
}
// ExampleOption_LogValue demonstrates the slog.LogValuer interface implementation.
func ExampleOption_LogValue() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Remove time for consistent output
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
// Some value
someResult := O.Some(42)
logger.Info("computation succeeded", "result", someResult)
// None value
noneResult := O.None[int]()
logger.Info("computation failed", "result", noneResult)
// Output:
// level=INFO msg="computation succeeded" result.some=42
// level=INFO msg="computation failed" result.none={}
}
// ExampleOption_formatting_comparison demonstrates different formatting options.
func ExampleOption_formatting_comparison() {
type User struct {
ID int
Name string
}
user := User{ID: 123, Name: "Alice"}
result := O.Some(user)
fmt.Printf("String(): %s\n", result.String())
fmt.Printf("GoString(): %s\n", result.GoString())
fmt.Printf("%%v: %v\n", result)
fmt.Printf("%%#v: %#v\n", result)
// Output:
// String(): Some[option_test.User]({123 Alice})
// GoString(): option.Some[option_test.User](option_test.User{ID:123, Name:"Alice"})
// %v: Some[option_test.User]({123 Alice})
// %#v: option.Some[option_test.User](option_test.User{ID:123, Name:"Alice"})
}
// ExampleOption_LogValue_structured demonstrates structured logging with Option.
func ExampleOption_LogValue_structured() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
// Simulate a computation pipeline
compute := func(x int) O.Option[int] {
if x < 0 {
return O.None[int]()
}
return O.Some(x * 2)
}
// Log successful computation
result1 := compute(21)
logger.Info("computation", "input", 21, "output", result1)
// Log failed computation
result2 := compute(-5)
logger.Warn("computation", "input", -5, "output", result2)
// Output:
// level=INFO msg=computation input=21 output.some=42
// level=WARN msg=computation input=-5 output.none={}
}
// Example_none_formatting demonstrates formatting of None values.
func Example_none_formatting() {
none := O.None[string]()
fmt.Printf("String(): %s\n", none.String())
fmt.Printf("GoString(): %s\n", none.GoString())
fmt.Printf("%%v: %v\n", none)
fmt.Printf("%%#v: %#v\n", none)
// Output:
// String(): None[string]
// GoString(): option.None[string]
// %v: None[string]
// %#v: option.None[string]
}

100
v2/option/format.go Normal file
View File

@@ -0,0 +1,100 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package option
import (
"fmt"
"log/slog"
"github.com/IBM/fp-go/v2/internal/formatting"
)
const (
noneGoTemplate = "option.None[%s]"
someGoTemplate = "option.Some[%s](%#v)"
noneFmtTemplate = "None[%s]"
someFmtTemplate = "Some[%s](%v)"
)
// GoString implements fmt.GoStringer for Option.
// Returns a Go-syntax representation of the Option value.
//
// Example:
//
// Some(42).GoString() // "option.Some[int](42)"
// None[int]().GoString() // "option.None[int]()"
//
//go:noinline
func (s Option[A]) GoString() string {
if s.isSome {
return fmt.Sprintf(someGoTemplate, formatting.TypeInfo(s.value), s.value)
}
return fmt.Sprintf(noneGoTemplate, formatting.TypeInfo(new(A)))
}
// LogValue implements slog.LogValuer for Option.
// Returns a slog.Value that represents the Option for structured logging.
// Returns a group value with "some" key for Some values and "none" key for None values.
//
// Example:
//
// logger := slog.Default()
// result := Some(42)
// logger.Info("result", "value", result)
// // Logs: {"msg":"result","value":{"some":42}}
//
// empty := None[int]()
// logger.Info("empty", "value", empty)
// // Logs: {"msg":"empty","value":{"none":{}}}
//
//go:noinline
func (s Option[A]) LogValue() slog.Value {
if s.isSome {
return slog.GroupValue(slog.Any("some", s.value))
}
return slog.GroupValue(slog.Any("none", struct{}{}))
}
// Format implements fmt.Formatter for Option.
// Supports all standard format verbs:
// - %s, %v, %+v: uses String() representation
// - %#v: uses GoString() representation
// - %q: quoted String() representation
// - other verbs: uses String() representation
//
// Example:
//
// opt := Some(42)
// fmt.Printf("%s", opt) // "Some[int](42)"
// fmt.Printf("%v", opt) // "Some[int](42)"
// fmt.Printf("%#v", opt) // "option.Some[int](42)"
//
//go:noinline
func (s Option[A]) Format(f fmt.State, c rune) {
formatting.FmtString(s, f, c)
}
// optString prints some debug info for the object
//
//go:noinline
func optString(isSome bool, value any) string {
if isSome {
return fmt.Sprintf(someFmtTemplate, formatting.TypeInfo(value), value)
}
// For None, just show the type without ()
return fmt.Sprintf(noneFmtTemplate, formatting.TypeInfo(value))
}

307
v2/option/format_test.go Normal file
View File

@@ -0,0 +1,307 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package option
import (
"bytes"
"fmt"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
)
func TestString(t *testing.T) {
t.Run("Some value", func(t *testing.T) {
opt := Some(42)
result := opt.String()
assert.Equal(t, "Some[int](42)", result)
})
t.Run("None value", func(t *testing.T) {
opt := None[int]()
result := opt.String()
assert.Equal(t, "None[int]", result)
})
t.Run("Some with string", func(t *testing.T) {
opt := Some("hello")
result := opt.String()
assert.Equal(t, "Some[string](hello)", result)
})
t.Run("None with string", func(t *testing.T) {
opt := None[string]()
result := opt.String()
assert.Equal(t, "None[string]", result)
})
}
func TestGoString(t *testing.T) {
t.Run("Some value", func(t *testing.T) {
opt := Some(42)
result := opt.GoString()
assert.Contains(t, result, "option.Some")
assert.Contains(t, result, "42")
})
t.Run("None value", func(t *testing.T) {
opt := None[int]()
result := opt.GoString()
assert.Contains(t, result, "option.None")
assert.Contains(t, result, "int")
})
t.Run("Some with struct", func(t *testing.T) {
type TestStruct struct {
Name string
Age int
}
opt := Some(TestStruct{Name: "Alice", Age: 30})
result := opt.GoString()
assert.Contains(t, result, "option.Some")
assert.Contains(t, result, "Alice")
assert.Contains(t, result, "30")
})
t.Run("None with custom type", func(t *testing.T) {
opt := None[string]()
result := opt.GoString()
assert.Contains(t, result, "option.None")
assert.Contains(t, result, "string")
})
}
func TestFormatInterface(t *testing.T) {
t.Run("Some value with %s", func(t *testing.T) {
opt := Some(42)
result := fmt.Sprintf("%s", opt)
assert.Equal(t, "Some[int](42)", result)
})
t.Run("None value with %s", func(t *testing.T) {
opt := None[int]()
result := fmt.Sprintf("%s", opt)
assert.Equal(t, "None[int]", result)
})
t.Run("Some value with %v", func(t *testing.T) {
opt := Some(42)
result := fmt.Sprintf("%v", opt)
assert.Equal(t, "Some[int](42)", result)
})
t.Run("None value with %v", func(t *testing.T) {
opt := None[string]()
result := fmt.Sprintf("%v", opt)
assert.Equal(t, "None[string]", result)
})
t.Run("Some value with %+v", func(t *testing.T) {
opt := Some(42)
result := fmt.Sprintf("%+v", opt)
assert.Contains(t, result, "Some")
assert.Contains(t, result, "42")
})
t.Run("Some value with %#v (GoString)", func(t *testing.T) {
opt := Some(42)
result := fmt.Sprintf("%#v", opt)
assert.Contains(t, result, "option.Some")
assert.Contains(t, result, "42")
})
t.Run("None value with %#v (GoString)", func(t *testing.T) {
opt := None[int]()
result := fmt.Sprintf("%#v", opt)
assert.Contains(t, result, "option.None")
assert.Contains(t, result, "int")
})
t.Run("Some value with %q", func(t *testing.T) {
opt := Some("hello")
result := fmt.Sprintf("%q", opt)
assert.Contains(t, result, "Some")
})
t.Run("Some value with %T", func(t *testing.T) {
opt := Some(42)
result := fmt.Sprintf("%T", opt)
assert.Contains(t, result, "option.Option")
})
}
func TestLogValue(t *testing.T) {
t.Run("Some value", func(t *testing.T) {
opt := Some(42)
logValue := opt.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 1)
assert.Equal(t, "some", attrs[0].Key)
assert.Equal(t, int64(42), attrs[0].Value.Any())
})
t.Run("None value", func(t *testing.T) {
opt := None[int]()
logValue := opt.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 1)
assert.Equal(t, "none", attrs[0].Key)
// Value should be struct{}{}
assert.Equal(t, struct{}{}, attrs[0].Value.Any())
})
t.Run("Some with string", func(t *testing.T) {
opt := Some("success")
logValue := opt.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 1)
assert.Equal(t, "some", attrs[0].Key)
assert.Equal(t, "success", attrs[0].Value.Any())
})
t.Run("None with string", func(t *testing.T) {
opt := None[string]()
logValue := opt.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 1)
assert.Equal(t, "none", attrs[0].Key)
assert.Equal(t, struct{}{}, attrs[0].Value.Any())
})
t.Run("Integration with slog - Some", func(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
opt := Some(42)
logger.Info("test message", "result", opt)
output := buf.String()
assert.Contains(t, output, "test message")
assert.Contains(t, output, "result")
assert.Contains(t, output, "some")
assert.Contains(t, output, "42")
})
t.Run("Integration with slog - None", func(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
opt := None[int]()
logger.Info("test message", "result", opt)
output := buf.String()
assert.Contains(t, output, "test message")
assert.Contains(t, output, "result")
assert.Contains(t, output, "none")
})
}
func TestFormatComprehensive(t *testing.T) {
t.Run("All format verbs for Some", func(t *testing.T) {
opt := Some(42)
tests := []struct {
verb string
contains []string
}{
{"%s", []string{"Some", "42"}},
{"%v", []string{"Some", "42"}},
{"%+v", []string{"Some", "42"}},
{"%#v", []string{"option.Some", "42"}},
{"%T", []string{"option.Option"}},
}
for _, tt := range tests {
t.Run(tt.verb, func(t *testing.T) {
result := fmt.Sprintf(tt.verb, opt)
for _, substr := range tt.contains {
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
}
})
}
})
t.Run("All format verbs for None", func(t *testing.T) {
opt := None[int]()
tests := []struct {
verb string
contains []string
}{
{"%s", []string{"None", "int"}},
{"%v", []string{"None", "int"}},
{"%+v", []string{"None", "int"}},
{"%#v", []string{"option.None", "int"}},
{"%T", []string{"option.Option"}},
}
for _, tt := range tests {
t.Run(tt.verb, func(t *testing.T) {
result := fmt.Sprintf(tt.verb, opt)
for _, substr := range tt.contains {
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
}
})
}
})
}
func TestInterfaceImplementations(t *testing.T) {
t.Run("fmt.Stringer interface", func(t *testing.T) {
var _ fmt.Stringer = Some(42)
var _ fmt.Stringer = None[int]()
})
t.Run("fmt.GoStringer interface", func(t *testing.T) {
var _ fmt.GoStringer = Some(42)
var _ fmt.GoStringer = None[int]()
})
t.Run("fmt.Formatter interface", func(t *testing.T) {
var _ fmt.Formatter = Some(42)
var _ fmt.Formatter = None[int]()
})
t.Run("slog.LogValuer interface", func(t *testing.T) {
var _ slog.LogValuer = Some(42)
var _ slog.LogValuer = None[int]()
})
}

View File

@@ -59,6 +59,11 @@ func FromEq[A any](pred eq.Eq[A]) func(A) Kleisli[A, A] {
return F.Flow2(P.IsEqual(pred), FromPredicate[A])
}
//go:inline
func FromStrictEq[A comparable]() func(A) Kleisli[A, A] {
return FromEq(eq.FromStrictEquals[A]())
}
// FromNillable converts a pointer to an Option.
// Returns Some if the pointer is non-nil, None otherwise.
//

View File

@@ -0,0 +1,175 @@
// Copyright (c) 2024 - 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 pair_test
import (
"errors"
"fmt"
"log/slog"
"os"
P "github.com/IBM/fp-go/v2/pair"
)
// ExamplePair_String demonstrates the fmt.Stringer interface implementation.
func ExamplePair_String() {
p1 := P.MakePair("username", 42)
p2 := P.MakePair(100, "active")
fmt.Println(p1.String())
fmt.Println(p2.String())
// Output:
// Pair[string, int](username, 42)
// Pair[int, string](100, active)
}
// ExamplePair_GoString demonstrates the fmt.GoStringer interface implementation.
func ExamplePair_GoString() {
p1 := P.MakePair("key", 42)
p2 := P.MakePair(errors.New("error"), "value")
fmt.Printf("%#v\n", p1)
fmt.Printf("%#v\n", p2)
// Output:
// pair.MakePair[string, int]("key", 42)
// pair.MakePair[error, string](&errors.errorString{s:"error"}, "value")
}
// ExamplePair_Format demonstrates the fmt.Formatter interface implementation.
func ExamplePair_Format() {
p := P.MakePair("config", 8080)
// Different format verbs
fmt.Printf("%%s: %s\n", p)
fmt.Printf("%%v: %v\n", p)
fmt.Printf("%%+v: %+v\n", p)
fmt.Printf("%%#v: %#v\n", p)
// Output:
// %s: Pair[string, int](config, 8080)
// %v: Pair[string, int](config, 8080)
// %+v: Pair[string, int](config, 8080)
// %#v: pair.MakePair[string, int]("config", 8080)
}
// ExamplePair_LogValue demonstrates the slog.LogValuer interface implementation.
func ExamplePair_LogValue() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Remove time for consistent output
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
// Pair with string and int
p1 := P.MakePair("username", 42)
logger.Info("user data", "data", p1)
// Pair with error and string
p2 := P.MakePair(errors.New("connection failed"), "retry")
logger.Error("operation failed", "status", p2)
// Output:
// level=INFO msg="user data" data.head=username data.tail=42
// level=ERROR msg="operation failed" status.head="connection failed" status.tail=retry
}
// ExamplePair_formatting_comparison demonstrates different formatting options.
func ExamplePair_formatting_comparison() {
type Config struct {
Host string
Port int
}
config := Config{Host: "localhost", Port: 8080}
p := P.MakePair(config, []string{"api", "web"})
fmt.Printf("String(): %s\n", p.String())
fmt.Printf("GoString(): %s\n", p.GoString())
fmt.Printf("%%v: %v\n", p)
fmt.Printf("%%#v: %#v\n", p)
// Output:
// String(): Pair[pair_test.Config, []string]({localhost 8080}, [api web])
// GoString(): pair.MakePair[pair_test.Config, []string](pair_test.Config{Host:"localhost", Port:8080}, []string{"api", "web"})
// %v: Pair[pair_test.Config, []string]({localhost 8080}, [api web])
// %#v: pair.MakePair[pair_test.Config, []string](pair_test.Config{Host:"localhost", Port:8080}, []string{"api", "web"})
}
// ExamplePair_LogValue_structured demonstrates structured logging with Pair.
func ExamplePair_LogValue_structured() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
// Simulate a key-value store operation
operation := func(key string, value int) P.Pair[string, int] {
return P.MakePair(key, value)
}
// Log successful operation
result1 := operation("counter", 42)
logger.Info("store operation", "key", "counter", "result", result1)
// Log another operation
result2 := operation("timeout", 30)
logger.Info("store operation", "key", "timeout", "result", result2)
// Output:
// level=INFO msg="store operation" key=counter result.head=counter result.tail=42
// level=INFO msg="store operation" key=timeout result.head=timeout result.tail=30
}
// ExamplePair_formatting_with_maps demonstrates formatting pairs containing maps.
func ExamplePair_formatting_with_maps() {
metadata := map[string]string{
"version": "1.0",
"author": "Alice",
}
p := P.MakePair("config", metadata)
fmt.Printf("%%v: %v\n", p)
fmt.Printf("%%s: %s\n", p)
// Output:
// %v: Pair[string, map[string]string](config, map[author:Alice version:1.0])
// %s: Pair[string, map[string]string](config, map[author:Alice version:1.0])
}
// ExamplePair_formatting_nested demonstrates formatting nested pairs.
func ExamplePair_formatting_nested() {
inner := P.MakePair("inner", 10)
outer := P.MakePair(inner, "outer")
fmt.Printf("%%v: %v\n", outer)
fmt.Printf("%%#v: %#v\n", outer)
// Output:
// %v: Pair[pair.Pair[string,int], string](Pair[string, int](inner, 10), outer)
// %#v: pair.MakePair[pair.Pair[string,int], string](pair.MakePair[string, int]("inner", 10), "outer")
}

89
v2/pair/format.go Normal file
View File

@@ -0,0 +1,89 @@
// Copyright (c) 2024 - 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 pair
import (
"fmt"
"log/slog"
"github.com/IBM/fp-go/v2/internal/formatting"
)
const (
pairGoTemplate = "pair.MakePair[%s, %s](%#v, %#v)"
pairFmtTemplate = "Pair[%T, %T](%v, %v)"
)
func goString[L, R any](l L, r R) string {
return fmt.Sprintf(pairGoTemplate, formatting.TypeInfo(new(L)), formatting.TypeInfo(new(R)), l, r)
}
// String prints some debug info for the object
//
//go:noinline
func (p Pair[L, R]) String() string {
return fmt.Sprintf(pairFmtTemplate, p.l, p.r, p.l, p.r)
}
// Format implements fmt.Formatter for Pair.
// Supports all standard format verbs:
// - %s, %v, %+v: uses String() representation
// - %#v: uses GoString() representation
// - %q: quoted String() representation
// - other verbs: uses String() representation
//
// Example:
//
// p := pair.MakePair("key", 42)
// fmt.Printf("%s", p) // "Pair[string, int](key, 42)"
// fmt.Printf("%v", p) // "Pair[string, int](key, 42)"
// fmt.Printf("%#v", p) // "pair.MakePair[string, int]("key", 42)"
//
//go:noinline
func (p Pair[L, R]) Format(f fmt.State, c rune) {
formatting.FmtString(p, f, c)
}
// GoString implements fmt.GoStringer for Pair.
// Returns a Go-syntax representation of the Pair value.
//
// Example:
//
// pair.MakePair("key", 42).GoString() // "pair.MakePair[string, int]("key", 42)"
//
//go:noinline
func (p Pair[L, R]) GoString() string {
return goString(p.l, p.r)
}
// LogValue implements slog.LogValuer for Pair.
// Returns a slog.Value that represents the Pair for structured logging.
// Returns a group value with "head" and "tail" keys.
//
// Example:
//
// logger := slog.Default()
// p := pair.MakePair("key", 42)
// logger.Info("pair value", "data", p)
// // Logs: {"msg":"pair value","data":{"head":"key","tail":42}}
//
//go:noinline
func (p Pair[L, R]) LogValue() slog.Value {
return slog.GroupValue(
slog.Any("head", p.l),
slog.Any("tail", p.r),
)
}

272
v2/pair/format_test.go Normal file
View File

@@ -0,0 +1,272 @@
// Copyright (c) 2024 - 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 pair
import (
"bytes"
"errors"
"fmt"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
)
func TestString(t *testing.T) {
t.Run("Pair with int and string", func(t *testing.T) {
p := MakePair("hello", 42)
result := p.String()
assert.Equal(t, "Pair[string, int](hello, 42)", result)
})
t.Run("Pair with string and string", func(t *testing.T) {
p := MakePair("key", "value")
result := p.String()
assert.Equal(t, "Pair[string, string](key, value)", result)
})
t.Run("Pair with error", func(t *testing.T) {
p := MakePair(errors.New("test error"), 42)
result := p.String()
assert.Contains(t, result, "Pair[*errors.errorString, int]")
assert.Contains(t, result, "test error")
})
t.Run("Pair with struct", func(t *testing.T) {
type User struct {
Name string
Age int
}
p := MakePair(User{Name: "Alice", Age: 30}, "active")
result := p.String()
assert.Contains(t, result, "Pair")
assert.Contains(t, result, "Alice")
assert.Contains(t, result, "30")
})
}
func TestGoString(t *testing.T) {
t.Run("Pair with int and string", func(t *testing.T) {
p := MakePair("hello", 42)
result := p.GoString()
assert.Contains(t, result, "pair.MakePair")
assert.Contains(t, result, "string")
assert.Contains(t, result, "int")
assert.Contains(t, result, "hello")
assert.Contains(t, result, "42")
})
t.Run("Pair with error", func(t *testing.T) {
p := MakePair(errors.New("test error"), 42)
result := p.GoString()
assert.Contains(t, result, "pair.MakePair")
assert.Contains(t, result, "test error")
})
t.Run("Pair with struct", func(t *testing.T) {
type TestStruct struct {
Name string
Age int
}
p := MakePair(TestStruct{Name: "Bob", Age: 25}, 100)
result := p.GoString()
assert.Contains(t, result, "pair.MakePair")
assert.Contains(t, result, "Bob")
assert.Contains(t, result, "25")
assert.Contains(t, result, "100")
})
}
func TestFormatInterface(t *testing.T) {
t.Run("Pair with %s", func(t *testing.T) {
p := MakePair("key", 42)
result := fmt.Sprintf("%s", p)
assert.Equal(t, "Pair[string, int](key, 42)", result)
})
t.Run("Pair with %v", func(t *testing.T) {
p := MakePair("key", 42)
result := fmt.Sprintf("%v", p)
assert.Equal(t, "Pair[string, int](key, 42)", result)
})
t.Run("Pair with %+v", func(t *testing.T) {
p := MakePair("key", 42)
result := fmt.Sprintf("%+v", p)
assert.Contains(t, result, "Pair")
assert.Contains(t, result, "key")
assert.Contains(t, result, "42")
})
t.Run("Pair with %#v (GoString)", func(t *testing.T) {
p := MakePair("key", 42)
result := fmt.Sprintf("%#v", p)
assert.Contains(t, result, "pair.MakePair")
assert.Contains(t, result, "key")
assert.Contains(t, result, "42")
})
t.Run("Pair with %q", func(t *testing.T) {
p := MakePair("key", "value")
result := fmt.Sprintf("%q", p)
// Should use String() representation
assert.Contains(t, result, "Pair")
})
t.Run("Pair with %T", func(t *testing.T) {
p := MakePair("key", 42)
result := fmt.Sprintf("%T", p)
assert.Contains(t, result, "pair.Pair")
})
}
func TestLogValue(t *testing.T) {
t.Run("Pair with int and string", func(t *testing.T) {
p := MakePair("key", 42)
logValue := p.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 2)
assert.Equal(t, "head", attrs[0].Key)
assert.Equal(t, "key", attrs[0].Value.Any())
assert.Equal(t, "tail", attrs[1].Key)
assert.Equal(t, int64(42), attrs[1].Value.Any())
})
t.Run("Pair with error", func(t *testing.T) {
p := MakePair(errors.New("test error"), "value")
logValue := p.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 2)
assert.Equal(t, "head", attrs[0].Key)
assert.NotNil(t, attrs[0].Value.Any())
assert.Equal(t, "tail", attrs[1].Key)
assert.Equal(t, "value", attrs[1].Value.Any())
})
t.Run("Pair with strings", func(t *testing.T) {
p := MakePair("first", "second")
logValue := p.LogValue()
// Should be a group value
assert.Equal(t, slog.KindGroup, logValue.Kind())
// Extract the group attributes
attrs := logValue.Group()
assert.Len(t, attrs, 2)
assert.Equal(t, "head", attrs[0].Key)
assert.Equal(t, "first", attrs[0].Value.Any())
assert.Equal(t, "tail", attrs[1].Key)
assert.Equal(t, "second", attrs[1].Value.Any())
})
t.Run("Integration with slog", func(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
p := MakePair("username", 42)
logger.Info("test message", "data", p)
output := buf.String()
assert.Contains(t, output, "test message")
assert.Contains(t, output, "data")
assert.Contains(t, output, "head")
assert.Contains(t, output, "username")
assert.Contains(t, output, "tail")
assert.Contains(t, output, "42")
})
}
func TestFormatComprehensive(t *testing.T) {
t.Run("All format verbs", func(t *testing.T) {
p := MakePair("key", 42)
tests := []struct {
verb string
contains []string
}{
{"%s", []string{"Pair", "key", "42"}},
{"%v", []string{"Pair", "key", "42"}},
{"%+v", []string{"Pair", "key", "42"}},
{"%#v", []string{"pair.MakePair", "key", "42"}},
{"%T", []string{"pair.Pair"}},
}
for _, tt := range tests {
t.Run(tt.verb, func(t *testing.T) {
result := fmt.Sprintf(tt.verb, p)
for _, substr := range tt.contains {
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
}
})
}
})
t.Run("Complex types", func(t *testing.T) {
type Config struct {
Host string
Port int
}
p := MakePair(Config{Host: "localhost", Port: 8080}, []string{"a", "b", "c"})
tests := []struct {
verb string
contains []string
}{
{"%s", []string{"Pair", "localhost", "8080"}},
{"%v", []string{"Pair", "localhost", "8080"}},
{"%#v", []string{"pair.MakePair", "localhost", "8080"}},
}
for _, tt := range tests {
t.Run(tt.verb, func(t *testing.T) {
result := fmt.Sprintf(tt.verb, p)
for _, substr := range tt.contains {
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
}
})
}
})
}
func TestInterfaceImplementations(t *testing.T) {
t.Run("fmt.Stringer interface", func(t *testing.T) {
var _ fmt.Stringer = MakePair("key", 42)
})
t.Run("fmt.GoStringer interface", func(t *testing.T) {
var _ fmt.GoStringer = MakePair("key", 42)
})
t.Run("fmt.Formatter interface", func(t *testing.T) {
var _ fmt.Formatter = MakePair("key", 42)
})
t.Run("slog.LogValuer interface", func(t *testing.T) {
var _ slog.LogValuer = MakePair("key", 42)
})
}

View File

@@ -16,27 +16,10 @@
package pair
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/tuple"
)
// String prints some debug info for the object
func (s Pair[A, B]) String() string {
return fmt.Sprintf("Pair[%T, %T](%v, %v)", s.l, s.r, s.l, s.r)
}
// Format prints some debug info for the object
func (s Pair[A, B]) Format(f fmt.State, c rune) {
switch c {
case 's':
fmt.Fprint(f, s.String())
default:
fmt.Fprint(f, s.String())
}
}
// Of creates a [Pair] with the same value in both the head and tail positions.
//
// Example:

View File

@@ -367,22 +367,6 @@ func TestFromStrictEquals(t *testing.T) {
assert.False(t, pairEq.Equals(p1, p3))
}
func TestString(t *testing.T) {
p := MakePair("hello", 42)
str := p.String()
assert.Contains(t, str, "Pair")
assert.Contains(t, str, "hello")
assert.Contains(t, str, "42")
}
func TestFormat(t *testing.T) {
p := MakePair("test", 100)
str := fmt.Sprintf("%s", p)
assert.Contains(t, str, "Pair")
assert.Contains(t, str, "test")
assert.Contains(t, str, "100")
}
func TestMonadHead(t *testing.T) {
stringMonoid := S.Monoid
monad := MonadHead[int, string, string](stringMonoid)

32
v2/reader/bracket.go Normal file
View File

@@ -0,0 +1,32 @@
package reader
import (
G "github.com/IBM/fp-go/v2/internal/bracket"
)
//go:inline
func Bracket[
R, A, B, ANY any](
acquire Reader[R, A],
use Kleisli[R, A, B],
release func(A, B) Reader[R, ANY],
) Reader[R, B] {
return G.Bracket[
Reader[R, A],
Reader[R, B],
Reader[R, ANY],
B,
A,
B,
](
Of[R, B],
MonadChain[R, A, B],
MonadChain[R, B, B],
MonadChain[R, ANY, B],
acquire,
use,
release,
)
}

View File

@@ -15,24 +15,26 @@
package readereither
import "github.com/IBM/fp-go/v2/either"
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/tailrec"
)
//go:inline
func TailRec[R, E, A, B any](f Kleisli[R, E, A, Either[A, B]]) Kleisli[R, E, A, B] {
func TailRec[R, E, A, B any](f Kleisli[R, E, A, tailrec.Trampoline[A, B]]) Kleisli[R, E, A, B] {
return func(a A) ReaderEither[R, E, B] {
initialReader := f(a)
return func(r R) Either[E, B] {
return func(r R) either.Either[E, B] {
current := initialReader(r)
for {
rec, e := either.Unwrap(current)
if either.IsLeft(current) {
return either.Left[B](e)
}
b, a := either.Unwrap(rec)
if either.IsRight(rec) {
return either.Right[E](b)
if rec.Landed {
return either.Right[E](rec.Land)
}
current = f(a)(r)
current = f(rec.Bounce)(r)
}
}
}

View File

@@ -27,7 +27,6 @@
// - Maintain functional composition and testability
//
// # Logging Use Case
//
// ReaderIO is especially well-suited for logging because it allows you to:
// - Pass a logger through your computation chain without explicit parameter threading
// - Compose logging operations with other side effects
@@ -93,6 +92,7 @@ package readerio
import (
"sync"
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/chain"
@@ -203,7 +203,7 @@ func MonadMap[R, A, B any](fa ReaderIO[R, A], f func(A) B) ReaderIO[R, B] {
// replaced := readerio.MonadMapTo(logAndCompute, "done")
// result := replaced(config)() // Prints "Computing...", returns "done"
func MonadMapTo[R, A, B any](fa ReaderIO[R, A], b B) ReaderIO[R, B] {
return MonadMap(fa, function.Constant1[A](b))
return MonadMap(fa, reader.Of[A](b))
}
// Map creates a function that applies a transformation to a ReaderIO value.
@@ -262,7 +262,7 @@ func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
// readerio.MapTo[Config, int]("complete"),
// )(config)() // Prints "Step executed", returns "complete"
func MapTo[R, A, B any](b B) Operator[R, A, B] {
return Map[R](function.Constant1[A](b))
return Map[R](reader.Of[A](b))
}
// MonadChain sequences two ReaderIO computations, where the second depends on the result of the first.
@@ -1111,3 +1111,17 @@ func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
func Read[A, R any](r R) func(ReaderIO[R, A]) IO[A] {
return reader.Read[IO[A]](r)
}
// Delay creates an operation that passes in the value after some delay
//
//go:inline
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
return function.Bind2nd(function.Flow2[ReaderIO[R, A]], io.Delay[A](delay))
}
// After creates an operation that passes after the given [time.Time]
//
//go:inline
func After[R, A any](timestamp time.Time) Operator[R, A, A] {
return function.Bind2nd(function.Flow2[ReaderIO[R, A]], io.After[A](timestamp))
}

View File

@@ -1,7 +1,5 @@
package readerio
import "github.com/IBM/fp-go/v2/either"
// TailRec implements stack-safe tail recursion for the ReaderIO monad.
//
// This function enables recursive computations that depend on an environment (Reader aspect)
@@ -10,12 +8,12 @@ import "github.com/IBM/fp-go/v2/either"
//
// # How It Works
//
// TailRec takes a Kleisli arrow that returns Either[A, B]:
// - Left(A): Continue recursion with the new state A
// - Right(B): Terminate recursion and return the final result B
// TailRec takes a Kleisli arrow that returns Trampoline[A, B]:
// - Bounce(A): Continue recursion with the new state A
// - Land(B): Terminate recursion and return the final result B
//
// The function iteratively applies the Kleisli arrow, passing the environment R to each
// iteration, until a Right(B) value is produced. This combines:
// iteration, until a Land(B) value is produced. This combines:
// - Environment dependency (Reader monad): Access to configuration, context, or dependencies
// - Side effects (IO monad): Logging, file I/O, network calls, etc.
// - Stack safety: Iterative execution prevents stack overflow
@@ -29,9 +27,9 @@ import "github.com/IBM/fp-go/v2/either"
// # Parameters
//
// - f: A Kleisli arrow (A => ReaderIO[R, Either[A, B]]) that:
// * Takes the current state A
// * Returns a ReaderIO that depends on environment R
// * Produces Either[A, B] to control recursion flow
// - Takes the current state A
// - Returns a ReaderIO that depends on environment R
// - Produces Either[A, B] to control recursion flow
//
// # Returns
//
@@ -117,13 +115,13 @@ import "github.com/IBM/fp-go/v2/either"
// (thousands or millions of iterations) will not cause stack overflow:
//
// // Safe for very large inputs
// sumToZero := readerio.TailRec(func(n int) readerio.ReaderIO[Env, either.Either[int, int]] {
// return func(env Env) io.IO[either.Either[int, int]] {
// return func() either.Either[int, int] {
// sumToZero := readerio.TailRec(func(n int) readerio.ReaderIO[Env, tailrec.Trampoline[int, int]] {
// return func(env Env) io.IO[tailrec.Trampoline[int, int]] {
// return func() tailrec.Trampoline[int, int] {
// if n <= 0 {
// return either.Right[int](0)
// return tailrec.Land[int](0)
// }
// return either.Left[int](n - 1)
// return tailrec.Bounce[int](n - 1)
// }
// }
// })
@@ -143,7 +141,7 @@ import "github.com/IBM/fp-go/v2/either"
// - [Chain]: For sequencing ReaderIO computations
// - [Ask]: For accessing the environment
// - [Asks]: For extracting values from the environment
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
func TailRec[R, A, B any](f Kleisli[R, A, Trampoline[A, B]]) Kleisli[R, A, B] {
return func(a A) ReaderIO[R, B] {
initialReader := f(a)
return func(r R) IO[B] {
@@ -151,11 +149,10 @@ func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
return func() B {
current := initialB()
for {
b, a := either.Unwrap(current)
if either.IsRight(current) {
return b
if current.Landed {
return current.Land
}
current = f(a)(r)()
current = f(current.Bounce)(r)()
}
}
}

View File

@@ -20,8 +20,8 @@ import (
"testing"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
G "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
@@ -54,15 +54,15 @@ func TestTailRecFactorial(t *testing.T) {
},
}
factorialStep := func(state State) ReaderIO[LoggerEnv, E.Either[State, int]] {
return func(env LoggerEnv) G.IO[E.Either[State, int]] {
return func() E.Either[State, int] {
factorialStep := func(state State) ReaderIO[LoggerEnv, Trampoline[State, int]] {
return func(env LoggerEnv) G.IO[Trampoline[State, int]] {
return func() Trampoline[State, int] {
if state.n <= 0 {
env.Logger(fmt.Sprintf("Complete: %d", state.acc))
return E.Right[State](state.acc)
return tailrec.Land[State](state.acc)
}
env.Logger(fmt.Sprintf("Step: %d * %d", state.n, state.acc))
return E.Left[int](State{state.n - 1, state.acc * state.n})
return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n})
}
}
}
@@ -86,13 +86,13 @@ func TestTailRecFibonacci(t *testing.T) {
env := TestEnv{Multiplier: 1, Logs: []string{}}
fibStep := func(state State) ReaderIO[TestEnv, E.Either[State, int]] {
return func(env TestEnv) G.IO[E.Either[State, int]] {
return func() E.Either[State, int] {
fibStep := func(state State) ReaderIO[TestEnv, Trampoline[State, int]] {
return func(env TestEnv) G.IO[Trampoline[State, int]] {
return func() Trampoline[State, int] {
if state.n <= 0 {
return E.Right[State](state.curr * env.Multiplier)
return tailrec.Land[State](state.curr * env.Multiplier)
}
return E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr})
return tailrec.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr})
}
}
}
@@ -107,13 +107,13 @@ func TestTailRecFibonacci(t *testing.T) {
func TestTailRecCountdown(t *testing.T) {
config := ConfigEnv{MinValue: 0, Step: 2}
countdownStep := func(n int) ReaderIO[ConfigEnv, E.Either[int, int]] {
return func(cfg ConfigEnv) G.IO[E.Either[int, int]] {
return func() E.Either[int, int] {
countdownStep := func(n int) ReaderIO[ConfigEnv, Trampoline[int, int]] {
return func(cfg ConfigEnv) G.IO[Trampoline[int, int]] {
return func() Trampoline[int, int] {
if n <= cfg.MinValue {
return E.Right[int](n)
return tailrec.Land[int](n)
}
return E.Left[int](n - cfg.Step)
return tailrec.Bounce[int](n - cfg.Step)
}
}
}
@@ -128,13 +128,13 @@ func TestTailRecCountdown(t *testing.T) {
func TestTailRecCountdownOddStep(t *testing.T) {
config := ConfigEnv{MinValue: 0, Step: 3}
countdownStep := func(n int) ReaderIO[ConfigEnv, E.Either[int, int]] {
return func(cfg ConfigEnv) G.IO[E.Either[int, int]] {
return func() E.Either[int, int] {
countdownStep := func(n int) ReaderIO[ConfigEnv, Trampoline[int, int]] {
return func(cfg ConfigEnv) G.IO[Trampoline[int, int]] {
return func() Trampoline[int, int] {
if n <= cfg.MinValue {
return E.Right[int](n)
return tailrec.Land[int](n)
}
return E.Left[int](n - cfg.Step)
return tailrec.Bounce[int](n - cfg.Step)
}
}
}
@@ -154,13 +154,13 @@ func TestTailRecSumList(t *testing.T) {
env := TestEnv{Multiplier: 2, Logs: []string{}}
sumStep := func(state State) ReaderIO[TestEnv, E.Either[State, int]] {
return func(env TestEnv) G.IO[E.Either[State, int]] {
return func() E.Either[State, int] {
sumStep := func(state State) ReaderIO[TestEnv, Trampoline[State, int]] {
return func(env TestEnv) G.IO[Trampoline[State, int]] {
return func() Trampoline[State, int] {
if A.IsEmpty(state.list) {
return E.Right[State](state.sum * env.Multiplier)
return tailrec.Land[State](state.sum * env.Multiplier)
}
return E.Left[int](State{state.list[1:], state.sum + state.list[0]})
return tailrec.Bounce[int](State{state.list[1:], state.sum + state.list[0]})
}
}
}
@@ -175,10 +175,10 @@ func TestTailRecSumList(t *testing.T) {
func TestTailRecImmediateTermination(t *testing.T) {
env := TestEnv{Multiplier: 1, Logs: []string{}}
immediateStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
return func(env TestEnv) G.IO[E.Either[int, int]] {
return func() E.Either[int, int] {
return E.Right[int](n * env.Multiplier)
immediateStep := func(n int) ReaderIO[TestEnv, Trampoline[int, int]] {
return func(env TestEnv) G.IO[Trampoline[int, int]] {
return func() Trampoline[int, int] {
return tailrec.Land[int](n * env.Multiplier)
}
}
}
@@ -193,13 +193,13 @@ func TestTailRecImmediateTermination(t *testing.T) {
func TestTailRecStackSafety(t *testing.T) {
env := TestEnv{Multiplier: 1, Logs: []string{}}
countdownStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
return func(env TestEnv) G.IO[E.Either[int, int]] {
return func() E.Either[int, int] {
countdownStep := func(n int) ReaderIO[TestEnv, Trampoline[int, int]] {
return func(env TestEnv) G.IO[Trampoline[int, int]] {
return func() Trampoline[int, int] {
if n <= 0 {
return E.Right[int](n)
return tailrec.Land[int](n)
}
return E.Left[int](n - 1)
return tailrec.Bounce[int](n - 1)
}
}
}
@@ -223,16 +223,16 @@ func TestTailRecFindInRange(t *testing.T) {
env := FindEnv{Target: 42}
findStep := func(state State) ReaderIO[FindEnv, E.Either[State, int]] {
return func(env FindEnv) G.IO[E.Either[State, int]] {
return func() E.Either[State, int] {
findStep := func(state State) ReaderIO[FindEnv, Trampoline[State, int]] {
return func(env FindEnv) G.IO[Trampoline[State, int]] {
return func() Trampoline[State, int] {
if state.current >= state.max {
return E.Right[State](-1) // Not found
return tailrec.Land[State](-1) // Not found
}
if state.current == env.Target {
return E.Right[State](state.current) // Found
return tailrec.Land[State](state.current) // Found
}
return E.Left[int](State{state.current + 1, state.max})
return tailrec.Bounce[int](State{state.current + 1, state.max})
}
}
}
@@ -256,16 +256,16 @@ func TestTailRecFindNotInRange(t *testing.T) {
env := FindEnv{Target: 200}
findStep := func(state State) ReaderIO[FindEnv, E.Either[State, int]] {
return func(env FindEnv) G.IO[E.Either[State, int]] {
return func() E.Either[State, int] {
findStep := func(state State) ReaderIO[FindEnv, Trampoline[State, int]] {
return func(env FindEnv) G.IO[Trampoline[State, int]] {
return func() Trampoline[State, int] {
if state.current >= state.max {
return E.Right[State](-1) // Not found
return tailrec.Land[State](-1) // Not found
}
if state.current == env.Target {
return E.Right[State](state.current) // Found
return tailrec.Land[State](state.current) // Found
}
return E.Left[int](State{state.current + 1, state.max})
return tailrec.Bounce[int](State{state.current + 1, state.max})
}
}
}
@@ -285,14 +285,14 @@ func TestTailRecWithLogging(t *testing.T) {
},
}
countdownStep := func(n int) ReaderIO[LoggerEnv, E.Either[int, int]] {
return func(env LoggerEnv) G.IO[E.Either[int, int]] {
return func() E.Either[int, int] {
countdownStep := func(n int) ReaderIO[LoggerEnv, Trampoline[int, int]] {
return func(env LoggerEnv) G.IO[Trampoline[int, int]] {
return func() Trampoline[int, int] {
env.Logger(fmt.Sprintf("Count: %d", n))
if n <= 0 {
return E.Right[int](n)
return tailrec.Land[int](n)
}
return E.Left[int](n - 1)
return tailrec.Bounce[int](n - 1)
}
}
}
@@ -315,17 +315,17 @@ func TestTailRecCollatzConjecture(t *testing.T) {
},
}
collatzStep := func(n int) ReaderIO[LoggerEnv, E.Either[int, int]] {
return func(env LoggerEnv) G.IO[E.Either[int, int]] {
return func() E.Either[int, int] {
collatzStep := func(n int) ReaderIO[LoggerEnv, Trampoline[int, int]] {
return func(env LoggerEnv) G.IO[Trampoline[int, int]] {
return func() Trampoline[int, int] {
env.Logger(fmt.Sprintf("n=%d", n))
if n <= 1 {
return E.Right[int](n)
return tailrec.Land[int](n)
}
if n%2 == 0 {
return E.Left[int](n / 2)
return tailrec.Bounce[int](n / 2)
}
return E.Left[int](3*n + 1)
return tailrec.Bounce[int](3*n + 1)
}
}
}
@@ -352,13 +352,13 @@ func TestTailRecPowerOfTwo(t *testing.T) {
env := PowerEnv{MaxExponent: 10}
powerStep := func(state State) ReaderIO[PowerEnv, E.Either[State, int]] {
return func(env PowerEnv) G.IO[E.Either[State, int]] {
return func() E.Either[State, int] {
powerStep := func(state State) ReaderIO[PowerEnv, Trampoline[State, int]] {
return func(env PowerEnv) G.IO[Trampoline[State, int]] {
return func() Trampoline[State, int] {
if state.exponent >= env.MaxExponent {
return E.Right[State](state.result)
return tailrec.Land[State](state.result)
}
return E.Left[int](State{state.exponent + 1, state.result * 2})
return tailrec.Bounce[int](State{state.exponent + 1, state.result * 2})
}
}
}
@@ -383,14 +383,14 @@ func TestTailRecGCD(t *testing.T) {
},
}
gcdStep := func(state State) ReaderIO[LoggerEnv, E.Either[State, int]] {
return func(env LoggerEnv) G.IO[E.Either[State, int]] {
return func() E.Either[State, int] {
gcdStep := func(state State) ReaderIO[LoggerEnv, Trampoline[State, int]] {
return func(env LoggerEnv) G.IO[Trampoline[State, int]] {
return func() Trampoline[State, int] {
env.Logger(fmt.Sprintf("gcd(%d, %d)", state.a, state.b))
if state.b == 0 {
return E.Right[State](state.a)
return tailrec.Land[State](state.a)
}
return E.Left[int](State{state.b, state.a % state.b})
return tailrec.Bounce[int](State{state.b, state.a % state.b})
}
}
}
@@ -412,13 +412,13 @@ func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
env := CounterEnv{Increment: 3, Limit: 20}
counterStep := func(n int) ReaderIO[CounterEnv, E.Either[int, int]] {
return func(env CounterEnv) G.IO[E.Either[int, int]] {
return func() E.Either[int, int] {
counterStep := func(n int) ReaderIO[CounterEnv, Trampoline[int, int]] {
return func(env CounterEnv) G.IO[Trampoline[int, int]] {
return func() Trampoline[int, int] {
if n >= env.Limit {
return E.Right[int](n)
return tailrec.Land[int](n)
}
return E.Left[int](n + env.Increment)
return tailrec.Bounce[int](n + env.Increment)
}
}
}
@@ -431,13 +431,13 @@ func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
// TestTailRecDifferentEnvironments tests that different environments produce different results
func TestTailRecDifferentEnvironments(t *testing.T) {
multiplyStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
return func(env TestEnv) G.IO[E.Either[int, int]] {
return func() E.Either[int, int] {
multiplyStep := func(n int) ReaderIO[TestEnv, Trampoline[int, int]] {
return func(env TestEnv) G.IO[Trampoline[int, int]] {
return func() Trampoline[int, int] {
if n <= 0 {
return E.Right[int](n)
return tailrec.Land[int](n)
}
return E.Left[int](n - 1)
return tailrec.Bounce[int](n - 1)
}
}
}

40
v2/readerio/retry.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerio
import (
"github.com/IBM/fp-go/v2/retry"
RG "github.com/IBM/fp-go/v2/retry/generic"
)
//go:inline
func Retrying[R, A any](
policy retry.RetryPolicy,
action Kleisli[R, retry.RetryStatus, A],
check func(A) bool,
) ReaderIO[R, A] {
// get an implementation for the types
return RG.Retrying(
Chain[R, A, A],
Chain[R, retry.RetryStatus, A],
Of[R, A],
Of[R, retry.RetryStatus],
Delay[R, retry.RetryStatus],
policy,
action,
check,
)
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/tailrec"
)
type (
@@ -52,4 +53,6 @@ type (
Operator[R, A, B any] = Kleisli[R, ReaderIO[R, A], B]
Consumer[A any] = consumer.Consumer[A]
Trampoline[B, L any] = tailrec.Trampoline[B, L]
)

View File

@@ -16,6 +16,8 @@
package readerioeither
import (
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/chain"
@@ -894,3 +896,17 @@ func ChainFirstLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA
func TapLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA, A, A] {
return ChainFirstLeft[A](f)
}
// Delay creates an operation that passes in the value after some delay
//
//go:inline
func Delay[R, E, A any](delay time.Duration) Operator[R, E, A, A] {
return function.Bind2nd(function.Flow2[ReaderIOEither[R, E, A]], io.Delay[Either[E, A]](delay))
}
// After creates an operation that passes after the given [time.Time]
//
//go:inline
func After[R, E, A any](timestamp time.Time) Operator[R, E, A, A] {
return function.Bind2nd(function.Flow2[ReaderIOEither[R, E, A]], io.After[Either[E, A]](timestamp))
}

View File

@@ -17,6 +17,7 @@ package readerioeither
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/tailrec"
)
// TailRec implements stack-safe tail recursion for the ReaderIOEither monad.
@@ -31,10 +32,10 @@ import (
//
// # How It Works
//
// TailRec takes a Kleisli arrow that returns Either[E, Either[A, B]]:
// TailRec takes a Kleisli arrow that returns IOEither[E, Trampoline[A, B]]:
// - Left(E): Computation failed with error E - recursion terminates
// - Right(Left(A)): Continue recursion with the new state A
// - Right(Right(B)): Terminate recursion successfully and return the final result B
// - Right(Bounce(A)): Continue recursion with the new state A
// - Right(Land(B)): Terminate recursion successfully and return the final result B
//
// The function iteratively applies the Kleisli arrow, passing the environment R to each
// iteration, until either an error (Left) or a final result (Right(Right(B))) is produced.
@@ -100,18 +101,18 @@ import (
// }
//
// // Factorial that logs each step and validates input
// factorialStep := func(state State) readerioeither.ReaderIOEither[Env, string, either.Either[State, int]] {
// return func(env Env) ioeither.IOEither[string, either.Either[State, int]] {
// return func() either.Either[string, either.Either[State, int]] {
// factorialStep := func(state State) readerioeither.ReaderIOEither[Env, string, tailrec.Trampoline[State, int]] {
// return func(env Env) ioeither.IOEither[string, tailrec.Trampoline[State, int]] {
// return func() either.Either[string, tailrec.Trampoline[State, int]] {
// if state.n > env.MaxN {
// return either.Left[either.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
// return either.Left[tailrec.Trampoline[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
// }
// if state.n <= 0 {
// env.Logger(fmt.Sprintf("Factorial complete: %d", state.acc))
// return either.Right[string](either.Right[State](state.acc))
// return either.Right[string](tailrec.Land[State](state.acc))
// }
// env.Logger(fmt.Sprintf("Computing: %d * %d", state.n, state.acc))
// return either.Right[string](either.Left[int](State{state.n - 1, state.acc * state.n}))
// return either.Right[string](tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}))
// }
// }
// }
@@ -134,12 +135,12 @@ import (
// retries int
// }
//
// processFilesStep := func(state ProcessState) readerioeither.ReaderIOEither[Config, error, either.Either[ProcessState, []string]] {
// return func(cfg Config) ioeither.IOEither[error, either.Either[ProcessState, []string]] {
// return func() either.Either[error, either.Either[ProcessState, []string]] {
// processFilesStep := func(state ProcessState) readerioeither.ReaderIOEither[Config, error, tailrec.Trampoline[ProcessState, []string]] {
// return func(cfg Config) ioeither.IOEither[error, tailrec.Trampoline[ProcessState, []string]] {
// return func() either.Either[error, tailrec.Trampoline[ProcessState, []string]] {
// if len(state.files) == 0 {
// cfg.Logger("All files processed")
// return either.Right[error](either.Right[ProcessState](state.results))
// return either.Right[error](tailrec.Land[ProcessState](state.results))
// }
// file := state.files[0]
// cfg.Logger(fmt.Sprintf("Processing: %s", file))
@@ -147,18 +148,18 @@ import (
// // Simulate file processing that might fail
// if err := processFile(file); err != nil {
// if state.retries >= cfg.MaxRetries {
// return either.Left[either.Either[ProcessState, []string]](
// return either.Left[tailrec.Trampoline[ProcessState, []string]](
// fmt.Errorf("max retries exceeded for %s: %w", file, err))
// }
// cfg.Logger(fmt.Sprintf("Retry %d for %s", state.retries+1, file))
// return either.Right[error](either.Left[[]string](ProcessState{
// return either.Right[error](tailrec.Bounce[[]string](ProcessState{
// files: state.files,
// results: state.results,
// retries: state.retries + 1,
// }))
// }
//
// return either.Right[error](either.Left[[]string](ProcessState{
// return either.Right[error](tailrec.Bounce[[]string](ProcessState{
// files: state.files[1:],
// results: append(state.results, file),
// retries: 0,
@@ -179,13 +180,13 @@ import (
// (thousands or millions of iterations) will not cause stack overflow:
//
// // Safe for very large inputs
// countdownStep := func(n int) readerioeither.ReaderIOEither[Env, error, either.Either[int, int]] {
// return func(env Env) ioeither.IOEither[error, either.Either[int, int]] {
// return func() either.Either[error, either.Either[int, int]] {
// countdownStep := func(n int) readerioeither.ReaderIOEither[Env, error, tailrec.Trampoline[int, int]] {
// return func(env Env) ioeither.IOEither[error, tailrec.Trampoline[int, int]] {
// return func() either.Either[error, tailrec.Trampoline[int, int]] {
// if n <= 0 {
// return either.Right[error](either.Right[int](0))
// return either.Right[error](tailrec.Land[int](0))
// }
// return either.Right[error](either.Left[int](n - 1))
// return either.Right[error](tailrec.Bounce[int](n - 1))
// }
// }
// }
@@ -194,16 +195,16 @@ import (
//
// # Error Handling Patterns
//
// The Either[E, Either[A, B]] structure provides two levels of control:
// The Either[E, Trampoline[A, B]] structure provides two levels of control:
//
// 1. Outer Either (Left(E)): Unrecoverable errors that terminate recursion
// - Validation failures
// - Resource exhaustion
// - Fatal errors
//
// 2. Inner Either (Right(Left(A)) or Right(Right(B))): Recursion control
// - Left(A): Continue with new state
// - Right(B): Terminate successfully
// 2. Inner Trampoline (Right(Bounce(A)) or Right(Land(B))): Recursion control
// - Bounce(A): Continue with new state
// - Land(B): Terminate successfully
//
// This separation allows for:
// - Early termination on errors
@@ -226,23 +227,22 @@ import (
// - [Chain]: For sequencing ReaderIOEither computations
// - [Ask]: For accessing the environment
// - [Left]/[Right]: For creating error/success values
func TailRec[R, E, A, B any](f Kleisli[R, E, A, Either[A, B]]) Kleisli[R, E, A, B] {
func TailRec[R, E, A, B any](f Kleisli[R, E, A, tailrec.Trampoline[A, B]]) Kleisli[R, E, A, B] {
return func(a A) ReaderIOEither[R, E, B] {
initialReader := f(a)
return func(r R) IOEither[E, B] {
initialB := initialReader(r)
return func() Either[E, B] {
return func() either.Either[E, B] {
current := initialB()
for {
rec, e := either.Unwrap(current)
if either.IsLeft(current) {
return either.Left[B](e)
}
b, a := either.Unwrap(rec)
if either.IsRight(rec) {
return either.Right[E](b)
if rec.Landed {
return either.Right[E](rec.Land)
}
current = f(a)(r)()
current = f(rec.Bounce)(r)()
}
}
}

View File

@@ -22,6 +22,7 @@ import (
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
IOE "github.com/IBM/fp-go/v2/ioeither"
TR "github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
@@ -58,18 +59,18 @@ func TestTailRecFactorial(t *testing.T) {
MaxN: 20,
}
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, TR.Trampoline[State, int]] {
return func(env LoggerEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.n > env.MaxN {
return E.Left[E.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
}
if state.n <= 0 {
env.Logger(fmt.Sprintf("Complete: %d", state.acc))
return E.Right[string](E.Right[State](state.acc))
return E.Right[string](TR.Land[State](state.acc))
}
env.Logger(fmt.Sprintf("Step: %d * %d", state.n, state.acc))
return E.Right[string](E.Left[int](State{state.n - 1, state.acc * state.n}))
return E.Right[string](TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
}
}
}
@@ -95,16 +96,16 @@ func TestTailRecFactorialError(t *testing.T) {
MaxN: 10,
}
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, TR.Trampoline[State, int]] {
return func(env LoggerEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.n > env.MaxN {
return E.Left[E.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
}
if state.n <= 0 {
return E.Right[string](E.Right[State](state.acc))
return E.Right[string](TR.Land[State](state.acc))
}
return E.Right[string](E.Left[int](State{state.n - 1, state.acc * state.n}))
return E.Right[string](TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
}
}
}
@@ -127,16 +128,16 @@ func TestTailRecFibonacci(t *testing.T) {
env := TestEnv{Multiplier: 1, MaxValue: 1000, Logs: []string{}}
fibStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
fibStep := func(state State) ReaderIOEither[TestEnv, string, TR.Trampoline[State, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.curr > env.MaxValue {
return E.Left[E.Either[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
}
if state.n <= 0 {
return E.Right[string](E.Right[State](state.curr * env.Multiplier))
return E.Right[string](TR.Land[State](state.curr * env.Multiplier))
}
return E.Right[string](E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
return E.Right[string](TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
}
}
}
@@ -157,16 +158,16 @@ func TestTailRecFibonacciError(t *testing.T) {
env := TestEnv{Multiplier: 1, MaxValue: 50, Logs: []string{}}
fibStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
fibStep := func(state State) ReaderIOEither[TestEnv, string, TR.Trampoline[State, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.curr > env.MaxValue {
return E.Left[E.Either[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
}
if state.n <= 0 {
return E.Right[string](E.Right[State](state.curr))
return E.Right[string](TR.Land[State](state.curr))
}
return E.Right[string](E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
return E.Right[string](TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
}
}
}
@@ -183,16 +184,16 @@ func TestTailRecFibonacciError(t *testing.T) {
func TestTailRecCountdown(t *testing.T) {
config := ConfigEnv{MinValue: 0, Step: 2, MaxRetries: 3}
countdownStep := func(n int) ReaderIOEither[ConfigEnv, string, E.Either[int, int]] {
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[int, int]] {
return func() E.Either[string, E.Either[int, int]] {
countdownStep := func(n int) ReaderIOEither[ConfigEnv, string, TR.Trampoline[int, int]] {
return func(cfg ConfigEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
return func() E.Either[string, TR.Trampoline[int, int]] {
if n < 0 {
return E.Left[E.Either[int, int]]("negative value")
return E.Left[TR.Trampoline[int, int]]("negative value")
}
if n <= cfg.MinValue {
return E.Right[string](E.Right[int](n))
return E.Right[string](TR.Land[int](n))
}
return E.Right[string](E.Left[int](n - cfg.Step))
return E.Right[string](TR.Bounce[int](n - cfg.Step))
}
}
}
@@ -212,16 +213,16 @@ func TestTailRecSumList(t *testing.T) {
env := TestEnv{Multiplier: 2, MaxValue: 100, Logs: []string{}}
sumStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
sumStep := func(state State) ReaderIOEither[TestEnv, string, TR.Trampoline[State, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.sum > env.MaxValue {
return E.Left[E.Either[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
}
if A.IsEmpty(state.list) {
return E.Right[string](E.Right[State](state.sum * env.Multiplier))
return E.Right[string](TR.Land[State](state.sum * env.Multiplier))
}
return E.Right[string](E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
return E.Right[string](TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
}
}
}
@@ -241,16 +242,16 @@ func TestTailRecSumListError(t *testing.T) {
env := TestEnv{Multiplier: 1, MaxValue: 10, Logs: []string{}}
sumStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
sumStep := func(state State) ReaderIOEither[TestEnv, string, TR.Trampoline[State, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.sum > env.MaxValue {
return E.Left[E.Either[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
}
if A.IsEmpty(state.list) {
return E.Right[string](E.Right[State](state.sum))
return E.Right[string](TR.Land[State](state.sum))
}
return E.Right[string](E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
return E.Right[string](TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
}
}
}
@@ -267,10 +268,10 @@ func TestTailRecSumListError(t *testing.T) {
func TestTailRecImmediateTermination(t *testing.T) {
env := TestEnv{Multiplier: 1, MaxValue: 100, Logs: []string{}}
immediateStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
return func() E.Either[string, E.Either[int, int]] {
return E.Right[string](E.Right[int](n * env.Multiplier))
immediateStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
return func() E.Either[string, TR.Trampoline[int, int]] {
return E.Right[string](TR.Land[int](n * env.Multiplier))
}
}
}
@@ -285,10 +286,10 @@ func TestTailRecImmediateTermination(t *testing.T) {
func TestTailRecImmediateError(t *testing.T) {
env := TestEnv{Multiplier: 1, MaxValue: 100, Logs: []string{}}
immediateErrorStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
return func() E.Either[string, E.Either[int, int]] {
return E.Left[E.Either[int, int]]("immediate error")
immediateErrorStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
return func() E.Either[string, TR.Trampoline[int, int]] {
return E.Left[TR.Trampoline[int, int]]("immediate error")
}
}
}
@@ -305,16 +306,16 @@ func TestTailRecImmediateError(t *testing.T) {
func TestTailRecStackSafety(t *testing.T) {
env := TestEnv{Multiplier: 1, MaxValue: 2000000, Logs: []string{}}
countdownStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
return func() E.Either[string, E.Either[int, int]] {
countdownStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
return func() E.Either[string, TR.Trampoline[int, int]] {
if n > env.MaxValue {
return E.Left[E.Either[int, int]]("value too large")
return E.Left[TR.Trampoline[int, int]]("value too large")
}
if n <= 0 {
return E.Right[string](E.Right[int](n))
return E.Right[string](TR.Land[int](n))
}
return E.Right[string](E.Left[int](n - 1))
return E.Right[string](TR.Bounce[int](n - 1))
}
}
}
@@ -339,19 +340,19 @@ func TestTailRecFindInRange(t *testing.T) {
env := FindEnv{Target: 42, MaxN: 1000}
findStep := func(state State) ReaderIOEither[FindEnv, string, E.Either[State, int]] {
return func(env FindEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
findStep := func(state State) ReaderIOEither[FindEnv, string, TR.Trampoline[State, int]] {
return func(env FindEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.current > env.MaxN {
return E.Left[E.Either[State, int]]("search exceeded max")
return E.Left[TR.Trampoline[State, int]]("search exceeded max")
}
if state.current >= state.max {
return E.Right[string](E.Right[State](-1)) // Not found
return E.Right[string](TR.Land[State](-1)) // Not found
}
if state.current == env.Target {
return E.Right[string](E.Right[State](state.current)) // Found
return E.Right[string](TR.Land[State](state.current)) // Found
}
return E.Right[string](E.Left[int](State{state.current + 1, state.max}))
return E.Right[string](TR.Bounce[int](State{state.current + 1, state.max}))
}
}
}
@@ -376,19 +377,19 @@ func TestTailRecFindNotInRange(t *testing.T) {
env := FindEnv{Target: 200, MaxN: 1000}
findStep := func(state State) ReaderIOEither[FindEnv, string, E.Either[State, int]] {
return func(env FindEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
findStep := func(state State) ReaderIOEither[FindEnv, string, TR.Trampoline[State, int]] {
return func(env FindEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.current > env.MaxN {
return E.Left[E.Either[State, int]]("search exceeded max")
return E.Left[TR.Trampoline[State, int]]("search exceeded max")
}
if state.current >= state.max {
return E.Right[string](E.Right[State](-1)) // Not found
return E.Right[string](TR.Land[State](-1)) // Not found
}
if state.current == env.Target {
return E.Right[string](E.Right[State](state.current)) // Found
return E.Right[string](TR.Land[State](state.current)) // Found
}
return E.Right[string](E.Left[int](State{state.current + 1, state.max}))
return E.Right[string](TR.Bounce[int](State{state.current + 1, state.max}))
}
}
}
@@ -409,17 +410,17 @@ func TestTailRecWithLogging(t *testing.T) {
MaxN: 100,
}
countdownStep := func(n int) ReaderIOEither[LoggerEnv, string, E.Either[int, int]] {
return func(env LoggerEnv) IOE.IOEither[string, E.Either[int, int]] {
return func() E.Either[string, E.Either[int, int]] {
countdownStep := func(n int) ReaderIOEither[LoggerEnv, string, TR.Trampoline[int, int]] {
return func(env LoggerEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
return func() E.Either[string, TR.Trampoline[int, int]] {
env.Logger(fmt.Sprintf("Count: %d", n))
if n > env.MaxN {
return E.Left[E.Either[int, int]]("value too large")
return E.Left[TR.Trampoline[int, int]]("value too large")
}
if n <= 0 {
return E.Right[string](E.Right[int](n))
return E.Right[string](TR.Land[int](n))
}
return E.Right[string](E.Left[int](n - 1))
return E.Right[string](TR.Bounce[int](n - 1))
}
}
}
@@ -448,17 +449,17 @@ func TestTailRecGCD(t *testing.T) {
MaxN: 1000,
}
gcdStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
gcdStep := func(state State) ReaderIOEither[LoggerEnv, string, TR.Trampoline[State, int]] {
return func(env LoggerEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
env.Logger(fmt.Sprintf("gcd(%d, %d)", state.a, state.b))
if state.a > env.MaxN || state.b > env.MaxN {
return E.Left[E.Either[State, int]]("values too large")
return E.Left[TR.Trampoline[State, int]]("values too large")
}
if state.b == 0 {
return E.Right[string](E.Right[State](state.a))
return E.Right[string](TR.Land[State](state.a))
}
return E.Right[string](E.Left[int](State{state.b, state.a % state.b}))
return E.Right[string](TR.Bounce[int](State{state.b, state.a % state.b}))
}
}
}
@@ -480,17 +481,17 @@ func TestTailRecRetryLogic(t *testing.T) {
config := ConfigEnv{MinValue: 0, Step: 1, MaxRetries: 3}
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, E.Either[State, int]] {
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, TR.Trampoline[State, int]] {
return func(cfg ConfigEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.attempt > cfg.MaxRetries {
return E.Left[E.Either[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
}
// Simulate success on 3rd attempt
if state.attempt == 3 {
return E.Right[string](E.Right[State](state.value))
return E.Right[string](TR.Land[State](state.value))
}
return E.Right[string](E.Left[int](State{state.attempt + 1, state.value}))
return E.Right[string](TR.Bounce[int](State{state.attempt + 1, state.value}))
}
}
}
@@ -510,14 +511,14 @@ func TestTailRecRetryExceeded(t *testing.T) {
config := ConfigEnv{MinValue: 0, Step: 1, MaxRetries: 2}
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, E.Either[State, int]] {
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[State, int]] {
return func() E.Either[string, E.Either[State, int]] {
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, TR.Trampoline[State, int]] {
return func(cfg ConfigEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
return func() E.Either[string, TR.Trampoline[State, int]] {
if state.attempt > cfg.MaxRetries {
return E.Left[E.Either[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
}
// Never succeeds
return E.Right[string](E.Left[int](State{state.attempt + 1, state.value}))
return E.Right[string](TR.Bounce[int](State{state.attempt + 1, state.value}))
}
}
}
@@ -540,16 +541,16 @@ func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
env := CounterEnv{Increment: 3, Limit: 20, MaxValue: 100}
counterStep := func(n int) ReaderIOEither[CounterEnv, string, E.Either[int, int]] {
return func(env CounterEnv) IOE.IOEither[string, E.Either[int, int]] {
return func() E.Either[string, E.Either[int, int]] {
counterStep := func(n int) ReaderIOEither[CounterEnv, string, TR.Trampoline[int, int]] {
return func(env CounterEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
return func() E.Either[string, TR.Trampoline[int, int]] {
if n > env.MaxValue {
return E.Left[E.Either[int, int]]("value exceeds max")
return E.Left[TR.Trampoline[int, int]]("value exceeds max")
}
if n >= env.Limit {
return E.Right[string](E.Right[int](n))
return E.Right[string](TR.Land[int](n))
}
return E.Right[string](E.Left[int](n + env.Increment))
return E.Right[string](TR.Bounce[int](n + env.Increment))
}
}
}
@@ -564,16 +565,16 @@ func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
func TestTailRecErrorInMiddle(t *testing.T) {
env := TestEnv{Multiplier: 1, MaxValue: 50, Logs: []string{}}
countdownStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
return func() E.Either[string, E.Either[int, int]] {
countdownStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
return func() E.Either[string, TR.Trampoline[int, int]] {
if n == 5 {
return E.Left[E.Either[int, int]]("error at 5")
return E.Left[TR.Trampoline[int, int]]("error at 5")
}
if n <= 0 {
return E.Right[string](E.Right[int](n))
return E.Right[string](TR.Land[int](n))
}
return E.Right[string](E.Left[int](n - 1))
return E.Right[string](TR.Bounce[int](n - 1))
}
}
}
@@ -588,13 +589,13 @@ func TestTailRecErrorInMiddle(t *testing.T) {
// TestTailRecDifferentEnvironments tests that different environments produce different results
func TestTailRecDifferentEnvironments(t *testing.T) {
multiplyStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
return func() E.Either[string, E.Either[int, int]] {
multiplyStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
return func() E.Either[string, TR.Trampoline[int, int]] {
if n <= 0 {
return E.Right[string](E.Right[int](n * env.Multiplier))
return E.Right[string](TR.Land[int](n * env.Multiplier))
}
return E.Right[string](E.Left[int](n - 1))
return E.Right[string](TR.Bounce[int](n - 1))
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
import (
RIO "github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/retry"
)
//go:inline
func Retrying[R, E, A any](
policy retry.RetryPolicy,
action Kleisli[R, E, retry.RetryStatus, A],
check func(Either[E, A]) bool,
) ReaderIOEither[R, E, A] {
// get an implementation for the types
return RIO.Retrying(policy, action, check)
}

View File

@@ -16,6 +16,10 @@
package readerioresult
import (
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
RE "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
@@ -797,3 +801,17 @@ func ChainFirstLeft[A, R, B any](f Kleisli[R, error, B]) Operator[R, A, A] {
func TapLeft[A, R, B any](f Kleisli[R, error, B]) Operator[R, A, A] {
return RIOE.TapLeft[A](f)
}
// Delay creates an operation that passes in the value after some delay
//
//go:inline
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
return function.Bind2nd(function.Flow2[ReaderIOResult[R, A]], io.Delay[Result[A]](delay))
}
// After creates an operation that passes after the given [time.Time]
//
//go:inline
func After[R, A any](timestamp time.Time) Operator[R, A, A] {
return function.Bind2nd(function.Flow2[ReaderIOResult[R, A]], io.After[Result[A]](timestamp))
}

View File

@@ -17,9 +17,10 @@ package readerioresult
import (
"github.com/IBM/fp-go/v2/readerioeither"
"github.com/IBM/fp-go/v2/tailrec"
)
//go:inline
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
func TailRec[R, A, B any](f Kleisli[R, A, tailrec.Trampoline[A, B]]) Kleisli[R, A, B] {
return readerioeither.TailRec(f)
}

109
v2/readerioresult/retry.go Normal file
View File

@@ -0,0 +1,109 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"github.com/IBM/fp-go/v2/readerioeither"
"github.com/IBM/fp-go/v2/retry"
)
// Retrying retries a ReaderIOResult computation according to a retry policy.
//
// This function implements a retry mechanism for operations that depend on a context (Reader),
// perform side effects (IO), and can fail (Result). It will repeatedly execute the action
// according to the retry policy until either:
// - The action succeeds and the check function returns false (no retry needed)
// - The retry policy returns None (retry limit reached)
// - The check function returns false (indicating success or a non-retryable failure)
//
// Parameters:
//
// - policy: A RetryPolicy that determines when and how long to wait between retries.
// The policy receives a RetryStatus on each iteration and returns an optional delay.
// If it returns None, retrying stops. Common policies include LimitRetries,
// ExponentialBackoff, and CapDelay from the retry package.
//
// - action: A Kleisli arrow that takes a RetryStatus and returns a ReaderIOResult[R, A].
// This function is called on each retry attempt and receives information about the
// current retry state (iteration number, cumulative delay, etc.). The action depends
// on a context of type R and produces a Result[A].
//
// - check: A predicate function that examines the Result[A] and returns true if the
// operation should be retried, or false if it should stop. This allows you to
// distinguish between retryable failures (e.g., network timeouts) and permanent
// failures (e.g., invalid input).
//
// Returns:
//
// A ReaderIOResult[R, A] that, when executed with a context, will perform the retry
// logic and return the final result.
//
// Type Parameters:
// - R: The type of the context/environment required by the action
// - A: The type of the success value
//
// Example:
//
// type Config struct {
// MaxRetries int
// BaseURL string
// }
//
// // Create a retry policy: exponential backoff with a cap, limited to 5 retries
// policy := M.Concat(
// retry.LimitRetries(5),
// retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
// )(retry.Monoid)
//
// // Action that fetches data, with retry status information
// fetchData := func(status retry.RetryStatus) ReaderIOResult[Config, string] {
// return func(cfg Config) IOResult[string] {
// return func() Result[string] {
// // Simulate an HTTP request that might fail
// if status.IterNumber < 3 {
// return result.Left[string](fmt.Errorf("temporary error"))
// }
// return result.Right[error]("success")
// }
// }
// }
//
// // Check function: retry on any error
// shouldRetry := func(r Result[string]) bool {
// return result.IsLeft(r)
// }
//
// // Create the retrying computation
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a config
// cfg := Config{MaxRetries: 5, BaseURL: "https://api.example.com"}
// ioResult := retryingFetch(cfg)
// finalResult := ioResult()
//
// See also:
// - retry.RetryPolicy for available retry policies
// - retry.RetryStatus for information passed to the action
// - readerioeither.Retrying for the underlying implementation
//
//go:inline
func Retrying[R, A any](
policy retry.RetryPolicy,
action Kleisli[R, retry.RetryStatus, A],
check func(Result[A]) bool,
) ReaderIOResult[R, A] {
// get an implementation for the types
return readerioeither.Retrying(policy, action, check)
}

View File

@@ -0,0 +1,315 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"context"
"fmt"
"testing"
"time"
"github.com/IBM/fp-go/v2/result"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
// Test configuration type
type testConfig struct {
maxRetries int
baseURL string
}
// Helper function to create a test retry policy
func testRetryPolicy() R.RetryPolicy {
return R.Monoid.Concat(
R.LimitRetries(5),
R.CapDelay(1*time.Second, R.ExponentialBackoff(10*time.Millisecond)),
)
}
// TestRetrying_SuccessOnFirstAttempt tests that Retrying succeeds immediately
// when the action succeeds on the first attempt.
func TestRetrying_SuccessOnFirstAttempt(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[testConfig, string] {
return func(cfg testConfig) IOResult[string] {
return func() Result[string] {
return result.Of("success")
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
cfg := testConfig{maxRetries: 5, baseURL: "https://api.example.com"}
res := retrying(cfg)()
assert.Equal(t, result.Of("success"), res)
}
// TestRetrying_SuccessAfterRetries tests that Retrying eventually succeeds
// after a few failed attempts.
func TestRetrying_SuccessAfterRetries(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[testConfig, string] {
return func(cfg testConfig) IOResult[string] {
return func() Result[string] {
// Fail on first 3 attempts, succeed on 4th
if status.IterNumber < 3 {
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
return result.Of(fmt.Sprintf("success on attempt %d", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
cfg := testConfig{maxRetries: 5, baseURL: "https://api.example.com"}
res := retrying(cfg)()
assert.Equal(t, result.Of("success on attempt 3"), res)
}
// TestRetrying_ExhaustsRetries tests that Retrying stops after the retry limit
// is reached and returns the last error.
func TestRetrying_ExhaustsRetries(t *testing.T) {
policy := R.LimitRetries(3)
action := func(status R.RetryStatus) ReaderIOResult[testConfig, string] {
return func(cfg testConfig) IOResult[string] {
return func() Result[string] {
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
cfg := testConfig{maxRetries: 3, baseURL: "https://api.example.com"}
res := retrying(cfg)()
assert.True(t, result.IsLeft(res))
assert.Equal(t, result.Left[string](fmt.Errorf("attempt 3 failed")), res)
}
// TestRetrying_CheckFunctionStopsRetry tests that the check function can
// stop retrying even when errors occur.
func TestRetrying_CheckFunctionStopsRetry(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[testConfig, string] {
return func(cfg testConfig) IOResult[string] {
return func() Result[string] {
if status.IterNumber == 0 {
return result.Left[string](fmt.Errorf("retryable error"))
}
return result.Left[string](fmt.Errorf("permanent error"))
}
}
}
// Only retry on "retryable error"
check := func(r Result[string]) bool {
return result.IsLeft(r) && result.Fold(
func(err error) bool { return err.Error() == "retryable error" },
func(string) bool { return false },
)(r)
}
retrying := Retrying(policy, action, check)
cfg := testConfig{maxRetries: 5, baseURL: "https://api.example.com"}
res := retrying(cfg)()
assert.Equal(t, result.Left[string](fmt.Errorf("permanent error")), res)
}
// TestRetrying_UsesContext tests that the Reader context is properly passed
// through to the action.
func TestRetrying_UsesContext(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[testConfig, string] {
return func(cfg testConfig) IOResult[string] {
return func() Result[string] {
// Use the config from context
if status.IterNumber < 2 {
return result.Left[string](fmt.Errorf("retry needed"))
}
return result.Of(fmt.Sprintf("success with baseURL: %s", cfg.baseURL))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
cfg := testConfig{maxRetries: 5, baseURL: "https://test.example.com"}
res := retrying(cfg)()
assert.Equal(t, result.Of("success with baseURL: https://test.example.com"), res)
}
// TestRetrying_RetryStatusProgression tests that the RetryStatus is properly
// updated on each iteration.
func TestRetrying_RetryStatusProgression(t *testing.T) {
policy := testRetryPolicy()
var iterations []uint
action := func(status R.RetryStatus) ReaderIOResult[testConfig, int] {
return func(cfg testConfig) IOResult[int] {
return func() Result[int] {
iterations = append(iterations, status.IterNumber)
if status.IterNumber < 3 {
return result.Left[int](fmt.Errorf("retry"))
}
return result.Of(int(status.IterNumber))
}
}
}
check := func(r Result[int]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
cfg := testConfig{maxRetries: 5, baseURL: "https://api.example.com"}
res := retrying(cfg)()
assert.Equal(t, result.Of(3), res)
// Should have attempted iterations 0, 1, 2, 3
assert.Equal(t, []uint{0, 1, 2, 3}, iterations)
}
// TestRetrying_WithContextContext tests using context.Context as the Reader type.
func TestRetrying_WithContextContext(t *testing.T) {
policy := R.LimitRetries(2)
action := func(status R.RetryStatus) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
if status.IterNumber == 0 {
return result.Left[string](fmt.Errorf("first attempt failed"))
}
return result.Of("success")
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
res := retrying(ctx)()
assert.Equal(t, result.Of("success"), res)
}
// TestRetrying_NoRetryOnSuccess tests that successful results are not retried
// even if the check function would return true.
func TestRetrying_NoRetryOnSuccess(t *testing.T) {
policy := testRetryPolicy()
callCount := 0
action := func(status R.RetryStatus) ReaderIOResult[testConfig, string] {
return func(cfg testConfig) IOResult[string] {
return func() Result[string] {
callCount++
return result.Of("success")
}
}
}
// This check would normally trigger a retry, but since the result is Right,
// it should not be called or should not cause a retry
check := func(r Result[string]) bool {
// Only retry on Left (error) results
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
cfg := testConfig{maxRetries: 5, baseURL: "https://api.example.com"}
res := retrying(cfg)()
assert.Equal(t, result.Of("success"), res)
// Should only be called once since it succeeded immediately
assert.Equal(t, 1, callCount)
}
// TestRetrying_ExponentialBackoff tests that exponential backoff is applied.
func TestRetrying_ExponentialBackoff(t *testing.T) {
// Use a policy with measurable delays
policy := R.Monoid.Concat(
R.LimitRetries(3),
R.ExponentialBackoff(50*time.Millisecond),
)
startTime := time.Now()
action := func(status R.RetryStatus) ReaderIOResult[testConfig, string] {
return func(cfg testConfig) IOResult[string] {
return func() Result[string] {
if status.IterNumber < 2 {
return result.Left[string](fmt.Errorf("retry"))
}
return result.Of("success")
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
cfg := testConfig{maxRetries: 3, baseURL: "https://api.example.com"}
res := retrying(cfg)()
elapsed := time.Since(startTime)
assert.Equal(t, result.Of("success"), res)
// With exponential backoff starting at 50ms:
// Iteration 0: no delay
// Iteration 1: 50ms delay
// Iteration 2: 100ms delay
// Total should be at least 150ms
assert.GreaterOrEqual(t, elapsed, 150*time.Millisecond)
}

View File

@@ -16,26 +16,25 @@
package readeroption
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tailrec"
)
//go:inline
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
func TailRec[R, A, B any](f Kleisli[R, A, tailrec.Trampoline[A, B]]) Kleisli[R, A, B] {
return func(a A) ReaderOption[R, B] {
initialReader := f(a)
return func(r R) Option[B] {
return func(r R) option.Option[B] {
current := initialReader(r)
for {
rec, ok := option.Unwrap(current)
if !ok {
return option.None[B]()
}
b, a := either.Unwrap(rec)
if either.IsRight(rec) {
return option.Some(b)
if rec.Landed {
return option.Some(rec.Land)
}
current = f(a)(r)
current = f(rec.Bounce)(r)
}
}
}

View File

@@ -17,9 +17,10 @@ package readerresult
import (
"github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/tailrec"
)
//go:inline
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
func TailRec[R, A, B any](f Kleisli[R, A, tailrec.Trampoline[A, B]]) Kleisli[R, A, B] {
return readereither.TailRec(f)
}

View File

@@ -16,7 +16,6 @@
package record
import (
Mo "github.com/IBM/fp-go/v2/monoid"
G "github.com/IBM/fp-go/v2/record/generic"
)
@@ -30,8 +29,8 @@ import (
// Count int
// }
// result := record.Do[string, State]()
func Do[K comparable, S any]() map[K]S {
return G.Do[map[K]S]()
func Do[K comparable, S any]() Record[K, S] {
return G.Do[Record[K, S]]()
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
@@ -68,29 +67,87 @@ func Do[K comparable, S any]() map[K]S {
// },
// ),
// )
func Bind[S1, T any, K comparable, S2 any](m Mo.Monoid[map[K]S2]) func(setter func(T) func(S1) S2, f func(S1) map[K]T) func(map[K]S1) map[K]S2 {
return G.Bind[map[K]S1, map[K]S2, map[K]T](m)
func Bind[S1, T any, K comparable, S2 any](m Monoid[Record[K, S2]]) func(
setter func(T) func(S1) S2,
f Kleisli[K, S1, T],
) Operator[K, S1, S2] {
return G.Bind[Record[K, S1], Record[K, S2], Record[K, T]](m)
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
// Let attaches the result of a computation to a context [S1] to produce a context [S2].
// Unlike Bind, Let does not require a Monoid because it transforms each value independently
// without merging multiple maps.
//
// The setter function takes the computed value and returns a function that updates the context.
// The computation function f takes the current context and produces a value.
//
// Example:
//
// type State struct {
// Name string
// Length int
// }
//
// result := F.Pipe2(
// map[string]State{"a": {Name: "Alice"}},
// record.Let(
// func(length int) func(State) State {
// return func(s State) State { s.Length = length; return s }
// },
// func(s State) int { return len(s.Name) },
// ),
// ) // map[string]State{"a": {Name: "Alice", Length: 5}}
func Let[S1, T any, K comparable, S2 any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(map[K]S1) map[K]S2 {
return G.Let[map[K]S1, map[K]S2](setter, f)
) Operator[K, S1, S2] {
return G.Let[Record[K, S1], Record[K, S2]](setter, f)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
// This is similar to Let but uses a fixed value instead of computing it from the context.
//
// The setter function takes the value and returns a function that updates the context.
//
// Example:
//
// type State struct {
// Name string
// Version int
// }
//
// result := F.Pipe2(
// map[string]State{"a": {Name: "Alice"}},
// record.LetTo(
// func(version int) func(State) State {
// return func(s State) State { s.Version = version; return s }
// },
// 2,
// ),
// ) // map[string]State{"a": {Name: "Alice", Version: 2}}
func LetTo[S1, T any, K comparable, S2 any](
setter func(T) func(S1) S2,
b T,
) func(map[K]S1) map[K]S2 {
return G.LetTo[map[K]S1, map[K]S2](setter, b)
) Operator[K, S1, S2] {
return G.LetTo[Record[K, S1], Record[K, S2]](setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any, K comparable](setter func(T) S1) func(map[K]T) map[K]S1 {
return G.BindTo[map[K]S1, map[K]T](setter)
// BindTo initializes a new state [S1] from a value [T].
// This is typically used as the first step in a do-notation chain to convert
// a simple map of values into a map of state objects.
//
// Example:
//
// type State struct {
// Name string
// }
//
// result := F.Pipe1(
// map[string]string{"a": "Alice", "b": "Bob"},
// record.BindTo(func(name string) State { return State{Name: name} }),
// ) // map[string]State{"a": {Name: "Alice"}, "b": {Name: "Bob"}}
func BindTo[S1, T any, K comparable](setter func(T) S1) Operator[K, T, S1] {
return G.BindTo[Record[K, S1], Record[K, T]](setter)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
@@ -126,6 +183,9 @@ func BindTo[S1, T any, K comparable](setter func(T) S1) func(map[K]T) map[K]S1 {
// counts,
// ),
// ) // map[string]State{"a": {Name: "Alice", Count: 10}, "b": {Name: "Bob", Count: 20}}
func ApS[S1, T any, K comparable, S2 any](m Mo.Monoid[map[K]S2]) func(setter func(T) func(S1) S2, fa map[K]T) func(map[K]S1) map[K]S2 {
return G.ApS[map[K]S1, map[K]S2, map[K]T](m)
func ApS[S1, T any, K comparable, S2 any](m Monoid[Record[K, S2]]) func(
setter func(T) func(S1) S2,
fa Record[K, T],
) Operator[K, S1, S2] {
return G.ApS[Record[K, S1], Record[K, S2], Record[K, T]](m)
}

227
v2/record/bind_test.go Normal file
View File

@@ -0,0 +1,227 @@
// 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 record
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
type TestState struct {
Name string
Count int
Version int
}
func TestDo(t *testing.T) {
result := Do[string, TestState]()
assert.NotNil(t, result)
assert.Empty(t, result)
assert.Equal(t, map[string]TestState{}, result)
}
func TestBindTo(t *testing.T) {
input := map[string]string{"a": "Alice", "b": "Bob"}
result := F.Pipe1(
input,
BindTo[TestState, string, string](func(name string) TestState {
return TestState{Name: name}
}),
)
expected := map[string]TestState{
"a": {Name: "Alice"},
"b": {Name: "Bob"},
}
assert.Equal(t, expected, result)
}
func TestLet(t *testing.T) {
input := map[string]TestState{
"a": {Name: "Alice"},
"b": {Name: "Bob"},
}
result := F.Pipe1(
input,
Let[TestState, int, string, TestState](
func(length int) func(TestState) TestState {
return func(s TestState) TestState {
s.Count = length
return s
}
},
func(s TestState) int {
return len(s.Name)
},
),
)
expected := map[string]TestState{
"a": {Name: "Alice", Count: 5},
"b": {Name: "Bob", Count: 3},
}
assert.Equal(t, expected, result)
}
func TestLetTo(t *testing.T) {
input := map[string]TestState{
"a": {Name: "Alice"},
"b": {Name: "Bob"},
}
result := F.Pipe1(
input,
LetTo[TestState, int, string, TestState](
func(version int) func(TestState) TestState {
return func(s TestState) TestState {
s.Version = version
return s
}
},
2,
),
)
expected := map[string]TestState{
"a": {Name: "Alice", Version: 2},
"b": {Name: "Bob", Version: 2},
}
assert.Equal(t, expected, result)
}
func TestBind(t *testing.T) {
monoid := MergeMonoid[string, TestState]()
// Bind chains computations where each step can depend on previous results
result := F.Pipe1(
map[string]string{"x": "test"},
Bind[string, int](monoid)(
func(length int) func(string) TestState {
return func(s string) TestState {
return TestState{Name: s, Count: length}
}
},
func(s string) map[string]int {
return map[string]int{"x": len(s)}
},
),
)
expected := map[string]TestState{
"x": {Name: "test", Count: 4},
}
assert.Equal(t, expected, result)
}
func TestApS(t *testing.T) {
monoid := MergeMonoid[string, TestState]()
// ApS applies independent computations
names := map[string]string{"x": "Alice"}
counts := map[string]int{"x": 10}
result := F.Pipe2(
map[string]TestState{"x": {}},
ApS[TestState, string](monoid)(
func(name string) func(TestState) TestState {
return func(s TestState) TestState {
s.Name = name
return s
}
},
names,
),
ApS[TestState, int](monoid)(
func(count int) func(TestState) TestState {
return func(s TestState) TestState {
s.Count = count
return s
}
},
counts,
),
)
expected := map[string]TestState{
"x": {Name: "Alice", Count: 10},
}
assert.Equal(t, expected, result)
}
func TestBindChain(t *testing.T) {
// Test a complete do-notation chain with BindTo, Let, and LetTo
result := F.Pipe3(
map[string]string{"x": "Alice", "y": "Bob"},
BindTo[TestState, string, string](func(name string) TestState {
return TestState{Name: name}
}),
Let[TestState, int, string, TestState](
func(count int) func(TestState) TestState {
return func(s TestState) TestState {
s.Count = count
return s
}
},
func(s TestState) int {
return len(s.Name)
},
),
LetTo[TestState, int, string, TestState](
func(version int) func(TestState) TestState {
return func(s TestState) TestState {
s.Version = version
return s
}
},
1,
),
)
expected := map[string]TestState{
"x": {Name: "Alice", Count: 5, Version: 1},
"y": {Name: "Bob", Count: 3, Version: 1},
}
assert.Equal(t, expected, result)
}
func TestBindWithDependentComputation(t *testing.T) {
// Test Bind where the computation creates new keys based on input
monoid := MergeMonoid[string, TestState]()
result := F.Pipe1(
map[string]int{"x": 5},
Bind[int, string](monoid)(
func(str string) func(int) TestState {
return func(n int) TestState {
return TestState{Name: str, Count: n}
}
},
func(n int) map[string]string {
// Create a string based on the number
result := ""
for i := 0; i < n; i++ {
result += "a"
}
return map[string]string{"x": result}
},
),
)
expected := map[string]TestState{
"x": {Name: "aaaaa", Count: 5},
}
assert.Equal(t, expected, result)
}
// Made with Bob

85
v2/record/coverage.out Normal file
View File

@@ -0,0 +1,85 @@
mode: set
github.com/IBM/fp-go/v2/record/bind.go:33.40,35.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:71.144,73.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:79.27,81.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:87.27,89.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:92.80,94.2 1 0
github.com/IBM/fp-go/v2/record/bind.go:129.135,131.2 1 0
github.com/IBM/fp-go/v2/record/eq.go:23.55,25.2 1 0
github.com/IBM/fp-go/v2/record/eq.go:28.56,30.2 1 1
github.com/IBM/fp-go/v2/record/monoid.go:25.75,27.2 1 1
github.com/IBM/fp-go/v2/record/monoid.go:30.63,32.2 1 1
github.com/IBM/fp-go/v2/record/monoid.go:35.64,37.2 1 1
github.com/IBM/fp-go/v2/record/monoid.go:40.59,42.2 1 1
github.com/IBM/fp-go/v2/record/record.go:28.51,30.2 1 1
github.com/IBM/fp-go/v2/record/record.go:33.54,35.2 1 1
github.com/IBM/fp-go/v2/record/record.go:38.47,40.2 1 1
github.com/IBM/fp-go/v2/record/record.go:43.49,45.2 1 1
github.com/IBM/fp-go/v2/record/record.go:48.72,50.2 1 1
github.com/IBM/fp-go/v2/record/record.go:53.92,55.2 1 1
github.com/IBM/fp-go/v2/record/record.go:57.80,59.2 1 1
github.com/IBM/fp-go/v2/record/record.go:61.92,63.2 1 1
github.com/IBM/fp-go/v2/record/record.go:65.84,67.2 1 1
github.com/IBM/fp-go/v2/record/record.go:69.96,71.2 1 1
github.com/IBM/fp-go/v2/record/record.go:73.71,75.2 1 1
github.com/IBM/fp-go/v2/record/record.go:77.83,79.2 1 1
github.com/IBM/fp-go/v2/record/record.go:81.87,83.2 1 1
github.com/IBM/fp-go/v2/record/record.go:85.75,87.2 1 1
github.com/IBM/fp-go/v2/record/record.go:89.69,91.2 1 1
github.com/IBM/fp-go/v2/record/record.go:93.73,95.2 1 1
github.com/IBM/fp-go/v2/record/record.go:97.81,99.2 1 1
github.com/IBM/fp-go/v2/record/record.go:101.85,103.2 1 1
github.com/IBM/fp-go/v2/record/record.go:106.65,108.2 1 1
github.com/IBM/fp-go/v2/record/record.go:111.67,113.2 1 1
github.com/IBM/fp-go/v2/record/record.go:116.52,118.2 1 1
github.com/IBM/fp-go/v2/record/record.go:120.84,122.2 1 0
github.com/IBM/fp-go/v2/record/record.go:125.70,127.2 1 1
github.com/IBM/fp-go/v2/record/record.go:130.43,132.2 1 1
github.com/IBM/fp-go/v2/record/record.go:135.47,137.2 1 1
github.com/IBM/fp-go/v2/record/record.go:139.60,141.2 1 1
github.com/IBM/fp-go/v2/record/record.go:143.62,145.2 1 1
github.com/IBM/fp-go/v2/record/record.go:147.65,149.2 1 1
github.com/IBM/fp-go/v2/record/record.go:151.68,153.2 1 1
github.com/IBM/fp-go/v2/record/record.go:155.63,157.2 1 1
github.com/IBM/fp-go/v2/record/record.go:160.55,162.2 1 1
github.com/IBM/fp-go/v2/record/record.go:165.103,167.2 1 1
github.com/IBM/fp-go/v2/record/record.go:170.91,172.2 1 1
github.com/IBM/fp-go/v2/record/record.go:175.72,177.2 1 1
github.com/IBM/fp-go/v2/record/record.go:180.84,182.2 1 1
github.com/IBM/fp-go/v2/record/record.go:185.49,187.2 1 1
github.com/IBM/fp-go/v2/record/record.go:190.52,192.2 1 1
github.com/IBM/fp-go/v2/record/record.go:195.46,197.2 1 1
github.com/IBM/fp-go/v2/record/record.go:199.124,201.2 1 0
github.com/IBM/fp-go/v2/record/record.go:203.112,205.2 1 1
github.com/IBM/fp-go/v2/record/record.go:207.125,209.2 1 0
github.com/IBM/fp-go/v2/record/record.go:211.113,213.2 1 1
github.com/IBM/fp-go/v2/record/record.go:216.85,218.2 1 1
github.com/IBM/fp-go/v2/record/record.go:221.141,223.2 1 1
github.com/IBM/fp-go/v2/record/record.go:226.129,228.2 1 0
github.com/IBM/fp-go/v2/record/record.go:231.86,233.2 1 1
github.com/IBM/fp-go/v2/record/record.go:236.98,238.2 1 0
github.com/IBM/fp-go/v2/record/record.go:241.64,243.2 1 1
github.com/IBM/fp-go/v2/record/record.go:246.104,248.2 1 0
github.com/IBM/fp-go/v2/record/record.go:251.92,253.2 1 0
github.com/IBM/fp-go/v2/record/record.go:256.108,258.2 1 1
github.com/IBM/fp-go/v2/record/record.go:261.86,263.2 1 0
github.com/IBM/fp-go/v2/record/record.go:266.120,268.2 1 0
github.com/IBM/fp-go/v2/record/record.go:271.69,273.2 1 1
github.com/IBM/fp-go/v2/record/record.go:276.71,278.2 1 1
github.com/IBM/fp-go/v2/record/record.go:280.78,282.2 1 1
github.com/IBM/fp-go/v2/record/record.go:284.74,286.2 1 1
github.com/IBM/fp-go/v2/record/record.go:289.51,291.2 1 1
github.com/IBM/fp-go/v2/record/record.go:294.80,296.2 1 1
github.com/IBM/fp-go/v2/record/record.go:305.88,307.2 1 0
github.com/IBM/fp-go/v2/record/record.go:314.73,316.2 1 1
github.com/IBM/fp-go/v2/record/record.go:324.60,326.2 1 0
github.com/IBM/fp-go/v2/record/record.go:332.55,334.2 1 1
github.com/IBM/fp-go/v2/record/record.go:336.105,338.2 1 1
github.com/IBM/fp-go/v2/record/record.go:340.106,342.2 1 1
github.com/IBM/fp-go/v2/record/record.go:344.48,346.2 1 1
github.com/IBM/fp-go/v2/record/semigroup.go:53.81,55.2 1 1
github.com/IBM/fp-go/v2/record/semigroup.go:80.69,82.2 1 1
github.com/IBM/fp-go/v2/record/semigroup.go:107.70,109.2 1 1
github.com/IBM/fp-go/v2/record/traverse.go:27.41,29.2 1 1
github.com/IBM/fp-go/v2/record/traverse.go:39.38,41.2 1 1
github.com/IBM/fp-go/v2/record/traverse.go:50.23,53.2 1 1

View File

@@ -20,11 +20,11 @@ import (
G "github.com/IBM/fp-go/v2/record/generic"
)
func Eq[K comparable, V any](e E.Eq[V]) E.Eq[map[K]V] {
return G.Eq[map[K]V](e)
func Eq[K comparable, V any](e E.Eq[V]) E.Eq[Record[K, V]] {
return G.Eq[Record[K, V]](e)
}
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
func FromStrictEquals[K, V comparable]() E.Eq[map[K]V] {
return G.FromStrictEquals[map[K]V]()
func FromStrictEquals[K, V comparable]() E.Eq[Record[K, V]] {
return G.FromStrictEquals[Record[K, V]]()
}

View File

@@ -26,7 +26,7 @@ import (
Mo "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ord"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
func IsEmpty[M ~map[K]V, K comparable, V any](r M) bool {
@@ -59,9 +59,9 @@ func ValuesOrd[M ~map[K]V, GV ~[]V, K comparable, V any](o ord.Ord[K]) func(r M)
func collectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K], r M, f func(K, V) R) GR {
// create the entries
entries := toEntriesOrd[M, []T.Tuple2[K, V]](o, r)
entries := toEntriesOrd[M, []pair.Pair[K, V]](o, r)
// collect this array
ft := T.Tupled2(f)
ft := pair.Paired(f)
count := len(entries)
result := make(GR, count)
for i := count - 1; i >= 0; i-- {
@@ -73,13 +73,13 @@ func collectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K], r M,
func reduceOrd[M ~map[K]V, K comparable, V, R any](o ord.Ord[K], r M, f func(K, R, V) R, initial R) R {
// create the entries
entries := toEntriesOrd[M, []T.Tuple2[K, V]](o, r)
entries := toEntriesOrd[M, []pair.Pair[K, V]](o, r)
// collect this array
current := initial
count := len(entries)
for i := 0; i < count; i++ {
t := entries[i]
current = f(T.First(t), current, T.Second(t))
current = f(pair.Head(t), current, pair.Tail(t))
}
// done
return current
@@ -318,32 +318,32 @@ func Size[M ~map[K]V, K comparable, V any](r M) int {
return len(r)
}
func ToArray[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
return collect[M, GT](r, T.MakeTuple2[K, V])
func ToArray[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](r M) GT {
return collect[M, GT](r, pair.MakePair[K, V])
}
func toEntriesOrd[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](o ord.Ord[K], r M) GT {
func toEntriesOrd[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](o ord.Ord[K], r M) GT {
// total number of elements
count := len(r)
// produce an array that we can sort by key
entries := make(GT, count)
idx := 0
for k, v := range r {
entries[idx] = T.MakeTuple2(k, v)
entries[idx] = pair.MakePair(k, v)
idx++
}
sort.Slice(entries, func(i, j int) bool {
return o.Compare(T.First(entries[i]), T.First(entries[j])) < 0
return o.Compare(pair.Head(entries[i]), pair.Head(entries[j])) < 0
})
// final entries
return entries
}
func ToEntriesOrd[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](o ord.Ord[K]) func(r M) GT {
func ToEntriesOrd[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](o ord.Ord[K]) func(r M) GT {
return F.Bind1st(toEntriesOrd[M, GT, K, V], o)
}
func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
func ToEntries[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](r M) GT {
return ToArray[M, GT](r)
}
@@ -351,7 +351,7 @@ func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
// its values into a tuple. The key and value are then used to populate the map. Duplicate
// values are resolved via the provided [Mg.Magma]
func FromFoldableMap[
FCT ~func(A) T.Tuple2[K, V],
FCT ~func(A) pair.Pair[K, V],
HKTA any,
FOLDABLE ~func(func(M, A) M, M) func(HKTA) M,
M ~map[K]V,
@@ -364,12 +364,12 @@ func FromFoldableMap[
dst = make(M)
}
e := f(a)
k := T.First(e)
k := pair.Head(e)
old, ok := dst[k]
if ok {
dst[k] = m.Concat(old, T.Second(e))
dst[k] = m.Concat(old, pair.Tail(e))
} else {
dst[k] = T.Second(e)
dst[k] = pair.Tail(e)
}
return dst
}, Empty[M]())
@@ -378,15 +378,15 @@ func FromFoldableMap[
func FromFoldable[
HKTA any,
FOLDABLE ~func(func(M, T.Tuple2[K, V]) M, M) func(HKTA) M,
FOLDABLE ~func(func(M, pair.Pair[K, V]) M, M) func(HKTA) M,
M ~map[K]V,
K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) M {
return FromFoldableMap[func(T.Tuple2[K, V]) T.Tuple2[K, V]](m, red)(F.Identity[T.Tuple2[K, V]])
return FromFoldableMap[func(pair.Pair[K, V]) pair.Pair[K, V]](m, red)(F.Identity[pair.Pair[K, V]])
}
func FromArrayMap[
FCT ~func(A) T.Tuple2[K, V],
FCT ~func(A) pair.Pair[K, V],
GA ~[]A,
M ~map[K]V,
A any,
@@ -396,17 +396,17 @@ func FromArrayMap[
}
func FromArray[
GA ~[]T.Tuple2[K, V],
GA ~[]pair.Pair[K, V],
M ~map[K]V,
K comparable,
V any](m Mg.Magma[V]) func(fa GA) M {
return FromFoldable(m, F.Bind23of3(RAG.Reduce[GA, T.Tuple2[K, V], M]))
return FromFoldable(m, F.Bind23of3(RAG.Reduce[GA, pair.Pair[K, V], M]))
}
func FromEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](fa GT) M {
func FromEntries[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](fa GT) M {
m := make(M)
for _, t := range fa {
upsertAtReadWrite(m, t.F1, t.F2)
upsertAtReadWrite(m, pair.Head(t), pair.Tail(t))
}
return m
}

View File

@@ -16,27 +16,34 @@
package record
import (
M "github.com/IBM/fp-go/v2/monoid"
G "github.com/IBM/fp-go/v2/record/generic"
S "github.com/IBM/fp-go/v2/semigroup"
)
// UnionMonoid computes the union of two maps of the same type
func UnionMonoid[K comparable, V any](s S.Semigroup[V]) M.Monoid[map[K]V] {
return G.UnionMonoid[map[K]V](s)
//
//go:inline
func UnionMonoid[K comparable, V any](s S.Semigroup[V]) Monoid[Record[K, V]] {
return G.UnionMonoid[Record[K, V]](s)
}
// UnionLastMonoid computes the union of two maps of the same type giving the last map precedence
func UnionLastMonoid[K comparable, V any]() M.Monoid[map[K]V] {
return G.UnionLastMonoid[map[K]V]()
//
//go:inline
func UnionLastMonoid[K comparable, V any]() Monoid[Record[K, V]] {
return G.UnionLastMonoid[Record[K, V]]()
}
// UnionFirstMonoid computes the union of two maps of the same type giving the first map precedence
func UnionFirstMonoid[K comparable, V any]() M.Monoid[map[K]V] {
return G.UnionFirstMonoid[map[K]V]()
//
//go:inline
func UnionFirstMonoid[K comparable, V any]() Monoid[Record[K, V]] {
return G.UnionFirstMonoid[Record[K, V]]()
}
// MergeMonoid computes the union of two maps of the same type giving the last map precedence
func MergeMonoid[K comparable, V any]() M.Monoid[map[K]V] {
return G.UnionLastMonoid[map[K]V]()
//
//go:inline
func MergeMonoid[K comparable, V any]() Monoid[Record[K, V]] {
return G.UnionLastMonoid[Record[K, V]]()
}

View File

@@ -16,295 +16,316 @@
package record
import (
EM "github.com/IBM/fp-go/v2/endomorphism"
Mg "github.com/IBM/fp-go/v2/magma"
Mo "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ord"
G "github.com/IBM/fp-go/v2/record/generic"
T "github.com/IBM/fp-go/v2/tuple"
)
// IsEmpty tests if a map is empty
func IsEmpty[K comparable, V any](r map[K]V) bool {
func IsEmpty[K comparable, V any](r Record[K, V]) bool {
return G.IsEmpty(r)
}
// IsNonEmpty tests if a map is not empty
func IsNonEmpty[K comparable, V any](r map[K]V) bool {
func IsNonEmpty[K comparable, V any](r Record[K, V]) bool {
return G.IsNonEmpty(r)
}
// Keys returns the key in a map
func Keys[K comparable, V any](r map[K]V) []K {
return G.Keys[map[K]V, []K](r)
func Keys[K comparable, V any](r Record[K, V]) []K {
return G.Keys[Record[K, V], []K](r)
}
// Values returns the values in a map
func Values[K comparable, V any](r map[K]V) []V {
return G.Values[map[K]V, []V](r)
func Values[K comparable, V any](r Record[K, V]) []V {
return G.Values[Record[K, V], []V](r)
}
// Collect applies a collector function to the key value pairs in a map and returns the result as an array
func Collect[K comparable, V, R any](f func(K, V) R) func(map[K]V) []R {
return G.Collect[map[K]V, []R](f)
func Collect[K comparable, V, R any](f func(K, V) R) func(Record[K, V]) []R {
return G.Collect[Record[K, V], []R](f)
}
// CollectOrd applies a collector function to the key value pairs in a map and returns the result as an array
func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(map[K]V) []R {
return G.CollectOrd[map[K]V, []R](o)
func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(Record[K, V]) []R {
return G.CollectOrd[Record[K, V], []R](o)
}
func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(map[K]V) R {
return G.Reduce[map[K]V](f, initial)
// Reduce reduces a map to a single value by applying a reducer function to each value
func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(Record[K, V]) R {
return G.Reduce[Record[K, V]](f, initial)
}
func ReduceWithIndex[K comparable, V, R any](f func(K, R, V) R, initial R) func(map[K]V) R {
return G.ReduceWithIndex[map[K]V](f, initial)
// ReduceWithIndex reduces a map to a single value by applying a reducer function to each key-value pair
func ReduceWithIndex[K comparable, V, R any](f func(K, R, V) R, initial R) func(Record[K, V]) R {
return G.ReduceWithIndex[Record[K, V]](f, initial)
}
func ReduceRef[K comparable, V, R any](f func(R, *V) R, initial R) func(map[K]V) R {
return G.ReduceRef[map[K]V](f, initial)
// ReduceRef reduces a map to a single value by applying a reducer function to each value reference
func ReduceRef[K comparable, V, R any](f func(R, *V) R, initial R) func(Record[K, V]) R {
return G.ReduceRef[Record[K, V]](f, initial)
}
func ReduceRefWithIndex[K comparable, V, R any](f func(K, R, *V) R, initial R) func(map[K]V) R {
return G.ReduceRefWithIndex[map[K]V](f, initial)
// ReduceRefWithIndex reduces a map to a single value by applying a reducer function to each key-value pair with value references
func ReduceRefWithIndex[K comparable, V, R any](f func(K, R, *V) R, initial R) func(Record[K, V]) R {
return G.ReduceRefWithIndex[Record[K, V]](f, initial)
}
func MonadMap[K comparable, V, R any](r map[K]V, f func(V) R) map[K]R {
return G.MonadMap[map[K]V, map[K]R](r, f)
// MonadMap transforms each value in a map using the provided function
func MonadMap[K comparable, V, R any](r Record[K, V], f func(V) R) Record[K, R] {
return G.MonadMap[Record[K, V], Record[K, R]](r, f)
}
func MonadMapWithIndex[K comparable, V, R any](r map[K]V, f func(K, V) R) map[K]R {
return G.MonadMapWithIndex[map[K]V, map[K]R](r, f)
// MonadMapWithIndex transforms each key-value pair in a map using the provided function
func MonadMapWithIndex[K comparable, V, R any](r Record[K, V], f func(K, V) R) Record[K, R] {
return G.MonadMapWithIndex[Record[K, V], Record[K, R]](r, f)
}
func MonadMapRefWithIndex[K comparable, V, R any](r map[K]V, f func(K, *V) R) map[K]R {
return G.MonadMapRefWithIndex[map[K]V, map[K]R](r, f)
// MonadMapRefWithIndex transforms each key-value pair in a map using the provided function with value references
func MonadMapRefWithIndex[K comparable, V, R any](r Record[K, V], f func(K, *V) R) Record[K, R] {
return G.MonadMapRefWithIndex[Record[K, V], Record[K, R]](r, f)
}
func MonadMapRef[K comparable, V, R any](r map[K]V, f func(*V) R) map[K]R {
return G.MonadMapRef[map[K]V, map[K]R](r, f)
// MonadMapRef transforms each value in a map using the provided function with value references
func MonadMapRef[K comparable, V, R any](r Record[K, V], f func(*V) R) Record[K, R] {
return G.MonadMapRef[Record[K, V], Record[K, R]](r, f)
}
func Map[K comparable, V, R any](f func(V) R) func(map[K]V) map[K]R {
return G.Map[map[K]V, map[K]R](f)
// Map returns a function that transforms each value in a map using the provided function
func Map[K comparable, V, R any](f func(V) R) Operator[K, V, R] {
return G.Map[Record[K, V], Record[K, R]](f)
}
func MapRef[K comparable, V, R any](f func(*V) R) func(map[K]V) map[K]R {
return G.MapRef[map[K]V, map[K]R](f)
// MapRef returns a function that transforms each value in a map using the provided function with value references
func MapRef[K comparable, V, R any](f func(*V) R) Operator[K, V, R] {
return G.MapRef[Record[K, V], Record[K, R]](f)
}
func MapWithIndex[K comparable, V, R any](f func(K, V) R) func(map[K]V) map[K]R {
return G.MapWithIndex[map[K]V, map[K]R](f)
// MapWithIndex returns a function that transforms each key-value pair in a map using the provided function
func MapWithIndex[K comparable, V, R any](f func(K, V) R) Operator[K, V, R] {
return G.MapWithIndex[Record[K, V], Record[K, R]](f)
}
func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) func(map[K]V) map[K]R {
return G.MapRefWithIndex[map[K]V, map[K]R](f)
// MapRefWithIndex returns a function that transforms each key-value pair in a map using the provided function with value references
func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) Operator[K, V, R] {
return G.MapRefWithIndex[Record[K, V], Record[K, R]](f)
}
// Lookup returns the entry for a key in a map if it exists
func Lookup[V any, K comparable](k K) func(map[K]V) O.Option[V] {
return G.Lookup[map[K]V](k)
func Lookup[V any, K comparable](k K) option.Kleisli[Record[K, V], V] {
return G.Lookup[Record[K, V]](k)
}
// MonadLookup returns the entry for a key in a map if it exists
func MonadLookup[V any, K comparable](m map[K]V, k K) O.Option[V] {
func MonadLookup[V any, K comparable](m Record[K, V], k K) Option[V] {
return G.MonadLookup(m, k)
}
// Has tests if a key is contained in a map
func Has[K comparable, V any](k K, r map[K]V) bool {
func Has[K comparable, V any](k K, r Record[K, V]) bool {
return G.Has(k, r)
}
func Union[K comparable, V any](m Mg.Magma[V]) func(map[K]V) func(map[K]V) map[K]V {
return G.Union[map[K]V](m)
// Union combines two maps using the provided Magma to resolve conflicts for duplicate keys
func Union[K comparable, V any](m Mg.Magma[V]) func(Record[K, V]) Operator[K, V, V] {
return G.Union[Record[K, V]](m)
}
// Merge combines two maps giving the values in the right one precedence. Also refer to [MergeMonoid]
func Merge[K comparable, V any](right map[K]V) func(map[K]V) map[K]V {
func Merge[K comparable, V any](right Record[K, V]) Operator[K, V, V] {
return G.Merge(right)
}
// Empty creates an empty map
func Empty[K comparable, V any]() map[K]V {
return G.Empty[map[K]V]()
func Empty[K comparable, V any]() Record[K, V] {
return G.Empty[Record[K, V]]()
}
// Size returns the number of elements in a map
func Size[K comparable, V any](r map[K]V) int {
func Size[K comparable, V any](r Record[K, V]) int {
return G.Size(r)
}
func ToArray[K comparable, V any](r map[K]V) []T.Tuple2[K, V] {
return G.ToArray[map[K]V, []T.Tuple2[K, V]](r)
// ToArray converts a map to an array of key-value pairs
func ToArray[K comparable, V any](r Record[K, V]) Entries[K, V] {
return G.ToArray[Record[K, V], Entries[K, V]](r)
}
func ToEntries[K comparable, V any](r map[K]V) []T.Tuple2[K, V] {
return G.ToEntries[map[K]V, []T.Tuple2[K, V]](r)
// ToEntries converts a map to an array of key-value pairs (alias for ToArray)
func ToEntries[K comparable, V any](r Record[K, V]) Entries[K, V] {
return G.ToEntries[Record[K, V], Entries[K, V]](r)
}
func FromEntries[K comparable, V any](fa []T.Tuple2[K, V]) map[K]V {
return G.FromEntries[map[K]V](fa)
// FromEntries creates a map from an array of key-value pairs
func FromEntries[K comparable, V any](fa Entries[K, V]) Record[K, V] {
return G.FromEntries[Record[K, V]](fa)
}
func UpsertAt[K comparable, V any](k K, v V) func(map[K]V) map[K]V {
return G.UpsertAt[map[K]V](k, v)
// UpsertAt returns a function that inserts or updates a key-value pair in a map
func UpsertAt[K comparable, V any](k K, v V) Operator[K, V, V] {
return G.UpsertAt[Record[K, V]](k, v)
}
func DeleteAt[K comparable, V any](k K) func(map[K]V) map[K]V {
return G.DeleteAt[map[K]V](k)
// DeleteAt returns a function that removes a key from a map
func DeleteAt[K comparable, V any](k K) Operator[K, V, V] {
return G.DeleteAt[Record[K, V]](k)
}
// Singleton creates a new map with a single entry
func Singleton[K comparable, V any](k K, v V) map[K]V {
return G.Singleton[map[K]V](k, v)
func Singleton[K comparable, V any](k K, v V) Record[K, V] {
return G.Singleton[Record[K, V]](k, v)
}
// FilterMapWithIndex creates a new map with only the elements for which the transformation function creates a Some
func FilterMapWithIndex[K comparable, V1, V2 any](f func(K, V1) O.Option[V2]) func(map[K]V1) map[K]V2 {
return G.FilterMapWithIndex[map[K]V1, map[K]V2](f)
func FilterMapWithIndex[K comparable, V1, V2 any](f func(K, V1) Option[V2]) Operator[K, V1, V2] {
return G.FilterMapWithIndex[Record[K, V1], Record[K, V2]](f)
}
// FilterMap creates a new map with only the elements for which the transformation function creates a Some
func FilterMap[K comparable, V1, V2 any](f func(V1) O.Option[V2]) func(map[K]V1) map[K]V2 {
return G.FilterMap[map[K]V1, map[K]V2](f)
func FilterMap[K comparable, V1, V2 any](f option.Kleisli[V1, V2]) Operator[K, V1, V2] {
return G.FilterMap[Record[K, V1], Record[K, V2]](f)
}
// Filter creates a new map with only the elements that match the predicate
func Filter[K comparable, V any](f func(K) bool) func(map[K]V) map[K]V {
return G.Filter[map[K]V](f)
func Filter[K comparable, V any](f Predicate[K]) Operator[K, V, V] {
return G.Filter[Record[K, V]](f)
}
// FilterWithIndex creates a new map with only the elements that match the predicate
func FilterWithIndex[K comparable, V any](f func(K, V) bool) func(map[K]V) map[K]V {
return G.FilterWithIndex[map[K]V](f)
func FilterWithIndex[K comparable, V any](f PredicateWithIndex[K, V]) Operator[K, V, V] {
return G.FilterWithIndex[Record[K, V]](f)
}
// IsNil checks if the map is set to nil
func IsNil[K comparable, V any](m map[K]V) bool {
func IsNil[K comparable, V any](m Record[K, V]) bool {
return G.IsNil(m)
}
// IsNonNil checks if the map is set to nil
func IsNonNil[K comparable, V any](m map[K]V) bool {
func IsNonNil[K comparable, V any](m Record[K, V]) bool {
return G.IsNonNil(m)
}
// ConstNil return a nil map
func ConstNil[K comparable, V any]() map[K]V {
return map[K]V(nil)
func ConstNil[K comparable, V any]() Record[K, V] {
return Record[K, V](nil)
}
func MonadChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2], r map[K]V1, f func(K, V1) map[K]V2) map[K]V2 {
// MonadChainWithIndex chains a map transformation function that produces maps, combining results using the provided Monoid
func MonadChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]], r Record[K, V1], f KleisliWithIndex[K, V1, V2]) Record[K, V2] {
return G.MonadChainWithIndex(m, r, f)
}
func MonadChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2], r map[K]V1, f func(V1) map[K]V2) map[K]V2 {
// MonadChain chains a map transformation function that produces maps, combining results using the provided Monoid
func MonadChain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]], r Record[K, V1], f Kleisli[K, V1, V2]) Record[K, V2] {
return G.MonadChain(m, r, f)
}
func ChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) map[K]V2) func(map[K]V1) map[K]V2 {
return G.ChainWithIndex[map[K]V1](m)
// ChainWithIndex returns a function that chains a map transformation function that produces maps, combining results using the provided Monoid
func ChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(KleisliWithIndex[K, V1, V2]) Operator[K, V1, V2] {
return G.ChainWithIndex[Record[K, V1]](m)
}
func Chain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) map[K]V2) func(map[K]V1) map[K]V2 {
return G.Chain[map[K]V1](m)
// Chain returns a function that chains a map transformation function that produces maps, combining results using the provided Monoid
func Chain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(Kleisli[K, V1, V2]) Operator[K, V1, V2] {
return G.Chain[Record[K, V1]](m)
}
// Flatten converts a nested map into a regular map
func Flatten[K comparable, V any](m Mo.Monoid[map[K]V]) func(map[K]map[K]V) map[K]V {
return G.Flatten[map[K]map[K]V](m)
func Flatten[K comparable, V any](m Monoid[Record[K, V]]) func(Record[K, Record[K, V]]) Record[K, V] {
return G.Flatten[Record[K, Record[K, V]]](m)
}
// FilterChainWithIndex creates a new map with only the elements for which the transformation function creates a Some
func FilterChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) O.Option[map[K]V2]) func(map[K]V1) map[K]V2 {
return G.FilterChainWithIndex[map[K]V1](m)
func FilterChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(func(K, V1) Option[Record[K, V2]]) Operator[K, V1, V2] {
return G.FilterChainWithIndex[Record[K, V1]](m)
}
// FilterChain creates a new map with only the elements for which the transformation function creates a Some
func FilterChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) O.Option[map[K]V2]) func(map[K]V1) map[K]V2 {
return G.FilterChain[map[K]V1](m)
func FilterChain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(option.Kleisli[V1, Record[K, V2]]) Operator[K, V1, V2] {
return G.FilterChain[Record[K, V1]](m)
}
// FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid.
func FoldMap[K comparable, A, B any](m Mo.Monoid[B]) func(func(A) B) func(map[K]A) B {
return G.FoldMap[map[K]A](m)
func FoldMap[K comparable, A, B any](m Monoid[B]) func(func(A) B) func(Record[K, A]) B {
return G.FoldMap[Record[K, A]](m)
}
// FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid.
func FoldMapWithIndex[K comparable, A, B any](m Mo.Monoid[B]) func(func(K, A) B) func(map[K]A) B {
return G.FoldMapWithIndex[map[K]A](m)
func FoldMapWithIndex[K comparable, A, B any](m Monoid[B]) func(func(K, A) B) func(Record[K, A]) B {
return G.FoldMapWithIndex[Record[K, A]](m)
}
// Fold folds the record using the provided Monoid.
func Fold[K comparable, A any](m Mo.Monoid[A]) func(map[K]A) A {
return G.Fold[map[K]A](m)
func Fold[K comparable, A any](m Monoid[A]) func(Record[K, A]) A {
return G.Fold[Record[K, A]](m)
}
// ReduceOrdWithIndex reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order
func ReduceOrdWithIndex[V, R any, K comparable](o ord.Ord[K]) func(func(K, R, V) R, R) func(map[K]V) R {
return G.ReduceOrdWithIndex[map[K]V, K, V, R](o)
func ReduceOrdWithIndex[V, R any, K comparable](o ord.Ord[K]) func(func(K, R, V) R, R) func(Record[K, V]) R {
return G.ReduceOrdWithIndex[Record[K, V], K, V, R](o)
}
// ReduceOrd reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order
func ReduceOrd[V, R any, K comparable](o ord.Ord[K]) func(func(R, V) R, R) func(map[K]V) R {
return G.ReduceOrd[map[K]V, K, V, R](o)
func ReduceOrd[V, R any, K comparable](o ord.Ord[K]) func(func(R, V) R, R) func(Record[K, V]) R {
return G.ReduceOrd[Record[K, V], K, V, R](o)
}
// FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order
func FoldMapOrd[A, B any, K comparable](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(A) B) func(map[K]A) B {
return G.FoldMapOrd[map[K]A, K, A, B](o)
func FoldMapOrd[A, B any, K comparable](o ord.Ord[K]) func(m Monoid[B]) func(func(A) B) func(Record[K, A]) B {
return G.FoldMapOrd[Record[K, A], K, A, B](o)
}
// Fold folds the record using the provided Monoid with the items passed in the given order
func FoldOrd[A any, K comparable](o ord.Ord[K]) func(m Mo.Monoid[A]) func(map[K]A) A {
return G.FoldOrd[map[K]A](o)
func FoldOrd[A any, K comparable](o ord.Ord[K]) func(m Monoid[A]) func(Record[K, A]) A {
return G.FoldOrd[Record[K, A]](o)
}
// FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order
func FoldMapOrdWithIndex[K comparable, A, B any](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(K, A) B) func(map[K]A) B {
return G.FoldMapOrdWithIndex[map[K]A, K, A, B](o)
func FoldMapOrdWithIndex[K comparable, A, B any](o ord.Ord[K]) func(m Monoid[B]) func(func(K, A) B) func(Record[K, A]) B {
return G.FoldMapOrdWithIndex[Record[K, A], K, A, B](o)
}
// KeysOrd returns the keys in the map in their given order
func KeysOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []K {
return G.KeysOrd[map[K]V, []K](o)
func KeysOrd[V any, K comparable](o ord.Ord[K]) func(r Record[K, V]) []K {
return G.KeysOrd[Record[K, V], []K](o)
}
// ValuesOrd returns the values in the map ordered by their keys in the given order
func ValuesOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []V {
return G.ValuesOrd[map[K]V, []V](o)
func ValuesOrd[V any, K comparable](o ord.Ord[K]) func(r Record[K, V]) []V {
return G.ValuesOrd[Record[K, V], []V](o)
}
func MonadFlap[B any, K comparable, A any](fab map[K]func(A) B, a A) map[K]B {
return G.MonadFlap[map[K]func(A) B, map[K]B](fab, a)
// MonadFlap applies a value to a map of functions, producing a map of results
func MonadFlap[B any, K comparable, A any](fab Record[K, func(A) B], a A) Record[K, B] {
return G.MonadFlap[Record[K, func(A) B], Record[K, B]](fab, a)
}
func Flap[B any, K comparable, A any](a A) func(map[K]func(A) B) map[K]B {
return G.Flap[map[K]func(A) B, map[K]B](a)
// Flap returns a function that applies a value to a map of functions, producing a map of results
func Flap[B any, K comparable, A any](a A) Operator[K, func(A) B, B] {
return G.Flap[Record[K, func(A) B], Record[K, B]](a)
}
// Copy creates a shallow copy of the map
func Copy[K comparable, V any](m map[K]V) map[K]V {
func Copy[K comparable, V any](m Record[K, V]) Record[K, V] {
return G.Copy(m)
}
// Clone creates a deep copy of the map using the provided endomorphism to clone the values
func Clone[K comparable, V any](f EM.Endomorphism[V]) EM.Endomorphism[map[K]V] {
return G.Clone[map[K]V](f)
func Clone[K comparable, V any](f Endomorphism[V]) Endomorphism[Record[K, V]] {
return G.Clone[Record[K, V]](f)
}
// FromFoldableMap converts from a reducer to a map
// Duplicate keys are resolved by the provided [Mg.Magma]
func FromFoldableMap[
FOLDABLE ~func(func(map[K]V, A) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function
FOLDABLE ~func(func(Record[K, V], A) Record[K, V], Record[K, V]) func(HKTA) Record[K, V], // the reduce function
A any,
HKTA any,
K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(f func(A) T.Tuple2[K, V]) func(fa HKTA) map[K]V {
return G.FromFoldableMap[func(A) T.Tuple2[K, V]](m, red)
V any](m Mg.Magma[V], red FOLDABLE) func(f func(A) Entry[K, V]) Kleisli[K, HKTA, V] {
return G.FromFoldableMap[func(A) Entry[K, V]](m, red)
}
// FromArrayMap converts from an array to a map
@@ -312,17 +333,17 @@ func FromFoldableMap[
func FromArrayMap[
A any,
K comparable,
V any](m Mg.Magma[V]) func(f func(A) T.Tuple2[K, V]) func(fa []A) map[K]V {
return G.FromArrayMap[func(A) T.Tuple2[K, V], []A, map[K]V](m)
V any](m Mg.Magma[V]) func(f func(A) Entry[K, V]) Kleisli[K, []A, V] {
return G.FromArrayMap[func(A) Entry[K, V], []A, Record[K, V]](m)
}
// FromFoldable converts from a reducer to a map
// Duplicate keys are resolved by the provided [Mg.Magma]
func FromFoldable[
HKTA any,
FOLDABLE ~func(func(map[K]V, T.Tuple2[K, V]) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function
FOLDABLE ~func(func(Record[K, V], Entry[K, V]) Record[K, V], Record[K, V]) func(HKTA) Record[K, V], // the reduce function
K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) map[K]V {
V any](m Mg.Magma[V], red FOLDABLE) Kleisli[K, HKTA, V] {
return G.FromFoldable(m, red)
}
@@ -330,14 +351,21 @@ func FromFoldable[
// Duplicate keys are resolved by the provided [Mg.Magma]
func FromArray[
K comparable,
V any](m Mg.Magma[V]) func(fa []T.Tuple2[K, V]) map[K]V {
return G.FromArray[[]T.Tuple2[K, V], map[K]V](m)
V any](m Mg.Magma[V]) Kleisli[K, Entries[K, V], V] {
return G.FromArray[Entries[K, V], Record[K, V]](m)
}
func MonadAp[A any, K comparable, B any](m Mo.Monoid[map[K]B], fab map[K]func(A) B, fa map[K]A) map[K]B {
// MonadAp applies a map of functions to a map of values, combining results using the provided Monoid
func MonadAp[A any, K comparable, B any](m Monoid[Record[K, B]], fab Record[K, func(A) B], fa Record[K, A]) Record[K, B] {
return G.MonadAp(m, fab, fa)
}
func Ap[A any, K comparable, B any](m Mo.Monoid[map[K]B]) func(fa map[K]A) func(map[K]func(A) B) map[K]B {
return G.Ap[map[K]B, map[K]func(A) B, map[K]A](m)
// Ap returns a function that applies a map of functions to a map of values, combining results using the provided Monoid
func Ap[A any, K comparable, B any](m Monoid[Record[K, B]]) func(fa Record[K, A]) Operator[K, func(A) B, B] {
return G.Ap[Record[K, B], Record[K, func(A) B], Record[K, A]](m)
}
// Of creates a map with a single key-value pair
func Of[K comparable, A any](k K, a A) Record[K, A] {
return Record[K, A]{k: a}
}

View File

@@ -25,8 +25,8 @@ import (
"github.com/IBM/fp-go/v2/internal/utils"
Mg "github.com/IBM/fp-go/v2/magma"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
@@ -156,7 +156,7 @@ func TestFromArrayMap(t *testing.T) {
src1 := A.From("a", "b", "c", "a")
frm := FromArrayMap[string, string](Mg.Second[string]())
f := frm(T.Replicate2[string])
f := frm(P.Of[string])
res1 := f(src1)
@@ -198,3 +198,555 @@ func TestHas(t *testing.T) {
assert.True(t, Has("a", nonEmpty))
assert.False(t, Has("c", nonEmpty))
}
func TestCollect(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
collector := Collect[string, int, string](func(k string, v int) string {
return fmt.Sprintf("%s=%d", k, v)
})
result := collector(data)
sort.Strings(result)
assert.Equal(t, []string{"a=1", "b=2", "c=3"}, result)
}
func TestCollectOrd(t *testing.T) {
data := map[string]int{
"c": 3,
"a": 1,
"b": 2,
}
collector := CollectOrd[int, string](S.Ord)(func(k string, v int) string {
return fmt.Sprintf("%s=%d", k, v)
})
result := collector(data)
assert.Equal(t, []string{"a=1", "b=2", "c=3"}, result)
}
func TestReduce(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
sum := Reduce[string, int, int](func(acc, v int) int {
return acc + v
}, 0)
result := sum(data)
assert.Equal(t, 6, result)
}
func TestReduceWithIndex(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
concat := ReduceWithIndex[string, int, string](func(k string, acc string, v int) string {
if acc == "" {
return fmt.Sprintf("%s:%d", k, v)
}
return fmt.Sprintf("%s,%s:%d", acc, k, v)
}, "")
result := concat(data)
// Result order is non-deterministic, so check it contains all parts
assert.Contains(t, result, "a:1")
assert.Contains(t, result, "b:2")
assert.Contains(t, result, "c:3")
}
func TestMonadMap(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
result := MonadMap(data, func(v int) int { return v * 2 })
assert.Equal(t, map[string]int{"a": 2, "b": 4, "c": 6}, result)
}
func TestMonadMapWithIndex(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
}
result := MonadMapWithIndex(data, func(k string, v int) string {
return fmt.Sprintf("%s=%d", k, v)
})
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
}
func TestMapWithIndex(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
}
mapper := MapWithIndex[string, int, string](func(k string, v int) string {
return fmt.Sprintf("%s=%d", k, v)
})
result := mapper(data)
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
}
func TestMonadLookup(t *testing.T) {
data := map[string]int{
"a": 1,
"b": 2,
}
assert.Equal(t, O.Some(1), MonadLookup(data, "a"))
assert.Equal(t, O.None[int](), MonadLookup(data, "c"))
}
func TestMerge(t *testing.T) {
left := map[string]int{"a": 1, "b": 2}
right := map[string]int{"b": 3, "c": 4}
result := Merge(right)(left)
assert.Equal(t, map[string]int{"a": 1, "b": 3, "c": 4}, result)
}
func TestSize(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
assert.Equal(t, 3, Size(data))
assert.Equal(t, 0, Size(Empty[string, int]()))
}
func TestToArray(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := ToArray(data)
assert.Len(t, result, 2)
// Check both entries exist (order is non-deterministic)
found := make(map[string]int)
for _, entry := range result {
found[P.Head(entry)] = P.Tail(entry)
}
assert.Equal(t, data, found)
}
func TestToEntries(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := ToEntries(data)
assert.Len(t, result, 2)
}
func TestFromEntries(t *testing.T) {
entries := Entries[string, int]{
P.MakePair("a", 1),
P.MakePair("b", 2),
}
result := FromEntries(entries)
assert.Equal(t, map[string]int{"a": 1, "b": 2}, result)
}
func TestUpsertAt(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := UpsertAt("c", 3)(data)
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, result)
// Original should be unchanged
assert.Equal(t, map[string]int{"a": 1, "b": 2}, data)
// Update existing
result2 := UpsertAt("a", 10)(data)
assert.Equal(t, map[string]int{"a": 10, "b": 2}, result2)
}
func TestDeleteAt(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
result := DeleteAt[string, int]("b")(data)
assert.Equal(t, map[string]int{"a": 1, "c": 3}, result)
// Original should be unchanged
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, data)
}
func TestSingleton(t *testing.T) {
result := Singleton("key", 42)
assert.Equal(t, map[string]int{"key": 42}, result)
}
func TestFilterMapWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
filter := FilterMapWithIndex[string, int, int](func(k string, v int) O.Option[int] {
if v%2 == 0 {
return O.Some(v * 10)
}
return O.None[int]()
})
result := filter(data)
assert.Equal(t, map[string]int{"b": 20}, result)
}
func TestFilterMap(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
filter := FilterMap[string, int, int](func(v int) O.Option[int] {
if v%2 == 0 {
return O.Some(v * 10)
}
return O.None[int]()
})
result := filter(data)
assert.Equal(t, map[string]int{"b": 20}, result)
}
func TestFilter(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
filter := Filter[string, int](func(k string) bool {
return k != "b"
})
result := filter(data)
assert.Equal(t, map[string]int{"a": 1, "c": 3}, result)
}
func TestFilterWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
filter := FilterWithIndex[string, int](func(k string, v int) bool {
return v%2 == 0
})
result := filter(data)
assert.Equal(t, map[string]int{"b": 2}, result)
}
func TestIsNil(t *testing.T) {
var nilMap map[string]int
nonNilMap := map[string]int{}
assert.True(t, IsNil(nilMap))
assert.False(t, IsNil(nonNilMap))
}
func TestIsNonNil(t *testing.T) {
var nilMap map[string]int
nonNilMap := map[string]int{}
assert.False(t, IsNonNil(nilMap))
assert.True(t, IsNonNil(nonNilMap))
}
func TestConstNil(t *testing.T) {
result := ConstNil[string, int]()
assert.Nil(t, result)
assert.True(t, IsNil(result))
}
func TestMonadChain(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
monoid := MergeMonoid[string, int]()
result := MonadChain(monoid, data, func(v int) map[string]int {
return map[string]int{
fmt.Sprintf("x%d", v): v * 10,
}
})
assert.Equal(t, map[string]int{"x1": 10, "x2": 20}, result)
}
func TestChain(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
monoid := MergeMonoid[string, int]()
chain := Chain[int, string, int](monoid)(func(v int) map[string]int {
return map[string]int{
fmt.Sprintf("x%d", v): v * 10,
}
})
result := chain(data)
assert.Equal(t, map[string]int{"x1": 10, "x2": 20}, result)
}
func TestFlatten(t *testing.T) {
nested := map[string]map[string]int{
"a": {"x": 1, "y": 2},
"b": {"z": 3},
}
monoid := MergeMonoid[string, int]()
flatten := Flatten(monoid)
result := flatten(nested)
assert.Equal(t, map[string]int{"x": 1, "y": 2, "z": 3}, result)
}
func TestFoldMap(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
// Use string monoid for simplicity
fold := FoldMap[string, int, string](S.Monoid)(func(v int) string {
return fmt.Sprintf("%d", v)
})
result := fold(data)
// Result contains all digits but order is non-deterministic
assert.Contains(t, result, "1")
assert.Contains(t, result, "2")
assert.Contains(t, result, "3")
}
func TestFold(t *testing.T) {
data := map[string]string{"a": "A", "b": "B", "c": "C"}
fold := Fold[string](S.Monoid)
result := fold(data)
// Result contains all letters but order is non-deterministic
assert.Contains(t, result, "A")
assert.Contains(t, result, "B")
assert.Contains(t, result, "C")
}
func TestKeysOrd(t *testing.T) {
data := map[string]int{"c": 3, "a": 1, "b": 2}
keys := KeysOrd[int](S.Ord)(data)
assert.Equal(t, []string{"a", "b", "c"}, keys)
}
func TestMonadFlap(t *testing.T) {
fns := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
"triple": func(x int) int { return x * 3 },
}
result := MonadFlap(fns, 5)
assert.Equal(t, map[string]int{"double": 10, "triple": 15}, result)
}
func TestFlap(t *testing.T) {
fns := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
"triple": func(x int) int { return x * 3 },
}
flap := Flap[int, string, int](5)
result := flap(fns)
assert.Equal(t, map[string]int{"double": 10, "triple": 15}, result)
}
func TestFromArray(t *testing.T) {
entries := Entries[string, int]{
P.MakePair("a", 1),
P.MakePair("b", 2),
P.MakePair("a", 3), // Duplicate key
}
// Use Second magma to keep last value
from := FromArray[string, int](Mg.Second[int]())
result := from(entries)
assert.Equal(t, map[string]int{"a": 3, "b": 2}, result)
}
func TestMonadAp(t *testing.T) {
fns := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
}
vals := map[string]int{
"double": 5,
}
monoid := MergeMonoid[string, int]()
result := MonadAp(monoid, fns, vals)
assert.Equal(t, map[string]int{"double": 10}, result)
}
func TestAp(t *testing.T) {
fns := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
}
vals := map[string]int{
"double": 5,
}
monoid := MergeMonoid[string, int]()
ap := Ap[int, string, int](monoid)(vals)
result := ap(fns)
assert.Equal(t, map[string]int{"double": 10}, result)
}
func TestOf(t *testing.T) {
result := Of("key", 42)
assert.Equal(t, map[string]int{"key": 42}, result)
}
func TestReduceRef(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
sum := ReduceRef[string, int, int](func(acc int, v *int) int {
return acc + *v
}, 0)
result := sum(data)
assert.Equal(t, 6, result)
}
func TestReduceRefWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
concat := ReduceRefWithIndex[string, int, string](func(k string, acc string, v *int) string {
if acc == "" {
return fmt.Sprintf("%s:%d", k, *v)
}
return fmt.Sprintf("%s,%s:%d", acc, k, *v)
}, "")
result := concat(data)
assert.Contains(t, result, "a:1")
assert.Contains(t, result, "b:2")
}
func TestMonadMapRef(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := MonadMapRef(data, func(v *int) int { return *v * 2 })
assert.Equal(t, map[string]int{"a": 2, "b": 4}, result)
}
func TestMapRef(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
mapper := MapRef[string, int, int](func(v *int) int { return *v * 2 })
result := mapper(data)
assert.Equal(t, map[string]int{"a": 2, "b": 4}, result)
}
func TestMonadMapRefWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
result := MonadMapRefWithIndex(data, func(k string, v *int) string {
return fmt.Sprintf("%s=%d", k, *v)
})
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
}
func TestMapRefWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
mapper := MapRefWithIndex[string, int, string](func(k string, v *int) string {
return fmt.Sprintf("%s=%d", k, *v)
})
result := mapper(data)
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
}
func TestUnion(t *testing.T) {
left := map[string]int{"a": 1, "b": 2}
right := map[string]int{"b": 3, "c": 4}
// Union combines maps, with the magma resolving conflicts
// The order is union(left)(right), which means right is merged into left
// First magma keeps the first value (from right in this case)
union := Union[string, int](Mg.First[int]())
result := union(left)(right)
assert.Equal(t, map[string]int{"a": 1, "b": 3, "c": 4}, result)
// Second magma keeps the second value (from left in this case)
union2 := Union[string, int](Mg.Second[int]())
result2 := union2(left)(right)
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 4}, result2)
}
func TestMonadChainWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
monoid := MergeMonoid[string, string]()
result := MonadChainWithIndex(monoid, data, func(k string, v int) map[string]string {
return map[string]string{
fmt.Sprintf("%s%d", k, v): fmt.Sprintf("val%d", v),
}
})
assert.Equal(t, map[string]string{"a1": "val1", "b2": "val2"}, result)
}
func TestChainWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2}
monoid := MergeMonoid[string, string]()
chain := ChainWithIndex[int, string, string](monoid)(func(k string, v int) map[string]string {
return map[string]string{
fmt.Sprintf("%s%d", k, v): fmt.Sprintf("val%d", v),
}
})
result := chain(data)
assert.Equal(t, map[string]string{"a1": "val1", "b2": "val2"}, result)
}
func TestFilterChainWithIndex(t *testing.T) {
src := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
f := func(k string, value int) O.Option[map[string]string] {
if value%2 != 0 {
return O.Of(map[string]string{
k: fmt.Sprintf("%s%d", k, value),
})
}
return O.None[map[string]string]()
}
monoid := MergeMonoid[string, string]()
res := FilterChainWithIndex[int](monoid)(f)(src)
assert.Equal(t, map[string]string{
"a": "a1",
"c": "c3",
}, res)
}
func TestFoldMapWithIndex(t *testing.T) {
data := map[string]int{"a": 1, "b": 2, "c": 3}
fold := FoldMapWithIndex[string, int, string](S.Monoid)(func(k string, v int) string {
return fmt.Sprintf("%s:%d", k, v)
})
result := fold(data)
// Result contains all pairs but order is non-deterministic
assert.Contains(t, result, "a:1")
assert.Contains(t, result, "b:2")
assert.Contains(t, result, "c:3")
}
func TestReduceOrd(t *testing.T) {
data := map[string]int{"c": 3, "a": 1, "b": 2}
sum := ReduceOrd[int, int](S.Ord)(func(acc, v int) int {
return acc + v
}, 0)
result := sum(data)
assert.Equal(t, 6, result)
}
func TestReduceOrdWithIndex(t *testing.T) {
data := map[string]int{"c": 3, "a": 1, "b": 2}
concat := ReduceOrdWithIndex[int, string](S.Ord)(func(k string, acc string, v int) string {
if acc == "" {
return fmt.Sprintf("%s:%d", k, v)
}
return fmt.Sprintf("%s,%s:%d", acc, k, v)
}, "")
result := concat(data)
// With Ord, keys should be in order
assert.Equal(t, "a:1,b:2,c:3", result)
}
func TestFoldMapOrdWithIndex(t *testing.T) {
data := map[string]int{"c": 3, "a": 1, "b": 2}
fold := FoldMapOrdWithIndex[string, int, string](S.Ord)(S.Monoid)(func(k string, v int) string {
return fmt.Sprintf("%s:%d,", k, v)
})
result := fold(data)
assert.Equal(t, "a:1,b:2,c:3,", result)
}
func TestFoldOrd(t *testing.T) {
data := map[string]string{"c": "C", "a": "A", "b": "B"}
fold := FoldOrd[string](S.Ord)(S.Monoid)
result := fold(data)
assert.Equal(t, "ABC", result)
}
func TestFromFoldableMap(t *testing.T) {
src := A.From("a", "b", "c", "a")
// Create a reducer function
reducer := A.Reduce[string, map[string]string]
from := FromFoldableMap[func(func(map[string]string, string) map[string]string, map[string]string) func([]string) map[string]string, string, []string, string, string](
Mg.Second[string](),
reducer,
)
f := from(P.Of[string])
result := f(src)
assert.Equal(t, map[string]string{
"a": "a",
"b": "b",
"c": "c",
}, result)
}
func TestFromFoldable(t *testing.T) {
entries := Entries[string, int]{
P.MakePair("a", 1),
P.MakePair("b", 2),
P.MakePair("a", 3), // Duplicate key
}
reducer := A.Reduce[Entry[string, int], map[string]int]
from := FromFoldable[[]Entry[string, int], func(func(map[string]int, Entry[string, int]) map[string]int, map[string]int) func([]Entry[string, int]) map[string]int, string, int](
Mg.Second[int](),
reducer,
)
result := from(entries)
assert.Equal(t, map[string]int{"a": 3, "b": 2}, result)
}

Some files were not shown because too many files have changed in this diff Show More