mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-19 23:42:05 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3c466bfb7 | ||
|
|
a6c6ea804f | ||
|
|
31ff98901e | ||
|
|
255cf4353c | ||
|
|
4dfc1b5a44 | ||
|
|
20398e67a9 | ||
|
|
fceda15701 |
16
v2/README.md
16
v2/README.md
@@ -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:**
|
||||
|
||||
@@ -368,5 +368,3 @@ func TestToNonEmptyArrayUseCases(t *testing.T) {
|
||||
assert.Equal(t, "default", result2)
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
41
v2/context/readerio/retry.go
Normal file
41
v2/context/readerio/retry.go
Normal 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,
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
179
v2/context/readerioresult/retry.go
Normal file
179
v2/context/readerioresult/retry.go
Normal 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,
|
||||
)
|
||||
|
||||
}
|
||||
511
v2/context/readerioresult/retry_test.go
Normal file
511
v2/context/readerioresult/retry_test.go
Normal 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")
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
84
v2/context/readerresult/retry.go
Normal file
84
v2/context/readerresult/retry.go
Normal 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,
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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].
|
||||
|
||||
149
v2/either/examples_format_test.go
Normal file
149
v2/either/examples_format_test.go
Normal 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
103
v2/either/format.go
Normal 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
311
v2/either/format_test.go
Normal 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"))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1033
v2/idiomatic/context/readerresult/README.md
Normal file
1033
v2/idiomatic/context/readerresult/README.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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](
|
||||
|
||||
627
v2/idiomatic/context/readerresult/bind_test.go
Normal file
627
v2/idiomatic/context/readerresult/bind_test.go
Normal 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)
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
630
v2/idiomatic/context/readerresult/bracket_test.go
Normal file
630
v2/idiomatic/context/readerresult/bracket_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
146
v2/idiomatic/context/readerresult/retry.go
Normal file
146
v2/idiomatic/context/readerresult/retry.go
Normal 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,
|
||||
)
|
||||
}
|
||||
482
v2/idiomatic/context/readerresult/retry_test.go
Normal file
482
v2/idiomatic/context/readerresult/retry_test.go
Normal 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")
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
78
v2/internal/formatting/type.go
Normal file
78
v2/internal/formatting/type.go
Normal 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
|
||||
}
|
||||
)
|
||||
123
v2/internal/formatting/utils.go
Normal file
123
v2/internal/formatting/utils.go
Normal 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(), "*")
|
||||
}
|
||||
369
v2/internal/formatting/utils_test.go
Normal file
369
v2/internal/formatting/utils_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
84
v2/optics/iso/format.go
Normal 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")
|
||||
}
|
||||
@@ -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
85
v2/optics/lens/format.go
Normal 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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
85
v2/optics/optional/format.go
Normal file
85
v2/optics/optional/format.go
Normal 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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
85
v2/optics/prism/format.go
Normal 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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -48,7 +48,7 @@ func ExampleOption_creation() {
|
||||
// Output:
|
||||
// None[int]
|
||||
// Some[string](value)
|
||||
// None[*string]
|
||||
// None[string]
|
||||
// true
|
||||
// None[int]
|
||||
// Some[int](4)
|
||||
|
||||
164
v2/option/examples_format_test.go
Normal file
164
v2/option/examples_format_test.go
Normal 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
100
v2/option/format.go
Normal 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
307
v2/option/format_test.go
Normal 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]()
|
||||
})
|
||||
}
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
175
v2/pair/examples_format_test.go
Normal file
175
v2/pair/examples_format_test.go
Normal 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
89
v2/pair/format.go
Normal 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
272
v2/pair/format_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
32
v2/reader/bracket.go
Normal 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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
40
v2/readerio/retry.go
Normal 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,
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
v2/readerioeither/retry.go
Normal file
30
v2/readerioeither/retry.go
Normal 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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
109
v2/readerioresult/retry.go
Normal 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)
|
||||
}
|
||||
315
v2/readerioresult/retry_test.go
Normal file
315
v2/readerioresult/retry_test.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
227
v2/record/bind_test.go
Normal 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
85
v2/record/coverage.out
Normal 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
|
||||
@@ -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]]()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]]()
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user