1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-28 13:12:03 +02:00

Compare commits

..

10 Commits

Author SHA1 Message Date
Dr. Carsten Leue
77a8cc6b09 fix: implement ApSO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:44:27 +01:00
Dr. Carsten Leue
bc8743fdfc fix: build error
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:21:37 +01:00
Dr. Carsten Leue
1837d3f86d fix: add semigroup helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 16:39:05 +01:00
Dr. Carsten Leue
b2d111e8ec fix: more doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 14:04:44 +01:00
Dr. Carsten Leue
ae141c85c6 fix: add tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 13:40:12 +01:00
Dr. Carsten Leue
1230b4581b doc: add doc links
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 10:12:20 +01:00
Dr. Carsten Leue
70c831c8f9 fix: simplify type arguments
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 16:27:21 +01:00
Dr. Carsten Leue
cc0c14c7cf fix: do not fail if coverage fails
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 11:35:16 +01:00
Dr. Carsten Leue
19159ad49e fix: WriteFile
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 10:42:46 +01:00
Dr. Carsten Leue
b9c8fb4ff1 fix: parameter order for Local and TapIOK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 17:55:32 +01:00
58 changed files with 5720 additions and 295 deletions

View File

@@ -42,6 +42,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -81,6 +82,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -106,6 +108,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -29,7 +29,7 @@ func TestFromReaderIOResult(t *testing.T) {
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Return a Reader that always passes
return result.Of[Reader](func(t *testing.T) bool {
return result.Of(func(t *testing.T) bool {
return true
})
}
@@ -46,7 +46,7 @@ func TestFromReaderIOResult(t *testing.T) {
// Create a ReaderIOResult that returns a successful Equal assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](Equal(42)(42))
return result.Of(Equal(42)(42))
}
}
@@ -80,7 +80,7 @@ func TestFromReaderIOResult(t *testing.T) {
// Create a ReaderIOResult that returns a failing assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](Equal(42)(43))
return result.Of(Equal(42)(43))
}
}
@@ -100,7 +100,7 @@ func TestFromReaderIOResult(t *testing.T) {
contextUsed = true
}
return func() result.Result[Reader] {
return result.Of[Reader](func(t *testing.T) bool {
return result.Of(func(t *testing.T) bool {
return true
})
}
@@ -118,7 +118,7 @@ func TestFromReaderIOResult(t *testing.T) {
// Create a ReaderIOResult that returns NoError assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](NoError(nil))
return result.Of(NoError(nil))
}
}
@@ -139,7 +139,7 @@ func TestFromReaderIOResult(t *testing.T) {
ArrayLength[int](3)(arr),
ArrayContains(2)(arr),
})
return result.Of[Reader](assertions)
return result.Of(assertions)
}
}
@@ -297,7 +297,7 @@ func TestFromReaderIO(t *testing.T) {
// Create a ReaderIO with Result assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
successResult := result.Of[int](42)
successResult := result.Of(42)
return Success(successResult)
}
}
@@ -338,7 +338,7 @@ func TestFromReaderIOResultIntegration(t *testing.T) {
}
// Return a successful assertion
return result.Of[Reader](Equal("test")("test"))
return result.Of(Equal("test")("test"))
}
}

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -388,8 +387,8 @@ func generateApplyHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -266,8 +265,8 @@ func generateBindHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -189,8 +188,8 @@ func generateDIHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -148,8 +147,8 @@ func generateEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -18,7 +18,6 @@ package cli
import (
"fmt"
"os"
"time"
)
func writePackage(f *os.File, pkg string) {
@@ -26,6 +25,6 @@ func writePackage(f *os.File, pkg string) {
fmt.Fprintf(f, "package %s\n\n", pkg)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
}

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -62,8 +61,8 @@ func generateIdentityHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -71,8 +70,8 @@ func generateIOHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -219,8 +218,8 @@ func generateIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -234,8 +233,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -76,8 +75,8 @@ func generateIOOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -148,8 +147,8 @@ func generateOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -378,8 +377,8 @@ func generatePipeHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -118,8 +117,8 @@ func generateReaderHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -131,8 +130,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -233,8 +232,8 @@ func generateReaderIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -246,8 +245,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -22,7 +22,6 @@ import (
"os"
"path/filepath"
"strings"
"time"
C "github.com/urfave/cli/v3"
)
@@ -399,8 +398,8 @@ func generateTupleHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -13,6 +13,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package constant provides the Const functor, a phantom type that ignores its second type parameter.
//
// The Const functor is a fundamental building block in functional programming that wraps a value
// of type E while having a phantom type parameter A. This makes it useful for:
// - Accumulating values during traversals (e.g., collecting metadata)
// - Implementing optics (lenses, prisms) where you need to track information
// - Building applicative functors that combine values using a semigroup
//
// # The Const Functor
//
// Const[E, A] wraps a value of type E and has a phantom type parameter A that doesn't affect
// the runtime value. This allows it to participate in functor and applicative operations while
// maintaining the wrapped value unchanged.
//
// # Key Properties
//
// - Map operations ignore the function and preserve the wrapped value
// - Ap operations combine wrapped values using a semigroup
// - The phantom type A allows type-safe composition with other functors
//
// # Example Usage
//
// // Accumulate string values
// c1 := Make[string, int]("hello")
// c2 := Make[string, int]("world")
//
// // Map doesn't change the wrapped value
// mapped := Map[string, int, string](strconv.Itoa)(c1) // Still contains "hello"
//
// // Ap combines values using a semigroup
// combined := Ap[string, int, int](S.Monoid)(c1)(c2) // Contains "helloworld"
package constant
import (
@@ -21,36 +52,209 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Const is a functor that wraps a value of type E with a phantom type parameter A.
//
// The Const functor is useful for accumulating values during traversals or implementing
// optics. The type parameter A is phantom - it doesn't affect the runtime value but allows
// the type to participate in functor and applicative operations.
//
// Type Parameters:
// - E: The type of the wrapped value (the actual data)
// - A: The phantom type parameter (not stored, only used for type-level operations)
//
// Example:
//
// // Create a Const that wraps a string
// c := Make[string, int]("metadata")
//
// // The int type parameter is phantom - no int value is stored
// value := Unwrap(c) // "metadata"
type Const[E, A any] struct {
value E
}
// Make creates a Const value wrapping the given value.
//
// This is the primary constructor for Const values. The second type parameter A
// is phantom and must be specified explicitly when needed for type inference.
//
// Type Parameters:
// - E: The type of the value to wrap
// - A: The phantom type parameter
//
// Parameters:
// - e: The value to wrap
//
// Returns:
// - A Const[E, A] wrapping the value
//
// Example:
//
// c := Make[string, int]("hello")
// value := Unwrap(c) // "hello"
func Make[E, A any](e E) Const[E, A] {
return Const[E, A]{value: e}
}
// Unwrap extracts the wrapped value from a Const.
//
// This is the inverse of Make, retrieving the actual value stored in the Const.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The phantom type parameter
//
// Parameters:
// - c: The Const to unwrap
//
// Returns:
// - The wrapped value of type E
//
// Example:
//
// c := Make[string, int]("world")
// value := Unwrap(c) // "world"
func Unwrap[E, A any](c Const[E, A]) E {
return c.value
}
// Of creates a Const containing the monoid's empty value, ignoring the input.
//
// This implements the Applicative's "pure" operation for Const. It creates a Const
// wrapping the monoid's identity element, regardless of the input value.
//
// Type Parameters:
// - E: The type of the wrapped value (must have a monoid)
// - A: The input type (ignored)
//
// Parameters:
// - m: The monoid providing the empty value
//
// Returns:
// - A function that ignores its input and returns Const[E, A] with the empty value
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// of := Of[string, int](S.Monoid)
// c := of(42) // Const[string, int] containing ""
// value := Unwrap(c) // ""
func Of[E, A any](m M.Monoid[E]) func(A) Const[E, A] {
return F.Constant1[A](Make[E, A](m.Empty()))
}
// MonadMap applies a function to the phantom type parameter without changing the wrapped value.
//
// This implements the Functor's map operation for Const. Since the type parameter A is phantom,
// the function is never actually called - the wrapped value E remains unchanged.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - fa: The Const to map over
// - _: The function to apply (ignored)
//
// Returns:
// - A Const[E, B] with the same wrapped value
//
// Example:
//
// c := Make[string, int]("hello")
// mapped := MonadMap(c, func(i int) string { return strconv.Itoa(i) })
// // mapped still contains "hello", function was never called
func MonadMap[E, A, B any](fa Const[E, A], _ func(A) B) Const[E, B] {
return Make[E, B](fa.value)
}
// MonadAp combines two Const values using a semigroup.
//
// This implements the Applicative's ap operation for Const. It combines the wrapped
// values from both Const instances using the provided semigroup, ignoring the function
// type in the first argument.
//
// Type Parameters:
// - E: The type of the wrapped values (must have a semigroup)
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - s: The semigroup for combining wrapped values
//
// Returns:
// - A function that takes two Const values and combines their wrapped values
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// ap := MonadAp[string, int, int](S.Monoid)
// c1 := Make[string, func(int) int]("hello")
// c2 := Make[string, int]("world")
// result := ap(c1, c2) // Const containing "helloworld"
func MonadAp[E, A, B any](s S.Semigroup[E]) func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
return func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
return Make[E, B](s.Concat(fab.value, fa.value))
}
}
// Map applies a function to the phantom type parameter without changing the wrapped value.
//
// This is the curried version of MonadMap, providing a more functional programming style.
// The function is never actually called since A is a phantom type.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - f: The function to apply (ignored)
//
// Returns:
// - A function that transforms Const[E, A] to Const[E, B]
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// c := Make[string, int]("data")
// mapped := F.Pipe1(c, Map[string, int, string](strconv.Itoa))
// // mapped still contains "data"
func Map[E, A, B any](f func(A) B) func(fa Const[E, A]) Const[E, B] {
return F.Bind2nd(MonadMap[E, A, B], f)
}
// Ap combines Const values using a semigroup in a curried style.
//
// This is the curried version of MonadAp, providing data-last style for better composition.
// It combines the wrapped values from both Const instances using the provided semigroup.
//
// Type Parameters:
// - E: The type of the wrapped values (must have a semigroup)
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - s: The semigroup for combining wrapped values
//
// Returns:
// - A curried function for combining Const values
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// c1 := Make[string, int]("hello")
// c2 := Make[string, func(int) int]("world")
// result := F.Pipe1(c1, Ap[string, int, int](S.Monoid)(c2))
// // result contains "helloworld"
func Ap[E, A, B any](s S.Semigroup[E]) func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {
monadap := MonadAp[E, A, B](s)
return func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {

View File

@@ -16,25 +16,340 @@
package constant
import (
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestMap(t *testing.T) {
fa := Make[string, int]("foo")
assert.Equal(t, fa, F.Pipe1(fa, Map[string](utils.Double)))
// TestMake tests the Make constructor
func TestMake(t *testing.T) {
t.Run("creates Const with string value", func(t *testing.T) {
c := Make[string, int]("hello")
assert.Equal(t, "hello", Unwrap(c))
})
t.Run("creates Const with int value", func(t *testing.T) {
c := Make[int, string](42)
assert.Equal(t, 42, Unwrap(c))
})
t.Run("creates Const with struct value", func(t *testing.T) {
type Config struct {
Name string
Port int
}
cfg := Config{Name: "server", Port: 8080}
c := Make[Config, bool](cfg)
assert.Equal(t, cfg, Unwrap(c))
})
}
// TestUnwrap tests extracting values from Const
func TestUnwrap(t *testing.T) {
t.Run("unwraps string value", func(t *testing.T) {
c := Make[string, int]("world")
value := Unwrap(c)
assert.Equal(t, "world", value)
})
t.Run("unwraps empty string", func(t *testing.T) {
c := Make[string, int]("")
value := Unwrap(c)
assert.Equal(t, "", value)
})
t.Run("unwraps zero value", func(t *testing.T) {
c := Make[int, string](0)
value := Unwrap(c)
assert.Equal(t, 0, value)
})
}
// TestOf tests the Of function
func TestOf(t *testing.T) {
assert.Equal(t, Make[string, int](""), Of[string, int](S.Monoid)(1))
t.Run("creates Const with monoid empty value", func(t *testing.T) {
of := Of[string, int](S.Monoid)
c := of(42)
assert.Equal(t, "", Unwrap(c))
})
t.Run("ignores input value", func(t *testing.T) {
of := Of[string, int](S.Monoid)
c1 := of(1)
c2 := of(100)
assert.Equal(t, Unwrap(c1), Unwrap(c2))
})
t.Run("works with int monoid", func(t *testing.T) {
of := Of[int, string](N.MonoidSum[int]())
c := of("ignored")
assert.Equal(t, 0, Unwrap(c))
})
}
func TestAp(t *testing.T) {
fab := Make[string, int]("bar")
assert.Equal(t, Make[string, int]("foobar"), Ap[string, int, int](S.Monoid)(fab)(Make[string, func(int) int]("foo")))
// TestMap tests the Map function
func TestMap(t *testing.T) {
t.Run("preserves wrapped value", func(t *testing.T) {
fa := Make[string, int]("foo")
result := F.Pipe1(fa, Map[string](utils.Double))
assert.Equal(t, "foo", Unwrap(result))
})
t.Run("changes phantom type", func(t *testing.T) {
fa := Make[string, int]("data")
fb := Map[string, int, string](strconv.Itoa)(fa)
// Value unchanged, but type changed from Const[string, int] to Const[string, string]
assert.Equal(t, "data", Unwrap(fb))
})
t.Run("function is never called", func(t *testing.T) {
called := false
fa := Make[string, int]("test")
fb := Map[string, int, string](func(i int) string {
called = true
return strconv.Itoa(i)
})(fa)
assert.False(t, called, "Map function should not be called")
assert.Equal(t, "test", Unwrap(fb))
})
}
// TestMonadMap tests the MonadMap function
func TestMonadMap(t *testing.T) {
t.Run("preserves wrapped value", func(t *testing.T) {
fa := Make[string, int]("original")
fb := MonadMap(fa, func(i int) string { return strconv.Itoa(i) })
assert.Equal(t, "original", Unwrap(fb))
})
t.Run("works with different types", func(t *testing.T) {
fa := Make[int, string](42)
fb := MonadMap(fa, func(s string) bool { return len(s) > 0 })
assert.Equal(t, 42, Unwrap(fb))
})
}
// TestAp tests the Ap function
func TestAp(t *testing.T) {
t.Run("combines string values", func(t *testing.T) {
fab := Make[string, int]("bar")
fa := Make[string, func(int) int]("foo")
result := Ap[string, int, int](S.Monoid)(fab)(fa)
assert.Equal(t, "foobar", Unwrap(result))
})
t.Run("combines int values with sum", func(t *testing.T) {
fab := Make[int, string](10)
fa := Make[int, func(string) string](5)
result := Ap[int, string, string](N.SemigroupSum[int]())(fab)(fa)
assert.Equal(t, 15, Unwrap(result))
})
t.Run("combines int values with product", func(t *testing.T) {
fab := Make[int, bool](3)
fa := Make[int, func(bool) bool](4)
result := Ap[int, bool, bool](N.SemigroupProduct[int]())(fab)(fa)
assert.Equal(t, 12, Unwrap(result))
})
}
// TestMonadAp tests the MonadAp function
func TestMonadAp(t *testing.T) {
t.Run("combines values using semigroup", func(t *testing.T) {
ap := MonadAp[string, int, int](S.Monoid)
fab := Make[string, func(int) int]("hello")
fa := Make[string, int]("world")
result := ap(fab, fa)
assert.Equal(t, "helloworld", Unwrap(result))
})
t.Run("works with empty strings", func(t *testing.T) {
ap := MonadAp[string, int, int](S.Monoid)
fab := Make[string, func(int) int]("")
fa := Make[string, int]("test")
result := ap(fab, fa)
assert.Equal(t, "test", Unwrap(result))
})
}
// TestMonoid tests the Monoid function
func TestMonoid(t *testing.T) {
t.Run("always returns constant value", func(t *testing.T) {
m := Monoid(42)
assert.Equal(t, 42, m.Concat(1, 2))
assert.Equal(t, 42, m.Concat(100, 200))
assert.Equal(t, 42, m.Empty())
})
t.Run("works with strings", func(t *testing.T) {
m := Monoid("constant")
assert.Equal(t, "constant", m.Concat("a", "b"))
assert.Equal(t, "constant", m.Empty())
})
t.Run("works with structs", func(t *testing.T) {
type Point struct{ X, Y int }
p := Point{X: 1, Y: 2}
m := Monoid(p)
assert.Equal(t, p, m.Concat(Point{X: 3, Y: 4}, Point{X: 5, Y: 6}))
assert.Equal(t, p, m.Empty())
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := Monoid(10)
// Left identity: Concat(Empty(), x) = x (both return constant)
assert.Equal(t, 10, m.Concat(m.Empty(), 5))
// Right identity: Concat(x, Empty()) = x (both return constant)
assert.Equal(t, 10, m.Concat(5, m.Empty()))
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
left := m.Concat(m.Concat(1, 2), 3)
right := m.Concat(1, m.Concat(2, 3))
assert.Equal(t, left, right)
assert.Equal(t, 10, left)
})
}
// TestConstFunctorLaws tests functor laws for Const
func TestConstFunctorLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// map id = id
fa := Make[string, int]("test")
mapped := Map[string, int, int](F.Identity[int])(fa)
assert.Equal(t, Unwrap(fa), Unwrap(mapped))
})
t.Run("composition law", func(t *testing.T) {
// map (g . f) = map g . map f
fa := Make[string, int]("data")
f := func(i int) string { return strconv.Itoa(i) }
g := func(s string) bool { return len(s) > 0 }
// map (g . f)
composed := Map[string, int, bool](func(i int) bool { return g(f(i)) })(fa)
// map g . map f
intermediate := F.Pipe1(fa, Map[string, int, string](f))
chained := Map[string, string, bool](g)(intermediate)
assert.Equal(t, Unwrap(composed), Unwrap(chained))
})
}
// TestConstApplicativeLaws tests applicative laws for Const
func TestConstApplicativeLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// For Const, ap combines the wrapped values using the semigroup
// ap (of id) v combines empty (from of) with v's value
v := Make[string, int]("value")
ofId := Of[string, func(int) int](S.Monoid)(F.Identity[int])
result := Ap[string, int, int](S.Monoid)(v)(ofId)
// Result combines "" (from Of) with "value" using string monoid
assert.Equal(t, "value", Unwrap(result))
})
t.Run("homomorphism law", func(t *testing.T) {
// ap (of f) (of x) = of (f x)
f := func(i int) string { return strconv.Itoa(i) }
x := 42
ofF := Of[string, func(int) string](S.Monoid)(f)
ofX := Of[string, int](S.Monoid)(x)
left := Ap[string, int, string](S.Monoid)(ofX)(ofF)
right := Of[string, string](S.Monoid)(f(x))
assert.Equal(t, Unwrap(left), Unwrap(right))
})
}
// TestConstEdgeCases tests edge cases
func TestConstEdgeCases(t *testing.T) {
t.Run("empty string values", func(t *testing.T) {
c := Make[string, int]("")
assert.Equal(t, "", Unwrap(c))
mapped := Map[string, int, string](strconv.Itoa)(c)
assert.Equal(t, "", Unwrap(mapped))
})
t.Run("zero values", func(t *testing.T) {
c := Make[int, string](0)
assert.Equal(t, 0, Unwrap(c))
})
t.Run("nil pointer", func(t *testing.T) {
var ptr *int
c := Make[*int, string](ptr)
assert.Nil(t, Unwrap(c))
})
t.Run("multiple map operations", func(t *testing.T) {
c := Make[string, int]("original")
// Chain multiple map operations
step1 := Map[string, int, string](strconv.Itoa)(c)
step2 := Map[string, string, bool](func(s string) bool { return len(s) > 0 })(step1)
result := Map[string, bool, int](func(b bool) int {
if b {
return 1
}
return 0
})(step2)
assert.Equal(t, "original", Unwrap(result))
})
}
// BenchmarkMake benchmarks the Make constructor
func BenchmarkMake(b *testing.B) {
b.ResetTimer()
for b.Loop() {
_ = Make[string, int]("test")
}
}
// BenchmarkUnwrap benchmarks the Unwrap function
func BenchmarkUnwrap(b *testing.B) {
c := Make[string, int]("test")
b.ResetTimer()
for b.Loop() {
_ = Unwrap(c)
}
}
// BenchmarkMap benchmarks the Map function
func BenchmarkMap(b *testing.B) {
c := Make[string, int]("test")
mapFn := Map[string, int, string](strconv.Itoa)
b.ResetTimer()
for b.Loop() {
_ = mapFn(c)
}
}
// BenchmarkAp benchmarks the Ap function
func BenchmarkAp(b *testing.B) {
fab := Make[string, int]("hello")
fa := Make[string, func(int) int]("world")
apFn := Ap[string, int, int](S.Monoid)
b.ResetTimer()
for b.Loop() {
_ = apFn(fab)(fa)
}
}
// BenchmarkMonoid benchmarks the Monoid function
func BenchmarkMonoid(b *testing.B) {
m := Monoid(42)
b.ResetTimer()
for b.Loop() {
_ = m.Concat(1, 2)
}
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package constant
import (
@@ -5,7 +20,47 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid returns a [M.Monoid] that returns a constant value in all operations
// Monoid creates a monoid that always returns a constant value.
//
// This creates a trivial monoid where both the Concat operation and Empty
// always return the same constant value, regardless of inputs. This is useful
// for testing, placeholder implementations, or when you need a monoid instance
// but the actual combining behavior doesn't matter.
//
// # Monoid Laws
//
// The constant monoid satisfies all monoid laws trivially:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always returns 'a'
// - Left Identity: Concat(Empty(), x) = x - both return 'a'
// - Right Identity: Concat(x, Empty()) = x - both return 'a'
//
// Type Parameters:
// - A: The type of the constant value
//
// Parameters:
// - a: The constant value to return in all operations
//
// Returns:
// - A Monoid[A] that always returns the constant value
//
// Example:
//
// // Create a monoid that always returns 42
// m := Monoid(42)
// result := m.Concat(1, 2) // 42
// empty := m.Empty() // 42
//
// // Useful for testing or placeholder implementations
// type Config struct {
// Timeout int
// }
// defaultConfig := Monoid(Config{Timeout: 30})
// config := defaultConfig.Concat(Config{Timeout: 10}, Config{Timeout: 20})
// // config is Config{Timeout: 30}
//
// See also:
// - function.Constant2: The underlying constant function
// - M.MakeMonoid: The monoid constructor
func Monoid[A any](a A) M.Monoid[A] {
return M.MakeMonoid(function.Constant2[A, A](a), a)
}

View File

@@ -83,6 +83,45 @@ var (
RIOE.WithContext[*os.File],
)
// Create creates or truncates a file for writing within the given context.
// If the file already exists, it is truncated. If it doesn't exist, it is created
// with mode 0666 (before umask).
//
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or WriteFile.
//
// Parameters:
// - path: The path to the file to create or truncate
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that creates the file
//
// Example:
//
// createFile := Create("output.txt")
// result := createFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// f.WriteString("Hello, World!")
// },
// )
//
// See Also:
// - WriteFile: For writing data to a file with automatic resource management
// - Open: For opening files for reading
// - Close: For closing file handles
Create = F.Flow3(
IOEF.Create,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name.
// The operation returns the filename on success, allowing for easy composition
// with other file operations.
@@ -191,3 +230,48 @@ func ReadFile(path string) ReaderIOResult[[]byte] {
}
})
}
// WriteFile writes data to a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// If the file doesn't exist, it is created with mode 0666 (before umask).
// If the file already exists, it is truncated before writing.
//
// The operation:
// - Creates or truncates the file for writing
// - Writes all data to the file
// - Automatically closes the file when done
// - Respects context cancellation during the write operation
//
// Parameters:
// - data: The byte slice to write to the file
//
// Returns:
// - Kleisli[string, []byte]: A function that takes a file path and returns a computation
// that writes the data and returns the written bytes on success
//
// Example:
//
// writeOp := WriteFile([]byte("Hello, World!"))
// result := writeOp("output.txt")(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Write error: %v", err) },
// func(data []byte) { log.Printf("Wrote %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// WriteFile(data) = Create >> WriteAll(data) >> Close
//
// See Also:
// - ReadFile: For reading file contents with automatic resource management
// - Create: For creating files without automatic writing
// - WriteAll: For writing to an already-open file handle
func WriteFile(data []byte) Kleisli[string, []byte] {
return F.Flow2(
Create,
WriteAll[*os.File](data),
)
}

View File

@@ -18,11 +18,16 @@ package file
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
R "github.com/IBM/fp-go/v2/context/readerioresult"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
J "github.com/IBM/fp-go/v2/json"
"github.com/stretchr/testify/assert"
)
type RecordType struct {
@@ -49,3 +54,267 @@ func ExampleReadFile() {
// Output:
// Right[string](Carsten)
}
func TestCreate(t *testing.T) {
ctx := context.Background()
t.Run("Success - creates new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_create.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was created
_, err := os.Stat(tempFile)
assert.NoError(t, err)
// Clean up file handle
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
})
t.Run("Success - truncates existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_truncate.txt")
// Create file with initial content
err := os.WriteFile(tempFile, []byte("initial content"), 0644)
assert.NoError(t, err)
// Create should truncate
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Close the file
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
// Verify file was truncated
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
// Try to create file in non-existent directory
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
createOp := Create(invalidPath)
result := createOp(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - file can be written to", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Write to the file
E.MonadFold(result,
func(err error) *os.File { t.Fatalf("Unexpected error: %v", err); return nil },
func(f *os.File) *os.File {
defer f.Close()
_, err := f.WriteString("test content")
assert.NoError(t, err)
return f
},
)
// Verify content was written
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, "test content", string(content))
})
t.Run("Context cancellation", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
createOp := Create(tempFile)
result := createOp(cancelCtx)()
// Note: File creation itself doesn't check context, but this tests the pattern
// In practice, context cancellation would affect subsequent operations
_ = result
})
}
func TestWriteFile(t *testing.T) {
ctx := context.Background()
t.Run("Success - writes data to new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
testData := []byte("Hello, World!")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify returned data
E.MonadFold(result,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("Success - overwrites existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_overwrite.txt")
// Write initial content
err := os.WriteFile(tempFile, []byte("old content"), 0644)
assert.NoError(t, err)
// Overwrite with new content
newData := []byte("new content")
writeOp := WriteFile(newData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was overwritten
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, newData, content)
})
t.Run("Success - writes empty data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_empty.txt")
emptyData := []byte{}
writeOp := WriteFile(emptyData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file is empty
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Success - writes large data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_large.txt")
largeData := make([]byte, 1024*1024) // 1MB
for i := range largeData {
largeData[i] = byte(i % 256)
}
writeOp := WriteFile(largeData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, largeData, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(invalidPath)(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - writes binary data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_binary.bin")
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
writeOp := WriteFile(binaryData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify binary content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, binaryData, content)
})
t.Run("Integration - write then read", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_roundtrip.txt")
testData := []byte("Round trip test data")
// Write data
writeOp := WriteFile(testData)
writeResult := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(writeResult))
// Read data back
readOp := ReadFile(tempFile)
readResult := readOp(ctx)()
assert.True(t, E.IsRight(readResult))
// Verify data matches
E.MonadFold(readResult,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
})
t.Run("Composition with Map", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_compose.txt")
testData := []byte("test data")
// Write and transform result
pipeline := F.Pipe1(
WriteFile(testData)(tempFile),
R.Map(func(data []byte) int { return len(data) }),
)
result := pipeline(ctx)()
assert.True(t, E.IsRight(result))
E.MonadFold(result,
func(err error) int { t.Fatalf("Unexpected error: %v", err); return 0 },
func(length int) int {
assert.Equal(t, len(testData), length)
return length
},
)
})
t.Run("Context cancellation during write", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(cancelCtx)()
// Note: The actual write may complete before cancellation is checked
// This test verifies the pattern works with cancelled contexts
_ = result
})
}

View File

@@ -26,7 +26,7 @@ type TestContext struct {
// runEffect is a helper function to run an effect with a context and return the result
func runEffect[C, A any](eff Effect[C, A], ctx C) (A, error) {
ioResult := Provide[C, A](ctx)(eff)
ioResult := Provide[A](ctx)(eff)
readerResult := RunSync(ioResult)
return readerResult(context.Background())
}

View File

@@ -51,7 +51,7 @@ import (
// )(dbEffect)
//
//go:inline
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
func Local[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
@@ -73,7 +73,7 @@ func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
//
//go:inline
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
func Contramap[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}

View File

@@ -44,11 +44,11 @@ func TestLocal(t *testing.T) {
}
// Apply Local to transform the context
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -70,11 +70,11 @@ func TestLocal(t *testing.T) {
return InnerContext{Value: outer.Value + " transformed"}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "original",
Number: 100,
})(outerEffect)
@@ -93,10 +93,10 @@ func TestLocal(t *testing.T) {
return InnerContext{Value: outer.Value}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -122,12 +122,12 @@ func TestLocal(t *testing.T) {
level3Effect := Of[Level3]("deep result")
// Transform Level2 -> Level3
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
local23 := Local[string](func(l2 Level2) Level3 {
return Level3{C: l2.B + "-c"}
})
// Transform Level1 -> Level2
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
local12 := Local[string](func(l1 Level1) Level2 {
return Level2{B: l1.A + "-b"}
})
@@ -136,7 +136,7 @@ func TestLocal(t *testing.T) {
level1Effect := local12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
ioResult := Provide[string](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -165,11 +165,11 @@ func TestLocal(t *testing.T) {
return app.DB
}
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
kleisli := Local[string](accessor)
appEffect := kleisli(dbEffect)
// Run with full AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
DB: DatabaseConfig{
Host: "localhost",
Port: 5432,
@@ -195,21 +195,21 @@ func TestContramap(t *testing.T) {
}
// Test Local
localKleisli := Local[OuterContext, InnerContext, int](accessor)
localKleisli := Local[int](accessor)
localEffect := localKleisli(innerEffect)
// Test Contramap
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
contramapKleisli := Contramap[int](accessor)
contramapEffect := contramapKleisli(innerEffect)
outerCtx := OuterContext{Value: "test", Number: 100}
// Run both
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
localIO := Provide[int](outerCtx)(localEffect)
localReader := RunSync(localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapIO := Provide[int](outerCtx)(contramapEffect)
contramapReader := RunSync(contramapIO)
contramapResult, contramapErr := contramapReader(context.Background())
@@ -225,10 +225,10 @@ func TestContramap(t *testing.T) {
return InnerContext{Value: outer.Value + " modified"}
}
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
kleisli := Contramap[string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "original",
Number: 50,
})(outerEffect)
@@ -247,10 +247,10 @@ func TestContramap(t *testing.T) {
return InnerContext{Value: outer.Value}
}
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
kleisli := Contramap[int](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, int](OuterContext{
ioResult := Provide[int](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -278,12 +278,12 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
effect3 := Of[Config3]("result")
// Use Local for first transformation
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
local23 := Local[string](func(c2 Config2) Config3 {
return Config3{Info: c2.Data}
})
// Use Contramap for second transformation
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
contramap12 := Contramap[string](func(c1 Config1) Config2 {
return Config2{Data: c1.Value}
})
@@ -292,7 +292,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
effect1 := contramap12(effect2)
// Run
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
ioResult := Provide[string](Config1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -326,7 +326,7 @@ func TestLocalEffectK(t *testing.T) {
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync(ioResult)
@@ -356,7 +356,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
ioResult := Provide[string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -384,7 +384,7 @@ func TestLocalEffectK(t *testing.T) {
transformK := LocalEffectK[string](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
ioResult := Provide[string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -417,7 +417,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ioResult := Provide[string](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync(ioResult)
@@ -456,7 +456,7 @@ func TestLocalEffectK(t *testing.T) {
level1Effect := transform12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
ioResult := Provide[string](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -497,7 +497,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
Environment: "prod",
DBHost: "localhost",
DBPort: 5432,
@@ -534,14 +534,14 @@ func TestLocalEffectK(t *testing.T) {
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
ioResult := Provide[string](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
ioResult2 := Provide[string](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync(ioResult2)
result, err2 := readerResult2(context.Background())
@@ -569,7 +569,7 @@ func TestLocalEffectK(t *testing.T) {
})
// Use Local for second transformation (pure)
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
local12 := Local[string](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
@@ -578,7 +578,7 @@ func TestLocalEffectK(t *testing.T) {
effect1 := local12(effect2)
// Run
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
ioResult := Provide[string](Level1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -610,7 +610,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[int](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
ioResult := Provide[int](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())

View File

@@ -20,6 +20,7 @@ import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
@@ -59,6 +60,11 @@ func FromThunk[C, A any](f Thunk[A]) Effect[C, A] {
return reader.Of[C](f)
}
//go:inline
func FromResult[C, A any](r Result[A]) Effect[C, A] {
return readerreaderioresult.FromEither[C](r)
}
// Succeed creates a successful Effect that produces the given value.
// This is the primary way to lift a pure value into the Effect context.
//
@@ -187,10 +193,126 @@ func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
// return effect.Of[MyContext](strconv.Itoa(x * 2))
// })(eff)
// // chained produces "84"
//
//go:inline
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.Chain(f)
}
//go:inline
func ChainFirst[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirst(f)
}
// ChainIOK chains an effect with a function that returns an IO action.
// This is useful for integrating IO-based computations (synchronous side effects)
// into effect chains. The IO action is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns IO[B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the IO-returning function with the effect
//
// # Example
//
// performIO := func(n int) io.IO[string] {
// return func() string {
// // Perform synchronous side effect
// return fmt.Sprintf("Value: %d", n)
// }
// }
//
// eff := effect.Of[MyContext](42)
// chained := effect.ChainIOK[MyContext](performIO)(eff)
// // chained produces "Value: 42"
//
//go:inline
func ChainIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainIOK[C](f)
}
// ChainFirstIOK chains an effect with a function that returns an IO action,
// but discards the result and returns the original value.
// This is useful for performing side effects (like logging) without changing the value.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the IO action (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns IO[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
//
// # Example
//
// logValue := func(n int) io.IO[any] {
// return func() any {
// fmt.Printf("Processing: %d\n", n)
// return nil
// }
// }
//
// eff := effect.Of[MyContext](42)
// logged := effect.ChainFirstIOK[MyContext](logValue)(eff)
// // Prints "Processing: 42" but still produces 42
//
//go:inline
func ChainFirstIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirstIOK[C](f)
}
// TapIOK is an alias for ChainFirstIOK.
// It chains an effect with a function that returns an IO action for side effects,
// but preserves the original value. This is useful for logging, debugging, or
// performing actions without changing the result.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the IO action (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns IO[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
//
// # Example
//
// logValue := func(n int) io.IO[any] {
// return func() any {
// fmt.Printf("Value: %d\n", n)
// return nil
// }
// }
//
// eff := effect.Of[MyContext](42)
// tapped := effect.TapIOK[MyContext](logValue)(eff)
// // Prints "Value: 42" but still produces 42
//
//go:inline
func TapIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirstIOK[C](f)
}
// Ap applies a function wrapped in an Effect to a value wrapped in an Effect.
// This is the applicative apply operation, useful for applying effects in parallel.
//

View File

@@ -0,0 +1,649 @@
// 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 effect
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestSucceed tests the Succeed function
func TestSucceed_Success(t *testing.T) {
t.Run("creates successful effect with int", func(t *testing.T) {
eff := Succeed[TestConfig](42)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("creates successful effect with string", func(t *testing.T) {
eff := Succeed[TestConfig]("hello")
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("hello"), outcome)
})
t.Run("creates successful effect with zero value", func(t *testing.T) {
eff := Succeed[TestConfig](0)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(0), outcome)
})
}
// TestFail tests the Fail function
func TestFail_Failure(t *testing.T) {
t.Run("creates failed effect with error", func(t *testing.T) {
testErr := errors.New("test error")
eff := Fail[TestConfig, int](testErr)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("preserves error message", func(t *testing.T) {
testErr := errors.New("specific error message")
eff := Fail[TestConfig, string](testErr)
outcome := eff(testConfig)(context.Background())()
assert.True(t, result.IsLeft(outcome))
extractedErr := result.MonadFold(outcome,
F.Identity[error],
func(string) error { return nil },
)
assert.Equal(t, testErr, extractedErr)
})
}
// TestOf tests the Of function
func TestOf_Success(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Of[TestConfig](100)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(100), outcome)
})
t.Run("is equivalent to Succeed", func(t *testing.T) {
value := "test"
eff1 := Of[TestConfig](value)
eff2 := Succeed[TestConfig](value)
outcome1 := eff1(testConfig)(context.Background())()
outcome2 := eff2(testConfig)(context.Background())()
assert.Equal(t, outcome1, outcome2)
})
}
// TestMap tests the Map function
func TestMap_Success(t *testing.T) {
t.Run("transforms success value", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(84), outcome)
})
t.Run("transforms type", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Map[TestConfig](func(x int) string { return strconv.Itoa(x) }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("chains multiple maps", func(t *testing.T) {
eff := F.Pipe2(
Of[TestConfig](10),
Map[TestConfig](func(x int) int { return x + 5 }),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
}
func TestMap_Failure(t *testing.T) {
t.Run("propagates error unchanged", func(t *testing.T) {
testErr := errors.New("test error")
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
// TestChain tests the Chain function
func TestChain_Success(t *testing.T) {
t.Run("sequences two effects", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Chain(func(x int) Effect[TestConfig, string] {
return Of[TestConfig](strconv.Itoa(x))
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("chains multiple effects", func(t *testing.T) {
eff := F.Pipe2(
Of[TestConfig](10),
Chain(func(x int) Effect[TestConfig, int] {
return Of[TestConfig](x + 5)
}),
Chain(func(x int) Effect[TestConfig, int] {
return Of[TestConfig](x * 2)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
}
func TestChain_Failure(t *testing.T) {
t.Run("propagates error from first effect", func(t *testing.T) {
testErr := errors.New("first error")
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Chain(func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("should not execute")
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
})
t.Run("propagates error from second effect", func(t *testing.T) {
testErr := errors.New("second error")
eff := F.Pipe1(
Of[TestConfig](42),
Chain(func(x int) Effect[TestConfig, string] {
return Fail[TestConfig, string](testErr)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
})
}
// TestChainIOK tests the ChainIOK function
func TestChainIOK_Success(t *testing.T) {
t.Run("chains with IO action", func(t *testing.T) {
counter := 0
eff := F.Pipe1(
Of[TestConfig](42),
ChainIOK[TestConfig](func(x int) io.IO[string] {
return func() string {
counter++
return fmt.Sprintf("Value: %d", x)
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 42"), outcome)
assert.Equal(t, 1, counter)
})
t.Run("chains multiple IO actions", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
ChainIOK[TestConfig](func(x int) io.IO[int] {
return func() int {
log = append(log, "first")
return x + 5
}
}),
ChainIOK[TestConfig](func(x int) io.IO[int] {
return func() int {
log = append(log, "second")
return x * 2
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestChainIOK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainIOK[TestConfig](func(x int) io.IO[string] {
return func() string {
executed = true
return "should not execute"
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
assert.False(t, executed)
})
}
// TestChainFirstIOK tests the ChainFirstIOK function
func TestChainFirstIOK_Success(t *testing.T) {
t.Run("executes IO but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, fmt.Sprintf("logged: %d", x))
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"logged: 42"}, log)
})
t.Run("chains multiple side effects", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, "first")
return nil
}
}),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, "second")
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestChainFirstIOK_Failure(t *testing.T) {
t.Run("propagates error without executing IO", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
executed = true
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestTapIOK tests the TapIOK function
func TestTapIOK_Success(t *testing.T) {
t.Run("executes IO but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, fmt.Sprintf("tapped: %d", x))
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tapped: 42"}, log)
})
t.Run("is equivalent to ChainFirstIOK", func(t *testing.T) {
log1 := []string{}
log2 := []string{}
eff1 := F.Pipe1(
Of[TestConfig](10),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log1 = append(log1, "tap")
return nil
}
}),
)
eff2 := F.Pipe1(
Of[TestConfig](10),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log2 = append(log2, "tap")
return nil
}
}),
)
outcome1 := eff1(testConfig)(context.Background())()
outcome2 := eff2(testConfig)(context.Background())()
assert.Equal(t, outcome1, outcome2)
assert.Equal(t, log1, log2)
})
}
func TestTapIOK_Failure(t *testing.T) {
t.Run("propagates error without executing IO", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
executed = true
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestChainResultK tests the ChainResultK function
func TestChainResultK_Success(t *testing.T) {
t.Run("chains with Result-returning function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Of[TestConfig]("42"),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("chains multiple Result operations", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe2(
Of[TestConfig]("10"),
ChainResultK[TestConfig](parseIntResult),
ChainResultK[TestConfig](func(x int) result.Result[string] {
return result.Of(fmt.Sprintf("Value: %d", x*2))
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 20"), outcome)
})
}
func TestChainResultK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := errors.New("test error")
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Fail[TestConfig, string](testErr),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("propagates error from Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Of[TestConfig]("not a number"),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.True(t, result.IsLeft(outcome))
})
}
// TestAp tests the Ap function
func TestAp_Success(t *testing.T) {
t.Run("applies function effect to value effect", func(t *testing.T) {
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
valEff := Of[TestConfig](21)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("applies function with different types", func(t *testing.T) {
fnEff := Of[TestConfig](func(x int) string { return strconv.Itoa(x) })
valEff := Of[TestConfig](42)
eff := Ap[string](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
}
func TestAp_Failure(t *testing.T) {
t.Run("propagates error from function effect", func(t *testing.T) {
testErr := errors.New("function error")
fnEff := Fail[TestConfig, func(int) int](testErr)
valEff := Of[TestConfig](42)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("propagates error from value effect", func(t *testing.T) {
testErr := errors.New("value error")
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
valEff := Fail[TestConfig, int](testErr)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
// TestSuspend tests the Suspend function
func TestSuspend_Success(t *testing.T) {
t.Run("delays evaluation of effect", func(t *testing.T) {
counter := 0
eff := Suspend(func() Effect[TestConfig, int] {
counter++
return Of[TestConfig](42)
})
assert.Equal(t, 0, counter, "should not evaluate immediately")
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, 1, counter, "should evaluate when run")
assert.Equal(t, result.Of(42), outcome)
})
t.Run("enables recursive effects", func(t *testing.T) {
var factorial func(int) Effect[TestConfig, int]
factorial = func(n int) Effect[TestConfig, int] {
if n <= 1 {
return Of[TestConfig](1)
}
return Suspend(func() Effect[TestConfig, int] {
return F.Pipe1(
factorial(n-1),
Map[TestConfig](func(x int) int { return x * n }),
)
})
}
outcome := factorial(5)(testConfig)(context.Background())()
assert.Equal(t, result.Of(120), outcome)
})
}
// TestTap tests the Tap function
func TestTap_Success(t *testing.T) {
t.Run("executes side effect but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, fmt.Sprintf("tapped: %d", x))
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tapped: 42"}, log)
})
t.Run("chains multiple taps", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, "first")
return Of[TestConfig, any](nil)
}),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, "second")
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestTap_Failure(t *testing.T) {
t.Run("propagates error without executing tap", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Tap(func(x int) Effect[TestConfig, any] {
executed = true
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestTernary tests the Ternary function
func TestTernary_Success(t *testing.T) {
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("large")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("small")
},
)
outcome := kleisli(15)(testConfig)(context.Background())()
assert.Equal(t, result.Of("large"), outcome)
})
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("large")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("small")
},
)
outcome := kleisli(5)(testConfig)(context.Background())()
assert.Equal(t, result.Of("small"), outcome)
})
t.Run("works with boundary value", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x >= 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("gte")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("lt")
},
)
outcome := kleisli(10)(testConfig)(context.Background())()
assert.Equal(t, result.Of("gte"), outcome)
})
}
// TestRead tests the Read function
func TestRead_Success(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
eff := Of[TestConfig](42)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("converts effect to thunk", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](10),
Map[TestConfig](func(x int) int { return x * testConfig.Multiplier }),
)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
t.Run("works with different contexts", func(t *testing.T) {
cfg1 := TestConfig{Multiplier: 2, Prefix: "A", DatabaseURL: ""}
cfg2 := TestConfig{Multiplier: 5, Prefix: "B", DatabaseURL: ""}
// Create an effect that uses the context's Multiplier
eff := F.Pipe1(
Of[TestConfig](10),
ChainReaderK(func(x int) reader.Reader[TestConfig, int] {
return func(cfg TestConfig) int {
return x * cfg.Multiplier
}
}),
)
thunk1 := Read[int](cfg1)(eff)
thunk2 := Read[int](cfg2)(eff)
outcome1 := thunk1(context.Background())()
outcome2 := thunk2(context.Background())()
assert.Equal(t, result.Of(20), outcome1)
assert.Equal(t, result.Of(50), outcome2)
})
}
func TestRead_Failure(t *testing.T) {
t.Run("propagates error from effect", func(t *testing.T) {
testErr := errors.New("test error")
eff := Fail[TestConfig, int](testErr)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}

View File

@@ -641,8 +641,8 @@ func TestChainThunkK_Integration(t *testing.T) {
computation := F.Pipe3(
Of[TestConfig](5),
ChainReaderK[TestConfig](addMultiplier),
ChainReaderIOK[TestConfig](logValue),
ChainReaderK(addMultiplier),
ChainReaderIOK(logValue),
ChainThunkK[TestConfig](processThunk),
)
outcome := computation(testConfig)(context.Background())()

View File

@@ -46,7 +46,7 @@ import (
// eff := effect.Of[MyContext](42)
// thunk := effect.Provide[MyContext, int](ctx)(eff)
// // thunk is now a ReaderIOResult[int] that can be run
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
func Provide[A, C any](c C) func(Effect[C, A]) ReaderIOResult[A] {
return readerreaderioresult.Read[A](c)
}

View File

@@ -28,7 +28,7 @@ func TestProvide(t *testing.T) {
ctx := TestContext{Value: "test-value"}
eff := Of[TestContext]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -45,7 +45,7 @@ func TestProvide(t *testing.T) {
cfg := Config{Host: "localhost", Port: 8080}
eff := Of[Config]("connected")
ioResult := Provide[Config, string](cfg)(eff)
ioResult := Provide[string](cfg)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -58,7 +58,7 @@ func TestProvide(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -74,7 +74,7 @@ func TestProvide(t *testing.T) {
ctx := SimpleContext{ID: 42}
eff := Of[SimpleContext](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -89,7 +89,7 @@ func TestProvide(t *testing.T) {
return Of[TestContext]("result")
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -104,7 +104,7 @@ func TestProvide(t *testing.T) {
return "mapped"
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -118,7 +118,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -130,7 +130,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
bgCtx := context.Background()
@@ -145,7 +145,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, int](expectedErr)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -162,7 +162,7 @@ func TestRunSync(t *testing.T) {
return Of[TestContext](x + 10)
})(Of[TestContext](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -174,7 +174,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
// Run multiple times
@@ -200,7 +200,7 @@ func TestRunSync(t *testing.T) {
user := User{Name: "Alice", Age: 30}
eff := Of[TestContext](user)
ioResult := Provide[TestContext, User](ctx)(eff)
ioResult := Provide[User](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -222,7 +222,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Of[AppConfig]("API call successful")
// Provide config and run
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
result, err := RunSync(Provide[string](cfg)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "API call successful", result)
@@ -238,7 +238,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Fail[AppConfig, string](expectedErr)
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
_, err := RunSync(Provide[string](cfg)(eff))(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
@@ -253,7 +253,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return Of[TestContext](x * 2)
})(Of[TestContext](21)))
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
result, err := RunSync(Provide[string](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "final", result)
@@ -281,7 +281,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return State{X: x}
})(Of[TestContext](10)))
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
result, err := RunSync(Provide[State](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
@@ -300,11 +300,11 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
innerEff := Of[InnerCtx]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
transformedEff := Local[string](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
result, err := RunSync(Provide[string](outerCtx)(transformedEff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner result", result)
@@ -318,7 +318,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return Of[TestContext](x * 2)
})(input)
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
result, err := RunSync(Provide[[]int](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)

View File

@@ -379,7 +379,7 @@ func TestMonadChainLeft(t *testing.T) {
func TestChainLeft(t *testing.T) {
t.Run("Curried function transforms Left value", func(t *testing.T) {
// Create a reusable error handler
handleNotFound := ChainLeft[error, string](func(err error) Either[string, int] {
handleNotFound := ChainLeft(func(err error) Either[string, int] {
if err.Error() == "not found" {
return Right[string](0)
}
@@ -391,7 +391,7 @@ func TestChainLeft(t *testing.T) {
})
t.Run("Curried function with Right value", func(t *testing.T) {
handler := ChainLeft[error, string](func(err error) Either[string, int] {
handler := ChainLeft(func(err error) Either[string, int] {
return Left[int]("should not be called")
})
@@ -401,7 +401,7 @@ func TestChainLeft(t *testing.T) {
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
// Create error transformer
toStringError := ChainLeft[int, string](func(code int) Either[string, string] {
toStringError := ChainLeft(func(code int) Either[string, string] {
return Left[string](fmt.Sprintf("Error: %d", code))
})
@@ -414,12 +414,12 @@ func TestChainLeft(t *testing.T) {
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
// First handler: convert error to string
handler1 := ChainLeft[error, string](func(err error) Either[string, int] {
handler1 := ChainLeft(func(err error) Either[string, int] {
return Left[int](err.Error())
})
// Second handler: add prefix to string error
handler2 := ChainLeft[string, string](func(s string) Either[string, int] {
handler2 := ChainLeft(func(s string) Either[string, int] {
return Left[int]("Handled: " + s)
})

52
v2/monoid/types.go Normal file
View File

@@ -0,0 +1,52 @@
// 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 monoid
import "github.com/IBM/fp-go/v2/function"
// Void is an alias for function.Void, representing the unit type.
//
// The Void type (also known as Unit in functional programming) has exactly one value,
// making it useful for representing the absence of meaningful information. It's similar
// to void in other languages, but as a value rather than the absence of a value.
//
// This type alias is provided in the monoid package for convenience when working with
// VoidMonoid and other monoid operations that may use the unit type.
//
// Common use cases:
// - As a return type for functions that perform side effects but don't return meaningful data
// - As a placeholder type parameter when a type is required but no data needs to be passed
// - In monoid operations where you need to track that operations occurred without caring about results
//
// See also:
// - function.Void: The underlying type definition
// - function.VOID: The single inhabitant of the Void type
// - VoidMonoid: A monoid instance for the Void type
//
// Example:
//
// // Function that performs an action but returns no meaningful data
// func logMessage(msg string) Void {
// fmt.Println(msg)
// return function.VOID
// }
//
// // Using Void in monoid operations
// m := VoidMonoid()
// result := m.Concat(function.VOID, function.VOID) // function.VOID
type (
Void = function.Void
)

65
v2/monoid/void.go Normal file
View File

@@ -0,0 +1,65 @@
// 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 monoid
import (
"github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
// VoidMonoid creates a Monoid for the Void (unit) type.
//
// The Void type has exactly one value (function.VOID), making it trivial to define
// a monoid. This monoid uses the Last semigroup, which always returns the second
// argument, though since all Void values are identical, the choice of semigroup
// doesn't affect the result.
//
// This monoid is useful in contexts where:
// - A monoid instance is required but no meaningful data needs to be combined
// - You need to track that an operation occurred without caring about its result
// - Building generic abstractions that work with any monoid, including the trivial case
//
// # Monoid Laws
//
// The VoidMonoid satisfies all monoid laws trivially:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always VOID
// - Left Identity: Concat(Empty(), x) = x - always VOID
// - Right Identity: Concat(x, Empty()) = x - always VOID
//
// Returns:
// - A Monoid[Void] instance
//
// Example:
//
// m := VoidMonoid()
// result := m.Concat(function.VOID, function.VOID) // function.VOID
// empty := m.Empty() // function.VOID
//
// // Useful for tracking operations without data
// type Action = func() Void
// actions := []Action{
// func() Void { fmt.Println("Action 1"); return function.VOID },
// func() Void { fmt.Println("Action 2"); return function.VOID },
// }
// // Execute all actions and combine results
// results := A.Map(func(a Action) Void { return a() })(actions)
// _ = ConcatAll(m)(results) // All actions executed, result is VOID
func VoidMonoid() Monoid[Void] {
return MakeMonoid(
S.Last[Void]().Concat,
function.VOID,
)
}

292
v2/monoid/void_test.go Normal file
View File

@@ -0,0 +1,292 @@
// 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 monoid
import (
"testing"
"github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestVoidMonoid_Basic tests basic VoidMonoid functionality
func TestVoidMonoid_Basic(t *testing.T) {
m := VoidMonoid()
// Test Empty returns VOID
empty := m.Empty()
assert.Equal(t, function.VOID, empty)
// Test Concat returns VOID (since all Void values are identical)
result := m.Concat(function.VOID, function.VOID)
assert.Equal(t, function.VOID, result)
}
// TestVoidMonoid_Laws verifies VoidMonoid satisfies monoid laws
func TestVoidMonoid_Laws(t *testing.T) {
m := VoidMonoid()
// Since Void has only one value, we test with that value
v := function.VOID
// Left Identity: Concat(Empty(), x) = x
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), v)
assert.Equal(t, v, result, "Left identity law failed")
})
// Right Identity: Concat(x, Empty()) = x
t.Run("right identity", func(t *testing.T) {
result := m.Concat(v, m.Empty())
assert.Equal(t, v, result, "Right identity law failed")
})
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
t.Run("associativity", func(t *testing.T) {
left := m.Concat(m.Concat(v, v), v)
right := m.Concat(v, m.Concat(v, v))
assert.Equal(t, left, right, "Associativity law failed")
})
// All results should be VOID
t.Run("all operations return VOID", func(t *testing.T) {
assert.Equal(t, function.VOID, m.Concat(v, v))
assert.Equal(t, function.VOID, m.Empty())
assert.Equal(t, function.VOID, m.Concat(m.Empty(), v))
assert.Equal(t, function.VOID, m.Concat(v, m.Empty()))
})
}
// TestVoidMonoid_ConcatAll tests combining multiple Void values
func TestVoidMonoid_ConcatAll(t *testing.T) {
m := VoidMonoid()
concatAll := ConcatAll(m)
tests := []struct {
name string
input []Void
expected Void
}{
{
name: "empty slice",
input: []Void{},
expected: function.VOID,
},
{
name: "single element",
input: []Void{function.VOID},
expected: function.VOID,
},
{
name: "multiple elements",
input: []Void{function.VOID, function.VOID, function.VOID},
expected: function.VOID,
},
{
name: "many elements",
input: make([]Void, 100),
expected: function.VOID,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize slice with VOID values
for i := range tt.input {
tt.input[i] = function.VOID
}
result := concatAll(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestVoidMonoid_Fold tests the Fold function with VoidMonoid
func TestVoidMonoid_Fold(t *testing.T) {
m := VoidMonoid()
fold := Fold(m)
// Fold should behave identically to ConcatAll
voids := []Void{function.VOID, function.VOID, function.VOID}
result := fold(voids)
assert.Equal(t, function.VOID, result)
// Empty fold
emptyResult := fold([]Void{})
assert.Equal(t, function.VOID, emptyResult)
}
// TestVoidMonoid_Reverse tests that Reverse doesn't affect VoidMonoid
func TestVoidMonoid_Reverse(t *testing.T) {
m := VoidMonoid()
reversed := Reverse(m)
// Since all Void values are identical, reverse should have no effect
v := function.VOID
assert.Equal(t, m.Concat(v, v), reversed.Concat(v, v))
assert.Equal(t, m.Empty(), reversed.Empty())
// Test identity laws still hold
assert.Equal(t, v, reversed.Concat(reversed.Empty(), v))
assert.Equal(t, v, reversed.Concat(v, reversed.Empty()))
}
// TestVoidMonoid_ToSemigroup tests conversion to Semigroup
func TestVoidMonoid_ToSemigroup(t *testing.T) {
m := VoidMonoid()
sg := ToSemigroup(m)
// Should work as a semigroup
result := sg.Concat(function.VOID, function.VOID)
assert.Equal(t, function.VOID, result)
// Verify it's the same underlying operation
assert.Equal(t, m.Concat(function.VOID, function.VOID), sg.Concat(function.VOID, function.VOID))
}
// TestVoidMonoid_FunctionMonoid tests VoidMonoid with FunctionMonoid
func TestVoidMonoid_FunctionMonoid(t *testing.T) {
m := VoidMonoid()
funcMonoid := FunctionMonoid[string](m)
// Create functions that return Void
f1 := func(s string) Void { return function.VOID }
f2 := func(s string) Void { return function.VOID }
// Combine functions
combined := funcMonoid.Concat(f1, f2)
// Test combined function
result := combined("test")
assert.Equal(t, function.VOID, result)
// Test empty function
emptyFunc := funcMonoid.Empty()
assert.Equal(t, function.VOID, emptyFunc("anything"))
}
// TestVoidMonoid_PracticalUsage demonstrates practical usage patterns
func TestVoidMonoid_PracticalUsage(t *testing.T) {
m := VoidMonoid()
// Simulate tracking that operations occurred without caring about results
type Action func() Void
actions := []Action{
func() Void { return function.VOID }, // Action 1
func() Void { return function.VOID }, // Action 2
func() Void { return function.VOID }, // Action 3
}
// Execute all actions and collect results
results := make([]Void, len(actions))
for i, action := range actions {
results[i] = action()
}
// Combine all results (all are VOID)
finalResult := ConcatAll(m)(results)
assert.Equal(t, function.VOID, finalResult)
}
// TestVoidMonoid_EdgeCases tests edge cases
func TestVoidMonoid_EdgeCases(t *testing.T) {
m := VoidMonoid()
t.Run("multiple concatenations", func(t *testing.T) {
// Chain multiple Concat operations
result := m.Concat(
m.Concat(
m.Concat(function.VOID, function.VOID),
function.VOID,
),
function.VOID,
)
assert.Equal(t, function.VOID, result)
})
t.Run("concat with empty", func(t *testing.T) {
// Various combinations with Empty()
assert.Equal(t, function.VOID, m.Concat(m.Empty(), m.Empty()))
assert.Equal(t, function.VOID, m.Concat(m.Concat(m.Empty(), function.VOID), m.Empty()))
})
t.Run("large slice", func(t *testing.T) {
// Test with a large number of elements
largeSlice := make([]Void, 10000)
for i := range largeSlice {
largeSlice[i] = function.VOID
}
result := ConcatAll(m)(largeSlice)
assert.Equal(t, function.VOID, result)
})
}
// TestVoidMonoid_TypeSafety verifies type safety
func TestVoidMonoid_TypeSafety(t *testing.T) {
m := VoidMonoid()
// Verify it implements Monoid interface
var _ Monoid[Void] = m
// Verify Empty returns correct type
empty := m.Empty()
var _ Void = empty
// Verify Concat returns correct type
result := m.Concat(function.VOID, function.VOID)
var _ Void = result
}
// BenchmarkVoidMonoid_Concat benchmarks the Concat operation
func BenchmarkVoidMonoid_Concat(b *testing.B) {
m := VoidMonoid()
v := function.VOID
b.ResetTimer()
for b.Loop() {
_ = m.Concat(v, v)
}
}
// BenchmarkVoidMonoid_ConcatAll benchmarks combining multiple Void values
func BenchmarkVoidMonoid_ConcatAll(b *testing.B) {
m := VoidMonoid()
concatAll := ConcatAll(m)
voids := make([]Void, 1000)
for i := range voids {
voids[i] = function.VOID
}
b.ResetTimer()
for b.Loop() {
_ = concatAll(voids)
}
}
// BenchmarkVoidMonoid_Empty benchmarks the Empty operation
func BenchmarkVoidMonoid_Empty(b *testing.B) {
m := VoidMonoid()
b.ResetTimer()
for b.Loop() {
_ = m.Empty()
}
}
// Made with Bob

View File

@@ -62,7 +62,7 @@ func TestMonadAltBasicFunctionality(t *testing.T) {
assert.True(t, either.IsRight(result), "should successfully decode with first codec")
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "HELLO", value)
})
@@ -105,7 +105,7 @@ func TestMonadAltBasicFunctionality(t *testing.T) {
assert.True(t, either.IsRight(result), "should successfully decode with second codec")
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, -5, value)
})
@@ -302,19 +302,19 @@ func TestAltOperator(t *testing.T) {
// Test with "42" - should use base codec
result1 := pipeline.Decode("42")
assert.True(t, either.IsRight(result1))
value1 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result1)
value1 := either.GetOrElse(reader.Of[validation.Errors](0))(result1)
assert.Equal(t, 42, value1)
// Test with "100" - should use fallback1
result2 := pipeline.Decode("100")
assert.True(t, either.IsRight(result2))
value2 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result2)
value2 := either.GetOrElse(reader.Of[validation.Errors](0))(result2)
assert.Equal(t, 100, value2)
// Test with "999" - should use fallback2
result3 := pipeline.Decode("999")
assert.True(t, either.IsRight(result3))
value3 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result3)
value3 := either.GetOrElse(reader.Of[validation.Errors](0))(result3)
assert.Equal(t, 999, value3)
})
}
@@ -449,7 +449,7 @@ func TestAltRoundTrip(t *testing.T) {
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
decoded := either.GetOrElse(reader.Of[validation.Errors](""))(decodeResult)
// Encode
encoded := altCodec.Encode(decoded)
@@ -487,7 +487,7 @@ func TestAltRoundTrip(t *testing.T) {
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
decoded := either.GetOrElse(reader.Of[validation.Errors](""))(decodeResult)
// Encode (uses first codec's encoder, which is identity)
encoded := altCodec.Encode(decoded)
@@ -619,7 +619,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, 10, value, "first success should win")
})
@@ -628,7 +628,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("42")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, 42, value)
})
@@ -637,7 +637,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("invalid")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
assert.Equal(t, 0, value, "should use default zero value")
})
})
@@ -768,21 +768,21 @@ func TestAltMonoid(t *testing.T) {
t.Run("uses primary when it succeeds", func(t *testing.T) {
result := combined.Decode("primary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "from primary", value)
})
t.Run("uses secondary when primary fails", func(t *testing.T) {
result := combined.Decode("secondary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "from secondary", value)
})
t.Run("uses default when both fail", func(t *testing.T) {
result := combined.Decode("other")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "default", value)
})
})
@@ -841,7 +841,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
// Empty (0) comes first, so it wins
assert.Equal(t, 0, value)
})
@@ -852,7 +852,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
assert.Equal(t, 10, value, "codec1 should win")
})
@@ -867,8 +867,8 @@ func TestAltMonoid(t *testing.T) {
assert.True(t, either.IsRight(resultLeft))
assert.True(t, either.IsRight(resultRight))
valueLeft := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultRight)
valueLeft := either.GetOrElse(reader.Of[validation.Errors](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors](-1))(resultRight)
// Both should return 10 (first success)
assert.Equal(t, valueLeft, valueRight)

299
v2/optics/codec/bind.go Normal file
View File

@@ -0,0 +1,299 @@
// 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 codec
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/semigroup"
)
// ApSL creates an applicative sequencing operator for codecs using a lens.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs,
// allowing you to build up complex codecs by combining a base codec with a field
// accessed through a lens. It's particularly useful for building struct codecs
// field-by-field in a composable way.
//
// The function combines:
// - Encoding: Extracts the field value using the lens, encodes it with fa, and
// combines it with the base encoding using the monoid
// - Validation: Validates the field using the lens and combines the validation
// with the base validation
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The field type accessed by the lens
// - O: The output type for encoding (must have a monoid)
// - I: The input type for decoding
//
// # Parameters
//
// - m: A Monoid[O] for combining encoded outputs
// - l: A Lens[S, T] that focuses on a specific field in S
// - fa: A Type[T, O, I] codec for the field type T
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the field
// specified by the lens.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Extract the field T using l.Get
// - Encode T to O using fa.Encode
// - Combine with the base encoding using the monoid
//
// 2. **Validation**: When validating input I:
// - Validate the field using fa.Validate through the lens
// - Combine with the base validation
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/lens"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Age int
// }
//
// // Lenses for Person fields
// nameLens := lens.MakeLens(
// func(p *Person) string { return p.Name },
// func(p *Person, name string) *Person { p.Name = name; return p },
// )
//
// // Build a Person codec field by field
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSL(S.Monoid, nameLens, codec.String),
// // ... add more fields
// )
//
// # Use Cases
//
// - Building struct codecs incrementally
// - Composing codecs for nested structures
// - Creating type-safe serialization/deserialization
// - Implementing Do-notation style codec construction
//
// # Notes
//
// - The monoid determines how encoded outputs are combined
// - The lens must be total (handle all cases safely)
// - This is typically used with other ApS functions to build complete codecs
// - The name is automatically generated for debugging purposes
//
// See also:
// - validate.ApSL: The underlying validation combinator
// - reader.ApplicativeMonoid: The monoid-based applicative instance
// - Lens: The optic for accessing struct fields
func ApSL[S, T, O, I any](
m Monoid[O],
l Lens[S, T],
fa Type[T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("ApS[%s x %s]", l, fa)
rm := reader.ApplicativeMonoid[S](m)
encConcat := F.Pipe1(
F.Flow2(
l.Get,
fa.Encode,
),
semigroup.AppendTo(rm),
)
valConcat := validate.ApSL(l, fa.Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
valConcat,
),
encConcat(t.Encode),
)
}
}
// ApSO creates an applicative sequencing operator for codecs using an optional.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs
// with optional fields, allowing you to build up complex codecs by combining a base
// codec with a field that may or may not be present. It's particularly useful for
// building struct codecs with optional fields in a composable way.
//
// The function combines:
// - Encoding: Attempts to extract the optional field value, encodes it if present,
// and combines it with the base encoding using the monoid. If the field is absent,
// only the base encoding is used.
// - Validation: Validates the optional field and combines the validation with the
// base validation using applicative semantics (error accumulation).
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The optional field type accessed by the optional
// - O: The output type for encoding (must have a monoid)
// - I: The input type for decoding
//
// # Parameters
//
// - m: A Monoid[O] for combining encoded outputs
// - o: An Optional[S, T] that focuses on a field in S that may not exist
// - fa: A Type[T, O, I] codec for the optional field type T
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the optional field
// specified by the optional.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Try to extract the optional field T using o.GetOption
// - If present (Some(T)): Encode T to O using fa.Encode and combine with base using monoid
// - If absent (None): Return only the base encoding unchanged
//
// 2. **Validation**: When validating input I:
// - Validate the optional field using fa.Validate through o.Set
// - Combine with the base validation using applicative semantics
// - Accumulates all validation errors from both base and field
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Difference from ApSL
//
// Unlike ApSL which works with required fields via Lens, ApSO handles optional fields:
// - ApSL: Field always exists, always encoded
// - ApSO: Field may not exist, only encoded when present
// - ApSO uses Optional.GetOption which returns Option[T]
// - ApSO gracefully handles missing fields without errors
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/optional"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Nickname *string // Optional field
// }
//
// // Optional for Person.Nickname
// nicknameOpt := optional.MakeOptional(
// func(p Person) option.Option[string] {
// if p.Nickname != nil {
// return option.Some(*p.Nickname)
// }
// return option.None[string]()
// },
// func(p Person, nick string) Person {
// p.Nickname = &nick
// return p
// },
// )
//
// // Build a Person codec with optional nickname
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
// )
//
// // Encoding with nickname present
// p1 := Person{Name: "Alice", Nickname: ptr("Ali")}
// encoded1 := personCodec.Encode(p1) // Includes nickname
//
// // Encoding with nickname absent
// p2 := Person{Name: "Bob", Nickname: nil}
// encoded2 := personCodec.Encode(p2) // No nickname in output
//
// # Use Cases
//
// - Building struct codecs with optional/nullable fields
// - Handling pointer fields that may be nil
// - Composing codecs for structures with optional nested data
// - Creating flexible serialization that omits absent fields
//
// # Notes
//
// - The monoid determines how encoded outputs are combined when field is present
// - When the optional field is absent, encoding returns base encoding unchanged
// - Validation still accumulates errors even for optional fields
// - The name is automatically generated for debugging purposes
//
// # See Also
//
// - ApSL: For required fields using Lens
// - validate.ApS: The underlying validation combinator
// - Optional: The optic for accessing optional fields
func ApSO[S, T, O, I any](
m Monoid[O],
o Optional[S, T],
fa Type[T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("ApS[%s x %s]", o, fa)
encConcat := F.Flow2(
o.GetOption,
option.Map(F.Flow2(
fa.Encode,
semigroup.AppendTo(m),
)),
)
valConcat := validate.ApS(o.Set, fa.Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
valConcat,
),
func(s S) O {
to := t.Encode(s)
return F.Pipe2(
encConcat(s),
option.Flap[O](to),
option.GetOrElse(lazy.Of(to)),
)
},
)
}
}

View File

@@ -0,0 +1,818 @@
// 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 codec
import (
"strconv"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// Test types for ApSL
type Person struct {
Name string
Age int
}
func TestApSL_EncodingCombination(t *testing.T) {
t.Run("combines encodings using monoid", func(t *testing.T) {
// Create a lens for Person.Name
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Create base codec that encodes to "Person:"
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected Person",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "Person:" },
)
// Create field codec for Name
nameCodec := MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSL to combine encodings
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding - should concatenate base encoding with field encoding
person := Person{Name: "Alice", Age: 30}
encoded := enhancedCodec.Encode(person)
// The monoid concatenates: base encoding + field encoding
// Note: The order depends on how the monoid is applied in ApSL
assert.Contains(t, encoded, "Person:")
assert.Contains(t, encoded, "Alice")
})
}
func TestApSL_ValidationCombination(t *testing.T) {
t.Run("validates field through lens", func(t *testing.T) {
// Create a lens for Person.Age
ageLens := lens.MakeLens(
func(p Person) int { return p.Age },
func(p Person, age int) Person {
return Person{Name: p.Name, Age: age}
},
)
// Create base codec that always succeeds
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected Person",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec for Age that validates positive numbers
ageCodec := MakeType(
"Age",
func(i any) validation.Result[int] {
if n, ok := i.(int); ok {
if n > 0 {
return validation.ToResult(validation.Success(n))
}
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: n,
Messsage: "age must be positive",
},
}))
}
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected int",
},
}))
},
func(i any) Decode[Context, int] {
return func(ctx Context) validation.Validation[int] {
if n, ok := i.(int); ok {
if n > 0 {
return validation.Success(n)
}
return validation.FailureWithMessage[int](n, "age must be positive")(ctx)
}
return validation.FailureWithMessage[int](i, "expected int")(ctx)
}
},
strconv.Itoa,
)
// Apply ApSL
operator := ApSL(S.Monoid, ageLens, ageCodec)
enhancedCodec := operator(baseCodec)
// Test with invalid age (negative) - field validation should fail
invalidPerson := Person{Name: "Charlie", Age: -5}
invalidResult := enhancedCodec.Decode(invalidPerson)
assert.True(t, either.IsLeft(invalidResult), "Should fail with negative age")
// Extract and verify we have errors
errors := either.MonadFold(invalidResult,
F.Identity[validation.Errors],
func(Person) validation.Errors { return nil },
)
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
func TestApSL_TypeChecking(t *testing.T) {
t.Run("preserves base type checker", func(t *testing.T) {
// Create a lens for Person.Name
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Create base codec with type checker
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected Person",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec
nameCodec := MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSL
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Test type checking with valid type
person := Person{Name: "Eve", Age: 22}
isResult := enhancedCodec.Is(person)
assert.True(t, either.IsRight(isResult), "Should accept Person type")
// Test type checking with invalid type
invalidResult := enhancedCodec.Is("not a person")
assert.True(t, either.IsLeft(invalidResult), "Should reject non-Person type")
})
}
func TestApSL_Naming(t *testing.T) {
t.Run("generates descriptive name", func(t *testing.T) {
// Create a lens for Person.Name
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Create base codec
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected Person",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec
nameCodec := MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSL
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Check that the name includes ApS
name := enhancedCodec.Name()
assert.Contains(t, name, "ApS", "Name should contain 'ApS'")
})
}
func TestApSL_ErrorAccumulation(t *testing.T) {
t.Run("accumulates validation errors", func(t *testing.T) {
// Create a lens for Person.Age
ageLens := lens.MakeLens(
func(p Person) int { return p.Age },
func(p Person, age int) Person {
return Person{Name: p.Name, Age: age}
},
)
// Create base codec that fails validation
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "base validation error",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
return validation.FailureWithMessage[Person](i, "base validation error")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec that also fails
ageCodec := MakeType(
"Age",
func(i any) validation.Result[int] {
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "age validation error",
},
}))
},
func(i any) Decode[Context, int] {
return func(ctx Context) validation.Validation[int] {
return validation.FailureWithMessage[int](i, "age validation error")(ctx)
}
},
strconv.Itoa,
)
// Apply ApSL
operator := ApSL(S.Monoid, ageLens, ageCodec)
enhancedCodec := operator(baseCodec)
// Test validation - should accumulate errors
person := Person{Name: "Dave", Age: 30}
result := enhancedCodec.Decode(person)
// Should fail
assert.True(t, either.IsLeft(result), "Should fail validation")
// Extract errors
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(Person) validation.Errors { return nil },
)
// Should have errors from both base and field validation
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
// Test types for ApSO
type PersonWithNickname struct {
Name string
Nickname *string
}
func TestApSO_EncodingWithPresentField(t *testing.T) {
t.Run("encodes optional field when present", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec that encodes to "Person:"
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "Person:" },
)
// Create field codec for Nickname
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO to combine encodings
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding with nickname present
nickname := "Ali"
person := PersonWithNickname{Name: "Alice", Nickname: &nickname}
encoded := enhancedCodec.Encode(person)
// Should include both base and nickname
assert.Contains(t, encoded, "Person:")
assert.Contains(t, encoded, "Ali")
})
}
func TestApSO_EncodingWithAbsentField(t *testing.T) {
t.Run("omits optional field when absent", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "Person:Bob" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding with nickname absent
person := PersonWithNickname{Name: "Bob", Nickname: nil}
encoded := enhancedCodec.Encode(person)
// Should only have base encoding
assert.Equal(t, "Person:Bob", encoded)
})
}
func TestApSO_TypeChecking(t *testing.T) {
t.Run("preserves base type checker", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec with type checker
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test type checking with valid type
nickname := "Eve"
person := PersonWithNickname{Name: "Eve", Nickname: &nickname}
isResult := enhancedCodec.Is(person)
assert.True(t, either.IsRight(isResult), "Should accept PersonWithNickname type")
// Test type checking with invalid type
invalidResult := enhancedCodec.Is("not a person")
assert.True(t, either.IsLeft(invalidResult), "Should reject non-PersonWithNickname type")
})
}
func TestApSO_Naming(t *testing.T) {
t.Run("generates descriptive name", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Check that the name includes ApS
name := enhancedCodec.Name()
assert.Contains(t, name, "ApS", "Name should contain 'ApS'")
})
}
func TestApSO_ErrorAccumulation(t *testing.T) {
t.Run("accumulates validation errors", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec that fails validation
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "base validation error",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
return validation.FailureWithMessage[PersonWithNickname](i, "base validation error")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec that also fails
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "nickname validation error",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
return validation.FailureWithMessage[string](i, "nickname validation error")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test validation with present nickname - should accumulate errors
nickname := "Dave"
person := PersonWithNickname{Name: "Dave", Nickname: &nickname}
result := enhancedCodec.Decode(person)
// Should fail
assert.True(t, either.IsLeft(result), "Should fail validation")
// Extract errors
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(PersonWithNickname) validation.Errors { return nil },
)
// Should have errors from both base and field validation
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
// Made with Bob

View File

@@ -100,7 +100,7 @@ func (t *typeImpl[A, O, I]) Is(i any) Result[A] {
// stringToInt := codec.MakeType(...) // Type[int, string, string]
// intToPositive := codec.MakeType(...) // Type[PositiveInt, int, int]
// composed := codec.Pipe(intToPositive)(stringToInt) // Type[PositiveInt, string, string]
func Pipe[A, B, O, I any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
func Pipe[O, I, A, B any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
return func(this Type[A, O, I]) Type[B, O, I] {
return MakeType(
fmt.Sprintf("Pipe(%s, %s)", this.Name(), ab.Name()),

View File

@@ -1748,7 +1748,7 @@ func TestFromRefinementComposition(t *testing.T) {
positiveCodec := FromRefinement(positiveIntPrism)
// Compose with Int codec using Pipe
composed := Pipe[int, int, int, any](positiveCodec)(Int())
composed := Pipe[int, any, int, int](positiveCodec)(Int())
t.Run("ComposedDecodeValid", func(t *testing.T) {
result := composed.Decode(42)

View File

@@ -159,7 +159,7 @@ func TestURL(t *testing.T) {
func TestDate(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, time.Time](time.Time{}))
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](time.Time{}))
t.Run("ISO 8601 date format", func(t *testing.T) {
dateCodec := Date("2006-01-02")

View File

@@ -70,7 +70,7 @@ func TestEitherEncode(t *testing.T) {
// TestEitherDecode tests decoding/validation of Either values
func TestEitherDecode(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, either.Either[string, int]](either.Left[int]("")))
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](either.Left[int]("")))
// Create codecs that both work with string input
stringCodec := Id[string]()

View File

@@ -10,6 +10,8 @@ import (
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/decoder"
"github.com/IBM/fp-go/v2/optics/encoder"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
@@ -338,4 +340,104 @@ type (
// - ApplicativeMonoid: Combines successful results using inner monoid
// - AlternativeMonoid: Combines applicative and alternative behaviors
Monoid[A any] = monoid.Monoid[A]
// Lens is an optic that focuses on a specific field within a product type S.
// It provides a way to get and set a field of type A within a structure of type S.
//
// A Lens[S, A] represents a relationship between a source type S and a focus type A,
// where the focus always exists (unlike Optional which may not exist).
//
// Lens operations:
// - Get: Extract the field value A from structure S
// - Set: Update the field value A in structure S, returning a new S
//
// Lens laws:
// 1. GetSet: If you get a value and then set it back, nothing changes
// Set(Get(s))(s) = s
// 2. SetGet: If you set a value, you can get it back
// Get(Set(a)(s)) = a
// 3. SetSet: Setting twice is the same as setting once with the final value
// Set(b)(Set(a)(s)) = Set(b)(s)
//
// In the codec context, lenses are used with ApSL to build codecs for struct fields:
// - Extract field values for encoding
// - Update field values during validation
// - Compose codec operations on nested structures
//
// Example:
// type Person struct { Name string; Age int }
//
// nameLens := lens.MakeLens(
// func(p Person) string { return p.Name },
// func(p Person, name string) Person { p.Name = name; return p },
// )
//
// // Use with ApSL to build a codec
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSL(S.Monoid, nameLens, codec.String),
// )
//
// See also:
// - ApSL: Applicative sequencing with lens
// - Optional: For fields that may not exist
Lens[S, A any] = lens.Lens[S, A]
// Optional is an optic that focuses on a field within a product type S that may not exist.
// It provides a way to get and set an optional field of type A within a structure of type S.
//
// An Optional[S, A] represents a relationship between a source type S and a focus type A,
// where the focus may or may not be present (unlike Lens where it always exists).
//
// Optional operations:
// - GetOption: Try to extract the field value, returning Option[A]
// - Set: Update the field value if it exists, returning a new S
//
// Optional laws:
// 1. GetSet (No-op on None): If GetOption returns None, Set has no effect
// GetOption(s) = None => Set(a)(s) = s
// 2. SetGet (Get what you Set): If GetOption returns Some, you can get back what you set
// GetOption(s) = Some(_) => GetOption(Set(a)(s)) = Some(a)
// 3. SetSet (Last Set Wins): Setting twice is the same as setting once with the final value
// Set(b)(Set(a)(s)) = Set(b)(s)
//
// In the codec context, optionals are used with ApSO to build codecs for optional fields:
// - Extract optional field values for encoding (only if present)
// - Update optional field values during validation
// - Handle nullable or pointer fields gracefully
// - Compose codec operations on structures with optional data
//
// Example:
// type Person struct {
// Name string
// Nickname *string // Optional field
// }
//
// nicknameOpt := optional.MakeOptional(
// func(p Person) option.Option[string] {
// if p.Nickname != nil {
// return option.Some(*p.Nickname)
// }
// return option.None[string]()
// },
// func(p Person, nick string) Person {
// p.Nickname = &nick
// return p
// },
// )
//
// // Use with ApSO to build a codec with optional field
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
// )
//
// // Encoding omits the field when absent
// p1 := Person{Name: "Alice", Nickname: nil}
// encoded := personCodec.Encode(p1) // No nickname in output
//
// See also:
// - ApSO: Applicative sequencing with optional
// - Lens: For fields that always exist
Optional[S, A any] = optional.Optional[S, A]
)

View File

@@ -39,7 +39,7 @@ func TestFromReaderResult_Success(t *testing.T) {
}
// Convert to Validate
validator := FromReaderResult[int, string](successRR)
validator := FromReaderResult(successRR)
// Execute the validator
validationResult := validator(42)(nil)
@@ -53,7 +53,7 @@ func TestFromReaderResult_Success(t *testing.T) {
parseIntRR := result.Eitherize1(strconv.Atoi)
// Convert to Validate
validator := FromReaderResult[string, int](parseIntRR)
validator := FromReaderResult(parseIntRR)
// Execute with valid input
validationResult := validator("123")(nil)
@@ -74,7 +74,7 @@ func TestFromReaderResult_Success(t *testing.T) {
}
// Convert to Validate
validator := FromReaderResult[string, User](createUserRR)
validator := FromReaderResult(createUserRR)
// Execute the validator
validationResult := validator("Alice")(nil)
@@ -88,7 +88,7 @@ func TestFromReaderResult_Success(t *testing.T) {
return result.Of(input * 2)
}
validator := FromReaderResult[int, int](successRR)
validator := FromReaderResult(successRR)
validationResult := validator(21)(Context{})
assert.Equal(t, validation.Success(42), validationResult)
@@ -99,7 +99,7 @@ func TestFromReaderResult_Success(t *testing.T) {
return result.Of(input + " processed")
}
validator := FromReaderResult[string, string](successRR)
validator := FromReaderResult(successRR)
ctx := Context{
{Key: "user", Type: "User"},
{Key: "name", Type: "string"},
@@ -122,7 +122,7 @@ func TestFromReaderResult_Failure(t *testing.T) {
}
// Convert to Validate
validator := FromReaderResult[string, int](failureRR)
validator := FromReaderResult(failureRR)
// Execute the validator
validationResult := validator("invalid")(nil)
@@ -147,7 +147,7 @@ func TestFromReaderResult_Failure(t *testing.T) {
return result.Left[string](originalErr)
}
validator := FromReaderResult[int, string](failureRR)
validator := FromReaderResult(failureRR)
validationResult := validator(42)(nil)
assert.True(t, either.IsLeft(validationResult))
@@ -166,7 +166,7 @@ func TestFromReaderResult_Failure(t *testing.T) {
return result.Left[int](errors.New("conversion failed"))
}
validator := FromReaderResult[string, int](failureRR)
validator := FromReaderResult(failureRR)
ctx := Context{
{Key: "user", Type: "User"},
{Key: "age", Type: "int"},
@@ -213,7 +213,7 @@ func TestFromReaderResult_Failure(t *testing.T) {
return result.Left[int](tc.err)
}
validator := FromReaderResult[string, int](failureRR)
validator := FromReaderResult(failureRR)
validationResult := validator(tc.input)(nil)
assert.True(t, either.IsLeft(validationResult))
@@ -251,7 +251,7 @@ func TestFromReaderResult_Integration(t *testing.T) {
// Combine validators
validator := F.Pipe1(
FromReaderResult[string, int](parseIntRR),
FromReaderResult(parseIntRR),
Chain(validatePositive),
)
@@ -273,8 +273,8 @@ func TestFromReaderResult_Integration(t *testing.T) {
// Convert and map to double the value
validator := F.Pipe1(
FromReaderResult[string, int](parseIntRR),
Map[string, int, int](func(n int) int { return n * 2 }),
FromReaderResult(parseIntRR),
Map[string](func(n int) int { return n * 2 }),
)
validationResult := validator("21")(nil)
@@ -294,7 +294,7 @@ func TestFromReaderResult_Integration(t *testing.T) {
Bind(func(p int) func(State) State {
return func(s State) State { s.parsed = p; return s }
}, func(s State) Validate[string, int] {
return FromReaderResult[string, int](parseIntRR)
return FromReaderResult(parseIntRR)
}),
Let[string](func(v bool) func(State) State {
return func(s State) State { s.valid = v; return s }
@@ -315,7 +315,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
return result.Of(input)
}
validator := FromReaderResult[int, int](successRR)
validator := FromReaderResult(successRR)
validationResult := validator(42)(nil)
assert.True(t, either.IsRight(validationResult))
@@ -326,7 +326,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
return result.Of(input)
}
validator := FromReaderResult[string, string](identityRR)
validator := FromReaderResult(identityRR)
validationResult := validator("")(nil)
assert.Equal(t, validation.Success(""), validationResult)
@@ -337,7 +337,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
return result.Of(input)
}
validator := FromReaderResult[int, int](identityRR)
validator := FromReaderResult(identityRR)
validationResult := validator(0)(nil)
assert.Equal(t, validation.Success(0), validationResult)
@@ -352,7 +352,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
return result.Of(&Data{Value: input})
}
validator := FromReaderResult[int, *Data](createDataRR)
validator := FromReaderResult(createDataRR)
validationResult := validator(42)(nil)
assert.True(t, either.IsRight(validationResult))
@@ -372,7 +372,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
return result.Of([]string{input, input})
}
validator := FromReaderResult[string, []string](splitRR)
validator := FromReaderResult(splitRR)
validationResult := validator("test")(nil)
assert.Equal(t, validation.Success([]string{"test", "test"}), validationResult)
@@ -383,7 +383,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
return result.Of(map[string]int{input: len(input)})
}
validator := FromReaderResult[string, map[string]int](createMapRR)
validator := FromReaderResult(createMapRR)
validationResult := validator("hello")(nil)
assert.Equal(t, validation.Success(map[string]int{"hello": 5}), validationResult)
@@ -398,7 +398,7 @@ func TestFromReaderResult_TypeSafety(t *testing.T) {
return result.Of(fmt.Sprintf("%d", input))
}
validator := FromReaderResult[int, string](intToStringRR)
validator := FromReaderResult(intToStringRR)
// This should compile and work correctly
validationResult := validator(42)(nil)
@@ -409,7 +409,7 @@ func TestFromReaderResult_TypeSafety(t *testing.T) {
// This test verifies that the output type is preserved
stringToIntRR := result.Eitherize1(strconv.Atoi)
validator := FromReaderResult[string, int](stringToIntRR)
validator := FromReaderResult(stringToIntRR)
validationResult := validator("42")(nil)
// The result should be Validation[int]
@@ -428,7 +428,7 @@ func TestFromReaderResult_TypeSafety(t *testing.T) {
return Output{Result: val}, nil
})
validator := FromReaderResult[Input, Output](transformRR)
validator := FromReaderResult(transformRR)
validationResult := validator(Input{Value: "42"})(nil)
assert.Equal(t, validation.Success(Output{Result: 42}), validationResult)
@@ -441,7 +441,7 @@ func BenchmarkFromReaderResult_Success(b *testing.B) {
return result.Of(input * 2)
}
validator := FromReaderResult[int, int](successRR)
validator := FromReaderResult(successRR)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -455,7 +455,7 @@ func BenchmarkFromReaderResult_Failure(b *testing.B) {
return result.Left[int](errors.New("error"))
}
validator := FromReaderResult[int, int](failureRR)
validator := FromReaderResult(failureRR)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -469,7 +469,7 @@ func BenchmarkFromReaderResult_WithContext(b *testing.B) {
return result.Of(input * 2)
}
validator := FromReaderResult[int, int](successRR)
validator := FromReaderResult(successRR)
ctx := Context{
{Key: "user", Type: "User"},
{Key: "age", Type: "int"},

View File

@@ -26,7 +26,7 @@ func TestMonadChainLeft(t *testing.T) {
handler := func(errs Errors) Validate[string, int] {
for _, err := range errs {
if err.Messsage == "validation failed" {
return Of[string, int](0) // recover with default
return Of[string](0) // recover with default
}
}
return func(input string) Reader[Context, Validation[int]] {
@@ -43,7 +43,7 @@ func TestMonadChainLeft(t *testing.T) {
})
t.Run("preserves success values unchanged", func(t *testing.T) {
successValidator := Of[string, int](42)
successValidator := Of[string](42)
handler := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
@@ -145,7 +145,7 @@ func TestMonadChainLeft(t *testing.T) {
}
handler := func(errs Errors) Validate[Config, string] {
return Of[Config, string]("default-value")
return Of[Config]("default-value")
}
validator := MonadChainLeft(failingValidator, handler)
@@ -194,7 +194,7 @@ func TestMonadChainLeft(t *testing.T) {
}
handler := func(errs Errors) Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
// MonadChainLeft - direct application
@@ -229,7 +229,7 @@ func TestMonadChainLeft(t *testing.T) {
// Check if we can recover
for _, err := range errs {
if err.Messsage == "error1" {
return Of[string, int](100) // recover
return Of[string](100) // recover
}
}
return func(input string) Reader[Context, Validation[int]] {
@@ -248,12 +248,12 @@ func TestMonadChainLeft(t *testing.T) {
})
t.Run("does not call handler on success", func(t *testing.T) {
successValidator := Of[string, int](42)
successValidator := Of[string](42)
handlerCalled := false
handler := func(errs Errors) Validate[string, int] {
handlerCalled = true
return Of[string, int](0)
return Of[string](0)
}
validator := MonadChainLeft(successValidator, handler)
@@ -267,9 +267,9 @@ func TestMonadChainLeft(t *testing.T) {
// TestMonadAlt tests the MonadAlt function
func TestMonadAlt(t *testing.T) {
t.Run("returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
return Of[string](100)
}
result := MonadAlt(validator1, validator2)("input")(nil)
@@ -285,7 +285,7 @@ func TestMonadAlt(t *testing.T) {
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
result := MonadAlt(failing, fallback)("input")(nil)
@@ -328,11 +328,11 @@ func TestMonadAlt(t *testing.T) {
})
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
evaluated := false
validator2 := func() Validate[string, int] {
evaluated = true
return Of[string, int](100)
return Of[string](100)
}
result := MonadAlt(validator1, validator2)("input")(nil)
@@ -349,7 +349,7 @@ func TestMonadAlt(t *testing.T) {
}
}
fallback := func() Validate[string, string] {
return Of[string, string]("fallback")
return Of[string]("fallback")
}
result := MonadAlt(failing, fallback)("input")(nil)
@@ -374,7 +374,7 @@ func TestMonadAlt(t *testing.T) {
}
}
succeeding := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
// Chain: try failing1, then failing2, then succeeding
@@ -395,7 +395,7 @@ func TestMonadAlt(t *testing.T) {
}
}
fallback := func() Validate[Config, string] {
return Of[Config, string]("default")
return Of[Config]("default")
}
result := MonadAlt(failing, fallback)(Config{Port: 9999})(nil)
@@ -458,9 +458,9 @@ func TestMonadAlt(t *testing.T) {
// TestAlt tests the Alt function
func TestAlt(t *testing.T) {
t.Run("returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
return Of[string](100)
}
withAlt := Alt(validator2)
@@ -477,7 +477,7 @@ func TestAlt(t *testing.T) {
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
withAlt := Alt(fallback)
@@ -522,11 +522,11 @@ func TestAlt(t *testing.T) {
})
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
evaluated := false
validator2 := func() Validate[string, int] {
evaluated = true
return Of[string, int](100)
return Of[string](100)
}
withAlt := Alt(validator2)
@@ -553,7 +553,7 @@ func TestAlt(t *testing.T) {
}
}
succeeding := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
// Use F.Pipe to chain alternatives
@@ -576,7 +576,7 @@ func TestAlt(t *testing.T) {
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
// Alt - curried for pipelines
@@ -592,9 +592,9 @@ func TestAlt(t *testing.T) {
// TestMonadAltAndAltEquivalence tests that MonadAlt and Alt are equivalent
func TestMonadAltAndAltEquivalence(t *testing.T) {
t.Run("both produce same results for success", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
return Of[string](100)
}
resultMonadAlt := MonadAlt(validator1, validator2)("input")(nil)
@@ -612,7 +612,7 @@ func TestMonadAltAndAltEquivalence(t *testing.T) {
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
resultMonadAlt := MonadAlt(failing, fallback)("input")(nil)

View File

@@ -15,7 +15,7 @@ import (
// TestAlternativeMonoid tests the AlternativeMonoid function
func TestAlternativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := AlternativeMonoid[string, string](S.Monoid)
m := AlternativeMonoid[string](S.Monoid)
t.Run("empty returns validator that succeeds with empty string", func(t *testing.T) {
empty := m.Empty()
@@ -25,8 +25,8 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("concat combines successful validators using monoid", func(t *testing.T) {
validator1 := Of[string, string]("Hello")
validator2 := Of[string, string](" World")
validator1 := Of[string]("Hello")
validator2 := Of[string](" World")
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
@@ -42,7 +42,7 @@ func TestAlternativeMonoid(t *testing.T) {
})
}
}
succeeding := Of[string, string]("fallback")
succeeding := Of[string]("fallback")
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
@@ -85,7 +85,7 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("concat with empty preserves validator", func(t *testing.T) {
validator := Of[string, string]("test")
validator := Of[string]("test")
empty := m.Empty()
result1 := m.Concat(validator, empty)("input")(nil)
@@ -110,7 +110,7 @@ func TestAlternativeMonoid(t *testing.T) {
func(a, b int) int { return a + b },
0,
)
m := AlternativeMonoid[string, int](intMonoid)
m := AlternativeMonoid[string](intMonoid)
t.Run("empty returns validator with zero", func(t *testing.T) {
empty := m.Empty()
@@ -124,8 +124,8 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
validator1 := Of[string, int](10)
validator2 := Of[string, int](32)
validator1 := Of[string](10)
validator2 := Of[string](32)
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
@@ -145,7 +145,7 @@ func TestAlternativeMonoid(t *testing.T) {
})
}
}
succeeding := Of[string, int](42)
succeeding := Of[string](42)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
@@ -158,10 +158,10 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("multiple concat operations", func(t *testing.T) {
validator1 := Of[string, int](1)
validator2 := Of[string, int](2)
validator3 := Of[string, int](3)
validator4 := Of[string, int](4)
validator1 := Of[string](1)
validator2 := Of[string](2)
validator3 := Of[string](3)
validator4 := Of[string](4)
combined := m.Concat(m.Concat(m.Concat(validator1, validator2), validator3), validator4)
result := combined("input")(nil)
@@ -175,11 +175,11 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AlternativeMonoid[string, string](S.Monoid)
m := AlternativeMonoid[string](S.Monoid)
validator1 := Of[string, string]("a")
validator2 := Of[string, string]("b")
validator3 := Of[string, string]("c")
validator1 := Of[string]("a")
validator2 := Of[string]("b")
validator3 := Of[string]("c")
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), validator1)("input")(nil)
@@ -222,7 +222,7 @@ func TestAlternativeMonoid(t *testing.T) {
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
m := AltMonoid(func() Validate[string, int] {
return Of[string, int](0)
return Of[string](0)
})
t.Run("empty returns the provided zero validator", func(t *testing.T) {
@@ -233,8 +233,8 @@ func TestAltMonoid(t *testing.T) {
})
t.Run("concat returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := Of[string, int](100)
validator1 := Of[string](42)
validator2 := Of[string](100)
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
@@ -250,7 +250,7 @@ func TestAltMonoid(t *testing.T) {
})
}
}
succeeding := Of[string, int](42)
succeeding := Of[string](42)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
@@ -341,7 +341,7 @@ func TestAltMonoid(t *testing.T) {
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Validate[string, string] {
return Of[string, string]("default")
return Of[string]("default")
})
primary := func(input string) Reader[Context, Validation[string]] {
@@ -358,7 +358,7 @@ func TestAltMonoid(t *testing.T) {
})
}
}
tertiary := Of[string, string]("tertiary value")
tertiary := Of[string]("tertiary value")
combined := m.Concat(m.Concat(primary, secondary), tertiary)
result := combined("input")(nil)
@@ -369,14 +369,14 @@ func TestAltMonoid(t *testing.T) {
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
// AltMonoid - first success wins
altM := AltMonoid(func() Validate[string, int] {
return Of[string, int](0)
return Of[string](0)
})
// AlternativeMonoid - combines successes
altMonoid := AlternativeMonoid[string, int](N.MonoidSum[int]())
altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
validator1 := Of[string, int](10)
validator2 := Of[string, int](32)
validator1 := Of[string](10)
validator2 := Of[string](32)
// AltMonoid: returns first success (10)
result1 := altM.Concat(validator1, validator2)("input")(nil)

View File

@@ -1405,7 +1405,7 @@ func TestFromResult(t *testing.T) {
t.Run("extract from successful result", func(t *testing.T) {
prism := FromResult[int]()
success := result.Of[int](42)
success := result.Of(42)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
@@ -1435,7 +1435,7 @@ func TestFromResult(t *testing.T) {
t.Run("works with string type", func(t *testing.T) {
prism := FromResult[string]()
success := result.Of[string]("hello")
success := result.Of("hello")
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
@@ -1451,7 +1451,7 @@ func TestFromResult(t *testing.T) {
prism := FromResult[Person]()
person := Person{Name: "Alice", Age: 30}
success := result.Of[Person](person)
success := result.Of(person)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
@@ -1465,9 +1465,9 @@ func TestFromResult(t *testing.T) {
func TestFromResultWithSet(t *testing.T) {
t.Run("set on successful result", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
setter := Set[result.Result[int]](200)
success := result.Of[int](42)
success := result.Of(42)
updated := setter(prism)(success)
// Verify the value was updated
@@ -1478,7 +1478,7 @@ func TestFromResultWithSet(t *testing.T) {
t.Run("set on error result leaves it unchanged", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
setter := Set[result.Result[int]](200)
failure := E.Left[int](errors.New("test error"))
updated := setter(prism)(failure)
@@ -1527,13 +1527,13 @@ func TestFromResultComposition(t *testing.T) {
composed := Compose[result.Result[int]](positivePrism)(FromResult[int]())
// Test with positive number
success := result.Of[int](42)
success := result.Of(42)
extracted := composed.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
// Test with negative number
negativeSuccess := result.Of[int](-5)
negativeSuccess := result.Of(-5)
extracted = composed.GetOption(negativeSuccess)
assert.True(t, O.IsNone(extracted))
@@ -1705,7 +1705,7 @@ func TestParseJSONWithSet(t *testing.T) {
originalJSON := []byte(`{"name":"Alice","age":30}`)
newPerson := Person{Name: "Bob", Age: 25}
setter := Set[[]byte, Person](newPerson)
setter := Set[[]byte](newPerson)
updatedJSON := setter(prism)(originalJSON)
// Parse the updated JSON
@@ -1722,7 +1722,7 @@ func TestParseJSONWithSet(t *testing.T) {
invalidJSON := []byte(`{invalid}`)
newPerson := Person{Name: "Charlie", Age: 35}
setter := Set[[]byte, Person](newPerson)
setter := Set[[]byte](newPerson)
result := setter(prism)(invalidJSON)
// Should return original unchanged since it couldn't be parsed

View File

@@ -630,7 +630,7 @@ func TestLocalIOK(t *testing.T) {
}
// Compose using LocalIOK
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOK[string](loadConfig)(useConfig)
result := adapted("config.json")()
assert.Equal(t, "localhost:8080", result)
@@ -650,7 +650,7 @@ func TestLocalIOK(t *testing.T) {
return io.Of(fmt.Sprintf("Processed: %d", n))
}
adapted := LocalIOK[string, int, string](loadData)(processData)
adapted := LocalIOK[string](loadData)(processData)
result := adapted("test")()
assert.Equal(t, "Processed: 40", result)
@@ -679,8 +679,8 @@ func TestLocalIOK(t *testing.T) {
}
// Compose transformations
step1 := LocalIOK[string, UserEnv, int](loadUser)(formatUser)
step2 := LocalIOK[string, int, string](parseID)(step1)
step1 := LocalIOK[string](loadUser)(formatUser)
step2 := LocalIOK[string](parseID)(step1)
result := step2("42")()
assert.Equal(t, "User ID: 42", result)
@@ -704,7 +704,7 @@ func TestLocalIOK(t *testing.T) {
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
}
adapted := LocalIOK[string, DatabaseConfig, AppConfig](extractDB)(connectDB)
adapted := LocalIOK[string](extractDB)(connectDB)
result := adapted(AppConfig{
Database: DatabaseConfig{Host: "", Port: 5432},
})()
@@ -735,8 +735,8 @@ func TestLocalIOK(t *testing.T) {
}
// Compose the pipeline
step1 := LocalIOK[string, SimpleConfig, string](parseConfig)(useConfig)
step2 := LocalIOK[string, string, ConfigFile](readFile)(step1)
step1 := LocalIOK[string](parseConfig)(useConfig)
step2 := LocalIOK[string](readFile)(step1)
result := step2(ConfigFile{Path: "app.json"})()
assert.Equal(t, "Using example.com:9000", result)

View File

@@ -149,7 +149,7 @@ func TestLocalIOK(t *testing.T) {
}
// Compose using LocalIOK
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOK[string, string](loadConfig)(useConfig)
result := adapted("config.json")()
assert.Equal(t, E.Of[string]("Port: 8080"), result)
@@ -169,7 +169,7 @@ func TestLocalIOK(t *testing.T) {
return IOE.Of[string]("Processed: " + strconv.Itoa(n))
}
adapted := LocalIOK[string, string, int, string](loadData)(processData)
adapted := LocalIOK[string, string](loadData)(processData)
result := adapted("test")()
assert.Equal(t, E.Of[string]("Processed: 40"), result)
@@ -188,7 +188,7 @@ func TestLocalIOK(t *testing.T) {
return IOE.Left[string]("operation failed")
}
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(failingOperation)
adapted := LocalIOK[string, string](loadConfig)(failingOperation)
result := adapted("config.json")()
assert.Equal(t, E.Left[string]("operation failed"), result)
@@ -216,8 +216,8 @@ func TestLocalIOK(t *testing.T) {
}
// Compose transformations
step1 := LocalIOK[string, string, SimpleConfig, int](loadConfig)(formatConfig)
step2 := LocalIOK[string, string, int, string](parseID)(step1)
step1 := LocalIOK[string, string](loadConfig)(formatConfig)
step2 := LocalIOK[string, string](parseID)(step1)
result := step2("42")()
assert.Equal(t, E.Of[string]("Port: 8042"), result)
@@ -243,7 +243,7 @@ func TestLocalIOEitherK(t *testing.T) {
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
// Success case
result := adapted("config.json")()
@@ -265,7 +265,7 @@ func TestLocalIOEitherK(t *testing.T) {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
result := adapted("missing.json")()
// Error from loadConfig should propagate
@@ -282,7 +282,7 @@ func TestLocalIOEitherK(t *testing.T) {
return IOE.Left[string]("operation failed")
}
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(failingOperation)
adapted := LocalIOEitherK[string](loadConfig)(failingOperation)
result := adapted("config.json")()
// Error from ReaderIOEither should propagate
@@ -317,8 +317,8 @@ func TestLocalIOEitherK(t *testing.T) {
}
// Compose transformations
step1 := LocalIOEitherK[string, SimpleConfig, int, string](loadConfig)(formatConfig)
step2 := LocalIOEitherK[string, int, string, string](parseID)(step1)
step1 := LocalIOEitherK[string](loadConfig)(formatConfig)
step2 := LocalIOEitherK[string](parseID)(step1)
// Success case
result := step2("42")()
@@ -364,8 +364,8 @@ func TestLocalIOEitherK(t *testing.T) {
}
// Compose the pipeline
step1 := LocalIOEitherK[string, SimpleConfig, string, string](parseConfig)(useConfig)
step2 := LocalIOEitherK[string, string, ConfigFile, string](readFile)(step1)
step1 := LocalIOEitherK[string](parseConfig)(useConfig)
step2 := LocalIOEitherK[string](readFile)(step1)
// Success case
result := step2(ConfigFile{Path: "app.json"})()

View File

@@ -37,11 +37,46 @@ import (
"github.com/IBM/fp-go/v2/readeroption"
)
// FromReaderOption converts a ReaderOption to a Kleisli arrow that handles None cases.
// When the Option is None, the provided lazy error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The value type
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Kleisli arrow that converts ReaderOption to ReaderIOEither
//
//go:inline
func FromReaderOption[R, A, E any](onNone Lazy[E]) Kleisli[R, E, ReaderOption[R, A], A] {
return function.Bind2nd(function.Flow2[ReaderOption[R, A], IOE.Kleisli[E, Option[A], A]], IOE.FromOption[A](onNone))
}
// FromReaderIO lifts a ReaderIO into a ReaderIOEither, placing the result in the Right side.
// This is an alias for RightReaderIO, converting a computation that cannot fail into one
// that can fail but never does.
//
// # Type Parameters
//
// - E: The error type (will never actually contain an error)
// - R: The context/environment type
// - A: The value type
//
// # Parameters
//
// - ma: The ReaderIO to lift
//
// # Returns
//
// - ReaderIOEither with the ReaderIO result in the Right side
//
//go:inline
func FromReaderIO[E, R, A any](ma ReaderIO[R, A]) ReaderIOEither[R, E, A] {
return RightReaderIO[E](ma)
@@ -121,6 +156,26 @@ func MonadChainFirst[R, E, A, B any](fa ReaderIOEither[R, E, A], f Kleisli[R, E,
f)
}
// MonadTap is an alias for MonadChainFirst.
// It sequences two computations but keeps the result of the first, emphasizing the
// side-effect nature of the operation (like "tapping" into a pipeline).
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - fa: The ReaderIOEither computation
// - f: The side effect Kleisli arrow
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTap[R, E, A, B any](fa ReaderIOEither[R, E, A], f Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirst(fa, f)
@@ -165,6 +220,26 @@ func MonadChainFirstEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f either
)
}
// MonadTapEitherK is an alias for MonadChainFirstEitherK.
// It chains an Either-returning computation while preserving the original value,
// emphasizing the side-effect nature of the operation.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Either-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f either.Kleisli[E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstEitherK(ma, f)
@@ -183,6 +258,25 @@ func ChainFirstEitherK[R, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, E,
)
}
// TapEitherK is an alias for ChainFirstEitherK.
// It returns a function that chains an Either-returning side effect while preserving
// the original value, emphasizing the "tap" pattern for observing values.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Either-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapEitherK[R, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, E, A, A] {
return ChainFirstEitherK[R](f)
@@ -213,6 +307,26 @@ func ChainReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E, A, B
)
}
// MonadChainFirstReaderK chains a Reader-returning computation while preserving the original value.
// Useful for performing Reader-based side effects (like logging with context) while keeping
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Reader-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -223,6 +337,25 @@ func MonadChainFirstReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader
)
}
// MonadTapReaderK is an alias for MonadChainFirstReaderK.
// It chains a Reader-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Reader-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderK(ma, f)
@@ -240,11 +373,49 @@ func ChainFirstReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E,
)
}
// TapReaderK is an alias for ChainFirstReaderK.
// It returns a function that chains a Reader-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Reader-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderK[E](f)
}
// MonadChainReaderIOK chains a ReaderIO-returning computation into a ReaderIOEither.
// The ReaderIO is automatically lifted into the ReaderIOEither context.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning function
//
// # Returns
//
// - ReaderIOEither with the result of the ReaderIO computation
//
//go:inline
func MonadChainReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
@@ -255,6 +426,24 @@ func MonadChainReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.
)
}
// ChainReaderIOK returns a function that chains a ReaderIO-returning function into ReaderIOEither.
// This is the curried version of MonadChainReaderIOK.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: The ReaderIO-returning function
//
// # Returns
//
// - Operator that chains the ReaderIO computation
//
//go:inline
func ChainReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, B] {
return fromreader.ChainReaderK(
@@ -264,6 +453,25 @@ func ChainReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E,
)
}
// MonadChainFirstReaderIOK chains a ReaderIO-returning computation while preserving the original value.
// Useful for performing ReaderIO-based side effects while keeping the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -274,11 +482,48 @@ func MonadChainFirstReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f read
)
}
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK.
// It chains a ReaderIO-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderIOK(ma, f)
}
// ChainFirstReaderIOK returns a function that chains a ReaderIO-returning function while
// preserving the original value. This is the curried version of MonadChainFirstReaderIOK.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func ChainFirstReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -288,11 +533,49 @@ func ChainFirstReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R
)
}
// TapReaderIOK is an alias for ChainFirstReaderIOK.
// It returns a function that chains a ReaderIO-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderIOK[E](f)
}
// MonadChainReaderEitherK chains a ReaderEither-returning computation into a ReaderIOEither.
// The ReaderEither is automatically lifted into the ReaderIOEither context.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning function
//
// # Returns
//
// - ReaderIOEither with the result of the ReaderEither computation
//
//go:inline
func MonadChainReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
@@ -315,6 +598,25 @@ func ChainReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E,
)
}
// MonadChainFirstReaderEitherK chains a ReaderEither-returning computation while preserving the original value.
// Useful for performing ReaderEither-based side effects while keeping the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -325,6 +627,25 @@ func MonadChainFirstReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f
)
}
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK.
// It chains a ReaderEither-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderEitherK(ma, f)
@@ -342,11 +663,48 @@ func ChainFirstReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[
)
}
// TapReaderEitherK is an alias for ChainFirstReaderEitherK.
// It returns a function that chains a ReaderEither-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderEitherK(f)
}
// ChainReaderOptionK returns a function that chains a ReaderOption-returning function into ReaderIOEither.
// When the Option is None, the provided error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func ChainReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, B] {
fro := FromReaderOption[R, B](onNone)
@@ -359,6 +717,24 @@ func ChainReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisl
}
}
// ChainFirstReaderOptionK returns a function that chains a ReaderOption-returning function
// while preserving the original value. When the Option is None, the provided error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func ChainFirstReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
fro := FromReaderOption[R, B](onNone)
@@ -371,6 +747,25 @@ func ChainFirstReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.K
}
}
// TapReaderOptionK is an alias for ChainFirstReaderOptionK.
// It returns a function that chains a ReaderOption-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func TapReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderOptionK[R, A, B](onNone)
@@ -401,6 +796,110 @@ func ChainIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, B]
)
}
// ChainFirstIOEitherK chains an IOEither computation while preserving the original value.
// This is useful for performing side effects that may fail (like logging, validation, or
// external API calls) while keeping the original value in the success case.
//
// The function executes the IOEither computation but discards its result, returning the
// original value if both computations succeed. If either computation fails, the error
// is propagated.
//
// This is the curried version that returns an Operator for use in function composition.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: IOEither.Kleisli function that performs the side effect
//
// # Returns
//
// - Operator that chains the side effect while preserving the original value
//
// # Example Usage
//
// type Config struct{ LogEnabled bool }
//
// logValue := func(v int) IOEither[error, string] {
// return IOE.Of[error](fmt.Sprintf("Value: %d", v))
// }
//
// pipeline := F.Pipe1(
// Of[Config, error](42),
// ChainFirstIOEitherK[Config](logValue),
// )
// result := pipeline(Config{LogEnabled: true})() // Right(42)
//
// # See Also
//
// - TapIOEitherK: Alias for ChainFirstIOEitherK
// - ChainIOEitherK: Chains IOEither and uses its result
// - ChainFirstEitherK: Similar but for Either computations
//
//go:inline
func ChainFirstIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, A] {
return fromioeither.ChainFirstIOEitherK(
Chain[R, E, A, A],
Map[R, E, B, A],
FromIOEither[R, E, B],
f,
)
}
// TapIOEitherK is an alias for ChainFirstIOEitherK.
// It executes an IOEither side effect while preserving the original value.
//
// The name "Tap" emphasizes the side-effect nature of the operation, similar to
// tapping into a pipeline to observe or log values without modifying the flow.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: IOEither.Kleisli function that performs the side effect
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
// # Example Usage
//
// type Config struct{}
//
// validatePositive := func(v int) IOEither[error, bool] {
// if v > 0 {
// return IOE.Of[error](true)
// }
// return IOE.Left[bool](errors.New("must be positive"))
// }
//
// pipeline := F.Pipe1(
// Of[Config, error](42),
// TapIOEitherK[Config](validatePositive),
// )
// result := pipeline(Config{})() // Right(42) if validation passes
//
// # See Also
//
// - ChainFirstIOEitherK: The underlying implementation
// - TapEitherK: Similar but for Either computations
// - TapIOK: Similar but for IO computations
//
//go:inline
func TapIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, A] {
return ChainFirstIOEitherK[R](f)
}
// MonadChainIOK chains an IO-returning computation into a ReaderIOEither.
// The IO is automatically lifted into the ReaderIOEither context (always succeeds).
//
@@ -440,6 +939,25 @@ func MonadChainFirstIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f io.Kleisli
)
}
// MonadTapIOK is an alias for MonadChainFirstIOK.
// It chains an IO-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The IO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f io.Kleisli[A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstIOK(ma, f)
@@ -458,6 +976,25 @@ func ChainFirstIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, A] {
)
}
// TapIOK is an alias for ChainFirstIOK.
// It returns a function that chains an IO-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The IO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, A] {
return ChainFirstIOK[R, E](f)
@@ -540,6 +1077,25 @@ func ChainFirst[R, E, A, B any](f Kleisli[R, E, A, B]) Operator[R, E, A, A] {
f)
}
// Tap is an alias for ChainFirst.
// It returns a function that sequences computations but keeps the first result,
// emphasizing the side-effect nature of the operation.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Kleisli arrow for the side effect
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func Tap[R, E, A, B any](f Kleisli[R, E, A, B]) Operator[R, E, A, A] {
return ChainFirst(f)

View File

@@ -733,3 +733,390 @@ func TestChainLeftIdenticalToOrElse(t *testing.T) {
assert.Equal(t, cfg, *chainLeftCfg)
})
}
func TestChainFirstIOEitherK(t *testing.T) {
type Config struct {
logEnabled bool
}
t.Run("Success - preserves original value", func(t *testing.T) {
sideEffectRan := false
logValue := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
sideEffectRan = true
return E.Right[error](fmt.Sprintf("Logged: %d", v))
}
}
pipeline := F.Pipe1(
Of[Config, error](42),
ChainFirstIOEitherK[Config](logValue),
)
result := pipeline(Config{logEnabled: true})()
assert.Equal(t, E.Right[error](42), result)
assert.True(t, sideEffectRan)
})
t.Run("Success - side effect result is discarded", func(t *testing.T) {
sideEffect := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error]("side effect result")
}
pipeline := F.Pipe1(
Of[Config, error](100),
ChainFirstIOEitherK[Config](sideEffect),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](100), result)
})
t.Run("Failure - side effect fails", func(t *testing.T) {
sideEffectError := errors.New("side effect failed")
failingSideEffect := func(v int) IOE.IOEither[error, string] {
return IOE.Left[string](sideEffectError)
}
pipeline := F.Pipe1(
Of[Config, error](42),
ChainFirstIOEitherK[Config](failingSideEffect),
)
result := pipeline(Config{})()
assert.Equal(t, E.Left[int](sideEffectError), result)
})
t.Run("Failure - original computation fails", func(t *testing.T) {
originalError := errors.New("original failed")
sideEffectRan := false
sideEffect := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
sideEffectRan = true
return E.Right[error]("logged")
}
}
pipeline := F.Pipe1(
Left[Config, int](originalError),
ChainFirstIOEitherK[Config](sideEffect),
)
result := pipeline(Config{})()
assert.Equal(t, E.Left[int](originalError), result)
assert.False(t, sideEffectRan, "Side effect should not run when original computation fails")
})
t.Run("Chaining multiple side effects", func(t *testing.T) {
log1Ran := false
log2Ran := false
log1 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
log1Ran = true
return E.Right[error]("log1")
}
}
log2 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
log2Ran = true
return E.Right[error]("log2")
}
}
pipeline := F.Pipe2(
Of[Config, error](42),
ChainFirstIOEitherK[Config](log1),
ChainFirstIOEitherK[Config](log2),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](42), result)
assert.True(t, log1Ran)
assert.True(t, log2Ran)
})
t.Run("Integration with Map", func(t *testing.T) {
validate := func(v int) IOE.IOEither[error, bool] {
if v > 0 {
return IOE.Of[error](true)
}
return IOE.Left[bool](errors.New("must be positive"))
}
pipeline := F.Pipe2(
Of[Config, error](10),
ChainFirstIOEitherK[Config](validate),
Map[Config, error](func(x int) int { return x * 2 }),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](20), result)
})
t.Run("Different types for input and side effect result", func(t *testing.T) {
convertToString := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error](fmt.Sprintf("Value: %d", v))
}
pipeline := F.Pipe1(
Of[Config, error](123),
ChainFirstIOEitherK[Config](convertToString),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](123), result)
})
t.Run("With context-dependent side effect", func(t *testing.T) {
logged := ""
logIfEnabled := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
logged = fmt.Sprintf("Logged: %d", v)
return E.Right[error](logged)
}
}
pipeline := F.Pipe1(
Of[Config, error](99),
ChainFirstIOEitherK[Config](logIfEnabled),
)
result := pipeline(Config{logEnabled: true})()
assert.Equal(t, E.Right[error](99), result)
assert.Equal(t, "Logged: 99", logged)
})
}
func TestTapIOEitherK(t *testing.T) {
type Config struct {
debugMode bool
}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tapped = true
return E.Right[error](fmt.Sprintf("Tapped: %d", v))
}
}
pipeline := F.Pipe1(
Of[Config, error](42),
TapIOEitherK[Config](tapFunc),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](42), result)
assert.True(t, tapped)
})
t.Run("Failure - tap fails", func(t *testing.T) {
tapError := errors.New("tap failed")
tapFunc := func(v int) IOE.IOEither[error, string] {
return IOE.Left[string](tapError)
}
pipeline := F.Pipe1(
Of[Config, error](42),
TapIOEitherK[Config](tapFunc),
)
result := pipeline(Config{})()
assert.Equal(t, E.Left[int](tapError), result)
})
t.Run("Failure - original computation fails", func(t *testing.T) {
originalError := errors.New("original failed")
tapped := false
tapFunc := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tapped = true
return E.Right[error]("tapped")
}
}
pipeline := F.Pipe1(
Left[Config, int](originalError),
TapIOEitherK[Config](tapFunc),
)
result := pipeline(Config{})()
assert.Equal(t, E.Left[int](originalError), result)
assert.False(t, tapped, "Tap should not run when original computation fails")
})
t.Run("Validation use case", func(t *testing.T) {
validatePositive := func(v int) IOE.IOEither[error, bool] {
if v > 0 {
return IOE.Of[error](true)
}
return IOE.Left[bool](errors.New("must be positive"))
}
pipeline := F.Pipe1(
Of[Config, error](42),
TapIOEitherK[Config](validatePositive),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](42), result)
})
t.Run("Validation failure", func(t *testing.T) {
validatePositive := func(v int) IOE.IOEither[error, bool] {
if v > 0 {
return IOE.Of[error](true)
}
return IOE.Left[bool](errors.New("must be positive"))
}
pipeline := F.Pipe1(
Of[Config, error](-5),
TapIOEitherK[Config](validatePositive),
)
result := pipeline(Config{})()
assert.True(t, E.IsLeft(result))
})
t.Run("Multiple taps in sequence", func(t *testing.T) {
tap1Ran := false
tap2Ran := false
tap3Ran := false
tap1 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tap1Ran = true
return E.Right[error]("tap1")
}
}
tap2 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tap2Ran = true
return E.Right[error]("tap2")
}
}
tap3 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tap3Ran = true
return E.Right[error]("tap3")
}
}
pipeline := F.Pipe3(
Of[Config, error](100),
TapIOEitherK[Config](tap1),
TapIOEitherK[Config](tap2),
TapIOEitherK[Config](tap3),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](100), result)
assert.True(t, tap1Ran)
assert.True(t, tap2Ran)
assert.True(t, tap3Ran)
})
t.Run("Tap with transformation pipeline", func(t *testing.T) {
tappedValue := 0
tapFunc := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tappedValue = v
return E.Right[error]("tapped")
}
}
pipeline := F.Pipe3(
Of[Config, error](10),
Map[Config, error](func(x int) int { return x * 2 }),
TapIOEitherK[Config](tapFunc),
Map[Config, error](func(x int) int { return x + 5 }),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](25), result)
assert.Equal(t, 20, tappedValue)
})
t.Run("Tap is alias for ChainFirstIOEitherK", func(t *testing.T) {
// Verify that TapIOEitherK and ChainFirstIOEitherK produce identical results
sideEffect := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error](fmt.Sprintf("Value: %d", v))
}
pipelineWithTap := F.Pipe1(
Of[Config, error](42),
TapIOEitherK[Config](sideEffect),
)
pipelineWithChainFirst := F.Pipe1(
Of[Config, error](42),
ChainFirstIOEitherK[Config](sideEffect),
)
resultTap := pipelineWithTap(Config{})()
resultChainFirst := pipelineWithChainFirst(Config{})()
assert.Equal(t, resultChainFirst, resultTap)
})
t.Run("Logging use case", func(t *testing.T) {
logs := []string{}
logValue := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
logMsg := fmt.Sprintf("Processing value: %d", v)
logs = append(logs, logMsg)
return E.Right[error](logMsg)
}
}
pipeline := F.Pipe2(
Of[Config, error](5),
TapIOEitherK[Config](logValue),
Map[Config, error](func(x int) int { return x * x }),
)
result := pipeline(Config{debugMode: true})()
assert.Equal(t, E.Right[error](25), result)
assert.Len(t, logs, 1)
assert.Equal(t, "Processing value: 5", logs[0])
})
t.Run("Error propagation in tap chain", func(t *testing.T) {
tap1 := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error]("tap1")
}
tap2 := func(v int) IOE.IOEither[error, string] {
return IOE.Left[string](errors.New("tap2 failed"))
}
tap3 := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error]("tap3")
}
pipeline := F.Pipe3(
Of[Config, error](42),
TapIOEitherK[Config](tap1),
TapIOEitherK[Config](tap2),
TapIOEitherK[Config](tap3),
)
result := pipeline(Config{})()
assert.True(t, E.IsLeft(result))
})
}

View File

@@ -32,7 +32,7 @@ func TestTraverseArray_AllSuccess(t *testing.T) {
}
input := []int{1, 2, 3, 4, 5}
result := TraverseArray[context.Context](double)(input)
result := TraverseArray(double)(input)
expected := O.Of([]int{2, 4, 6, 8, 10})
assert.Equal(t, expected, result(context.Background())())
@@ -48,7 +48,7 @@ func TestTraverseArray_OneFailure(t *testing.T) {
}
input := []int{1, 2, 3, 4, 5}
result := TraverseArray[context.Context](failOnThree)(input)
result := TraverseArray(failOnThree)(input)
expected := O.None[[]int]()
assert.Equal(t, expected, result(context.Background())())
@@ -61,7 +61,7 @@ func TestTraverseArray_EmptyArray(t *testing.T) {
}
input := []int{}
result := TraverseArray[context.Context](double)(input)
result := TraverseArray(double)(input)
expected := O.Of([]int{})
assert.Equal(t, expected, result(context.Background())())
@@ -82,7 +82,7 @@ func TestTraverseArray_WithEnvironment(t *testing.T) {
}
input := []int{1, 2, 3}
result := TraverseArray[Config](multiply)(input)
result := TraverseArray(multiply)(input)
cfg := Config{Multiplier: 10}
expected := O.Of([]int{10, 20, 30})
@@ -105,7 +105,7 @@ func TestTraverseArray_ChainedOperation(t *testing.T) {
result := F.Pipe1(
Of[Config]([]int{1, 2, 3, 4}),
Chain(TraverseArray[Config](multiplyByFactor)),
Chain(TraverseArray(multiplyByFactor)),
)
cfg := Config{Factor: 5}
@@ -120,7 +120,7 @@ func TestTraverseArrayWithIndex_AllSuccess(t *testing.T) {
}
input := []string{"a", "b", "c"}
result := TraverseArrayWithIndex[context.Context](addIndex)(input)
result := TraverseArrayWithIndex(addIndex)(input)
expected := O.Of([]string{"0:a", "1:b", "2:c"})
assert.Equal(t, expected, result(context.Background())())
@@ -136,7 +136,7 @@ func TestTraverseArrayWithIndex_OneFailure(t *testing.T) {
}
input := []string{"a", "b", "c"}
result := TraverseArrayWithIndex[context.Context](failOnIndex)(input)
result := TraverseArrayWithIndex(failOnIndex)(input)
expected := O.None[[]string]()
assert.Equal(t, expected, result(context.Background())())
@@ -149,7 +149,7 @@ func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
}
input := []string{}
result := TraverseArrayWithIndex[context.Context](addIndex)(input)
result := TraverseArrayWithIndex(addIndex)(input)
expected := O.Of([]string{})
assert.Equal(t, expected, result(context.Background())())
@@ -170,7 +170,7 @@ func TestTraverseArrayWithIndex_WithEnvironment(t *testing.T) {
}
input := []string{"a", "b", "c"}
result := TraverseArrayWithIndex[Config](formatWithIndex)(input)
result := TraverseArrayWithIndex(formatWithIndex)(input)
cfg := Config{Prefix: "item-"}
expected := O.Of([]string{"item-0:a", "item-1:b", "item-2:c"})
@@ -184,7 +184,7 @@ func TestTraverseArrayWithIndex_IndexUsedInLogic(t *testing.T) {
}
input := []int{10, 20, 30, 40}
result := TraverseArrayWithIndex[context.Context](multiplyByIndex)(input)
result := TraverseArrayWithIndex(multiplyByIndex)(input)
// 10*0=0, 20*1=20, 30*2=60, 40*3=120
expected := O.Of([]int{0, 20, 60, 120})
@@ -216,7 +216,7 @@ func TestTraverseArray_ComplexType(t *testing.T) {
{ID: 3, Name: "Charlie"},
}
result := TraverseArray[context.Context](loadProfile)(users)
result := TraverseArray(loadProfile)(users)
expected := O.Of([]UserProfile{
{UserID: 1, DisplayName: "Profile: Alice"},
@@ -247,12 +247,12 @@ func TestTraverseArray_ConditionalFailure(t *testing.T) {
// With MaxValue=3, should fail on 4 and 5
cfg1 := Config{MaxValue: 3}
result1 := TraverseArray[Config](validateAndDouble)(input)
result1 := TraverseArray(validateAndDouble)(input)
assert.Equal(t, O.None[[]int](), result1(cfg1)())
// With MaxValue=10, all should succeed
cfg2 := Config{MaxValue: 10}
result2 := TraverseArray[Config](validateAndDouble)(input)
result2 := TraverseArray(validateAndDouble)(input)
expected := O.Of([]int{2, 4, 6, 8, 10})
assert.Equal(t, expected, result2(cfg2)())
}

View File

@@ -67,7 +67,7 @@ func TestFromReader(t *testing.T) {
return cfg.Value * 2
}
ro := FromReader[Config](r)
ro := FromReader(r)
cfg := Config{Value: 21}
result := ro(cfg)()
@@ -83,7 +83,7 @@ func TestSomeReader(t *testing.T) {
return cfg.Value * 2
}
ro := SomeReader[Config](r)
ro := SomeReader(r)
cfg := Config{Value: 21}
result := ro(cfg)()

View File

@@ -20,6 +20,7 @@ import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
RE "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
@@ -28,6 +29,9 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// FromReaderOption converts a ReaderOption to a ReaderIOResult.
// If the ReaderOption is None, the provided function is called to produce the error.
//
//go:inline
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
return RIOE.FromReaderOption[R, A](onNone)
@@ -105,6 +109,9 @@ func MonadChainFirst[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) R
return RIOE.MonadChainFirst(fa, f)
}
// MonadTap is an alias for MonadChainFirst, executing a side effect while preserving the original value.
// The name "Tap" emphasizes the side-effect nature of the operation.
//
//go:inline
func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTap(fa, f)
@@ -150,6 +157,8 @@ func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleis
return RIOE.MonadChainFirstEitherK(ma, f)
}
// MonadTapEitherK is an alias for MonadChainFirstEitherK, executing a Result side effect while preserving the original value.
//
//go:inline
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
@@ -163,11 +172,43 @@ func ChainFirstEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
// TapEitherK is an alias for ChainFirstEitherK, executing a Result side effect while preserving the original value.
//
//go:inline
func TapEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
}
// ChainFirstIOEitherK chains an IOResult computation while preserving the original value.
// Useful for performing side effects that may fail while keeping the original value.
//
//go:inline
func ChainFirstIOEitherK[R, A, B any](f ioresult.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstIOEitherK[R](f)
}
// TapIOEitherK is an alias for ChainFirstIOEitherK, executing an IOResult side effect while preserving the original value.
//
//go:inline
func TapIOEitherK[R, A, B any](f ioresult.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapIOEitherK[R](f)
}
// ChainFirstIOResultK chains an IOResult computation while preserving the original value.
// This is an alias for ChainFirstIOEitherK with more explicit naming.
//
//go:inline
func ChainFirstIOResultK[R, A, B any](f ioresult.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstIOEitherK[R](f)
}
// TapIOResultK is an alias for ChainFirstIOResultK, executing an IOResult side effect while preserving the original value.
//
//go:inline
func TapIOResultK[R, A, B any](f ioresult.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapIOEitherK[R](f)
}
// MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
// Useful for validation or side effects that return Either.
//
@@ -176,19 +217,23 @@ func MonadChainFirstResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleis
return RIOE.MonadChainFirstEitherK(ma, f)
}
// MonadTapResultK is an alias for MonadChainFirstResultK, executing a Result side effect while preserving the original value.
//
//go:inline
func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
}
// ChainFirstEitherK returns a function that chains an Either computation while preserving the original value.
// This is the curried version of MonadChainFirstEitherK.
// ChainFirstResultK returns a function that chains a Result computation while preserving the original value.
// This is an alias for ChainFirstEitherK with more explicit naming.
//
//go:inline
func ChainFirstResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
// TapResultK is an alias for ChainFirstResultK, executing a Result side effect while preserving the original value.
//
//go:inline
func TapResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
@@ -210,36 +255,54 @@ func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderK[error](f)
}
// MonadChainFirstReaderK chains a Reader computation but keeps the original value.
// Useful for performing Reader-based side effects while preserving the original value.
//
//go:inline
func MonadChainFirstReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderK(ma, f)
}
// MonadTapReaderK is an alias for MonadChainFirstReaderK, executing a Reader side effect while preserving the original value.
//
//go:inline
func MonadTapReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapReaderK(ma, f)
}
// ChainFirstReaderK returns a function that chains a Reader computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderK.
//
//go:inline
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderK[error](f)
}
// TapReaderK is an alias for ChainFirstReaderK, executing a Reader side effect while preserving the original value.
//
//go:inline
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderK[error](f)
}
// ChainReaderOptionK returns a function that chains a ReaderOption-returning function into ReaderIOResult.
// If the ReaderOption is None, the provided error function is called.
//
//go:inline
func ChainReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderOptionK[R, A, B](onNone)
}
// ChainFirstReaderOptionK chains a ReaderOption computation while preserving the original value.
// If the ReaderOption is None, the provided error function is called.
//
//go:inline
func ChainFirstReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderOptionK[R, A, B](onNone)
}
// TapReaderOptionK is an alias for ChainFirstReaderOptionK, executing a ReaderOption side effect while preserving the original value.
//
//go:inline
func TapReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderOptionK[R, A, B](onNone)
@@ -261,84 +324,123 @@ func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A
return RIOE.ChainReaderEitherK(f)
}
// MonadChainFirstReaderEitherK chains a ReaderEither computation but keeps the original value.
// Useful for performing ReaderEither-based side effects while preserving the original value.
//
//go:inline
func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderEitherK(ma, f)
}
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK, executing a ReaderEither side effect while preserving the original value.
//
//go:inline
func MonadTapReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapReaderEitherK(ma, f)
}
// ChainFirstReaderEitherK returns a function that chains a ReaderEither computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderEitherK.
//
//go:inline
func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderEitherK(f)
}
// TapReaderEitherK is an alias for ChainFirstReaderEitherK, executing a ReaderEither side effect while preserving the original value.
//
//go:inline
func TapReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.TapReaderEitherK(f)
}
// MonadChainReaderResultK chains a ReaderResult-returning computation into a ReaderIOResult.
// This is an alias for MonadChainReaderEitherK with more explicit naming.
//
//go:inline
func MonadChainReaderResultK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainReaderEitherK(ma, f)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderK.
// ChainReaderResultK returns a function that chains a ReaderResult-returning function into ReaderIOResult.
// This is an alias for ChainReaderEitherK with more explicit naming.
//
//go:inline
func ChainReaderResultK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderEitherK(f)
}
// MonadChainFirstReaderResultK chains a ReaderResult computation but keeps the original value.
// This is an alias for MonadChainFirstReaderEitherK with more explicit naming.
//
//go:inline
func MonadChainFirstReaderResultK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderEitherK(ma, f)
}
// MonadTapReaderResultK is an alias for MonadChainFirstReaderResultK, executing a ReaderResult side effect while preserving the original value.
//
//go:inline
func MonadTapReaderResultK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapReaderEitherK(ma, f)
}
// ChainFirstReaderResultK returns a function that chains a ReaderResult computation while preserving the original value.
// This is an alias for ChainFirstReaderEitherK with more explicit naming.
//
//go:inline
func ChainFirstReaderResultK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderEitherK(f)
}
// TapReaderResultK is an alias for ChainFirstReaderResultK, executing a ReaderResult side effect while preserving the original value.
//
//go:inline
func TapReaderResultK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.TapReaderEitherK(f)
}
// MonadChainReaderIOK chains a ReaderIO-returning computation into a ReaderIOResult.
// The ReaderIO is automatically lifted into the ReaderIOResult context (always succeeds).
//
//go:inline
func MonadChainReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainReaderIOK(ma, f)
}
// ChainReaderIOK returns a function that chains a ReaderIO-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderIOK.
//
//go:inline
func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderIOK[error](f)
}
// MonadChainFirstReaderIOK chains a ReaderIO computation but keeps the original value.
// Useful for performing ReaderIO-based side effects while preserving the original value.
//
//go:inline
func MonadChainFirstReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderIOK(ma, f)
}
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK, executing a ReaderIO side effect while preserving the original value.
//
//go:inline
func MonadTapReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapReaderIOK(ma, f)
}
// ChainFirstReaderIOK returns a function that chains a ReaderIO computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderIOK.
//
//go:inline
func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderIOK[error](f)
}
// TapReaderIOK is an alias for ChainFirstReaderIOK, executing a ReaderIO side effect while preserving the original value.
//
//go:inline
func TapReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderIOK[error](f)
@@ -400,6 +502,8 @@ func MonadChainFirstIOK[R, A, B any](ma ReaderIOResult[R, A], f func(A) IO[B]) R
return RIOE.MonadChainFirstIOK(ma, f)
}
// MonadTapIOK is an alias for MonadChainFirstIOK, executing an IO side effect while preserving the original value.
//
//go:inline
func MonadTapIOK[R, A, B any](ma ReaderIOResult[R, A], f func(A) IO[B]) ReaderIOResult[R, A] {
return RIOE.MonadTapIOK(ma, f)
@@ -413,6 +517,8 @@ func ChainFirstIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, A] {
return RIOE.ChainFirstIOK[R, error](f)
}
// TapIOK is an alias for ChainFirstIOK, executing an IO side effect while preserving the original value.
//
//go:inline
func TapIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, A] {
return RIOE.TapIOK[R, error](f)
@@ -473,6 +579,9 @@ func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirst(f)
}
// Tap is an alias for ChainFirst, executing a side effect while preserving the original value.
// The name "Tap" emphasizes the side-effect nature of the operation.
//
//go:inline
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.Tap(f)
@@ -767,46 +876,70 @@ func Local[A, R1, R2 any](f func(R2) R1) func(ReaderIOResult[R1, A]) ReaderIORes
return RIOE.Local[error, A](f)
}
// Read executes a ReaderIOResult by providing a concrete environment value.
// This converts a ReaderIOResult[R, A] into an IOResult[A] by supplying the R value.
//
//go:inline
func Read[A, R any](r R) func(ReaderIOResult[R, A]) IOResult[A] {
return RIOE.Read[error, A](r)
}
// MonadChainLeft chains a computation on the error channel, allowing error recovery or transformation.
// If the computation is successful (Right), it passes through unchanged.
//
//go:inline
func MonadChainLeft[R, A any](fa ReaderIOResult[R, A], f Kleisli[R, error, A]) ReaderIOResult[R, A] {
return RIOE.MonadChainLeft(fa, f)
}
// ChainLeft returns a function that chains a computation on the error channel.
// This is the curried version of MonadChainLeft.
//
//go:inline
func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderIOResult[R, A]) ReaderIOResult[R, A] {
return RIOE.ChainLeft(f)
}
// MonadChainFirstLeft chains an error-handling computation but preserves the original error.
// Useful for logging or side effects on errors without changing the error value.
//
//go:inline
func MonadChainFirstLeft[A, R, B any](ma ReaderIOResult[R, A], f Kleisli[R, error, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstLeft(ma, f)
}
// MonadTapLeft is an alias for MonadChainFirstLeft, executing a side effect on errors while preserving the original error.
//
//go:inline
func MonadTapLeft[A, R, B any](ma ReaderIOResult[R, A], f Kleisli[R, error, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapLeft(ma, f)
}
// ChainFirstLeft returns a function that chains an error-handling computation while preserving the original error.
// This is the curried version of MonadChainFirstLeft.
//
//go:inline
func ChainFirstLeft[A, R, B any](f Kleisli[R, error, B]) Operator[R, A, A] {
return RIOE.ChainFirstLeft[A](f)
}
// ChainFirstLeftIOK chains an IO computation on errors while preserving the original error.
// Useful for IO-based error logging or side effects.
//
//go:inline
func ChainFirstLeftIOK[A, R, B any](f io.Kleisli[error, B]) Operator[R, A, A] {
return RIOE.ChainFirstLeftIOK[A, R](f)
}
// TapLeft is an alias for ChainFirstLeft, executing a side effect on errors while preserving the original error.
//
//go:inline
func TapLeft[A, R, B any](f Kleisli[R, error, B]) Operator[R, A, A] {
return RIOE.TapLeft[A](f)
}
// TapLeftIOK is an alias for ChainFirstLeftIOK, executing an IO side effect on errors while preserving the original error.
//
//go:inline
func TapLeftIOK[A, R, B any](f io.Kleisli[error, B]) Operator[R, A, A] {
return RIOE.TapLeftIOK[A, R](f)

View File

@@ -349,7 +349,7 @@ func TestLocalIOK(t *testing.T) {
}
// Compose using LocalIOK
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOK[string](loadConfig)(useConfig)
res := adapted("config.json")()
assert.Equal(t, result.Of("Port: 8080"), res)
@@ -371,7 +371,7 @@ func TestLocalIOK(t *testing.T) {
}
}
adapted := LocalIOK[string, int, string](loadData)(processData)
adapted := LocalIOK[string](loadData)(processData)
res := adapted("test")()
assert.Equal(t, result.Of("Processed: 40"), res)
@@ -392,7 +392,7 @@ func TestLocalIOK(t *testing.T) {
}
}
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(failingOperation)
adapted := LocalIOK[string](loadConfig)(failingOperation)
res := adapted("config.json")()
assert.True(t, result.IsLeft(res))
@@ -424,7 +424,7 @@ func TestLocalIOEitherK(t *testing.T) {
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")()
@@ -448,7 +448,7 @@ func TestLocalIOEitherK(t *testing.T) {
}
}
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
res := adapted("missing.json")()
// Error from loadConfig should propagate
@@ -469,7 +469,7 @@ func TestLocalIOEitherK(t *testing.T) {
}
}
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(failingOperation)
adapted := LocalIOEitherK[string](loadConfig)(failingOperation)
res := adapted("config.json")()
// Error from ReaderIOResult should propagate
@@ -502,7 +502,7 @@ func TestLocalIOResultK(t *testing.T) {
}
// Compose using LocalIOResultK
adapted := LocalIOResultK[string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOResultK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")()
@@ -526,7 +526,7 @@ func TestLocalIOResultK(t *testing.T) {
}
}
adapted := LocalIOResultK[string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOResultK[string](loadConfig)(useConfig)
res := adapted("missing.json")()
// Error from loadConfig should propagate
@@ -562,8 +562,8 @@ func TestLocalIOResultK(t *testing.T) {
}
// Compose transformations
step1 := LocalIOResultK[string, SimpleConfig, int](loadConfig)(formatConfig)
step2 := LocalIOResultK[string, int, string](parseID)(step1)
step1 := LocalIOResultK[string](loadConfig)(formatConfig)
step2 := LocalIOResultK[string](parseID)(step1)
// Success case
res := step2("test")()
@@ -607,8 +607,8 @@ func TestLocalIOResultK(t *testing.T) {
}
// Compose the pipeline
step1 := LocalIOResultK[string, SimpleConfig, string](parseConfig)(useConfig)
step2 := LocalIOResultK[string, string, ConfigFile](readFile)(step1)
step1 := LocalIOResultK[string](parseConfig)(useConfig)
step2 := LocalIOResultK[string](readFile)(step1)
// Success case
res := step2(ConfigFile{Path: "app.json"})()
@@ -619,3 +619,453 @@ func TestLocalIOResultK(t *testing.T) {
assert.True(t, result.IsLeft(resErr))
})
}
func TestChainFirstIOResultK(t *testing.T) {
type Config struct {
logEnabled bool
}
t.Run("Success - preserves original value", func(t *testing.T) {
sideEffectRan := false
logValue := func(v int) IOResult[string] {
return func() Result[string] {
sideEffectRan = true
return result.Of(fmt.Sprintf("Logged: %d", v))
}
}
pipeline := F.Pipe1(
Of[Config](42),
ChainFirstIOResultK[Config](logValue),
)
res := pipeline(Config{logEnabled: true})()
assert.Equal(t, result.Of(42), res)
assert.True(t, sideEffectRan)
})
t.Run("Failure - side effect fails", func(t *testing.T) {
sideEffectError := errors.New("side effect failed")
failingSideEffect := func(v int) IOResult[string] {
return func() Result[string] {
return result.Left[string](sideEffectError)
}
}
pipeline := F.Pipe1(
Of[Config](42),
ChainFirstIOResultK[Config](failingSideEffect),
)
res := pipeline(Config{})()
assert.True(t, E.IsLeft(res))
})
t.Run("Failure - original computation fails", func(t *testing.T) {
originalError := errors.New("original failed")
sideEffectRan := false
sideEffect := func(v int) IOResult[string] {
return func() Result[string] {
sideEffectRan = true
return result.Of("logged")
}
}
pipeline := F.Pipe1(
Left[Config, int](originalError),
ChainFirstIOResultK[Config](sideEffect),
)
res := pipeline(Config{})()
assert.Equal(t, result.Left[int](originalError), res)
assert.False(t, sideEffectRan)
})
}
func TestTapIOResultK(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of(fmt.Sprintf("Tapped: %d", v))
}
}
pipeline := F.Pipe1(
Of[Config](42),
TapIOResultK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
t.Run("Validation use case", func(t *testing.T) {
validatePositive := func(v int) IOResult[bool] {
return func() Result[bool] {
if v > 0 {
return result.Of(true)
}
return result.Left[bool](errors.New("must be positive"))
}
}
pipeline := F.Pipe1(
Of[Config](42),
TapIOResultK[Config](validatePositive),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
})
t.Run("Validation failure", func(t *testing.T) {
validatePositive := func(v int) IOResult[bool] {
return func() Result[bool] {
if v > 0 {
return result.Of(true)
}
return result.Left[bool](errors.New("must be positive"))
}
}
pipeline := F.Pipe1(
Of[Config](-5),
TapIOResultK[Config](validatePositive),
)
res := pipeline(Config{})()
assert.True(t, E.IsLeft(res))
})
}
func TestTap(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of(fmt.Sprintf("Tapped: %d", v))
}
}
}
pipeline := F.Pipe1(
Of[Config](42),
Tap(tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
t.Run("Tap is alias for ChainFirst", func(t *testing.T) {
sideEffect := func(v int) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Value: %d", v))
}
}
}
pipelineWithTap := F.Pipe1(
Of[Config](42),
Tap(sideEffect),
)
pipelineWithChainFirst := F.Pipe1(
Of[Config](42),
ChainFirst(sideEffect),
)
resultTap := pipelineWithTap(Config{})()
resultChainFirst := pipelineWithChainFirst(Config{})()
assert.Equal(t, resultChainFirst, resultTap)
})
}
func TestMonadTap(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of("tapped")
}
}
}
res := MonadTap(Of[Config](42), tapFunc)(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
}
func TestTapEitherK(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) Result[string] {
tapped = true
return result.Of(fmt.Sprintf("Tapped: %d", v))
}
pipeline := F.Pipe1(
Of[Config](42),
TapEitherK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
t.Run("Failure - tap fails", func(t *testing.T) {
tapError := errors.New("tap failed")
tapFunc := func(v int) Result[string] {
return result.Left[string](tapError)
}
pipeline := F.Pipe1(
Of[Config](42),
TapEitherK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.True(t, E.IsLeft(res))
})
}
func TestTapResultK(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) Result[string] {
tapped = true
return result.Of("tapped")
}
pipeline := F.Pipe1(
Of[Config](42),
TapResultK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
}
func TestTapReaderK(t *testing.T) {
type Config struct {
multiplier int
}
t.Run("Success - preserves original value with context", func(t *testing.T) {
tapped := false
tapFunc := func(v int) R.Reader[Config, string] {
return func(cfg Config) string {
tapped = true
return fmt.Sprintf("Value: %d, Multiplier: %d", v, cfg.multiplier)
}
}
pipeline := F.Pipe1(
Of[Config](42),
TapReaderK(tapFunc),
)
res := pipeline(Config{multiplier: 2})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
}
func TestTapIOK(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) IO[string] {
return func() string {
tapped = true
return fmt.Sprintf("Tapped: %d", v)
}
}
pipeline := F.Pipe1(
Of[Config](42),
TapIOK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
}
func TestRead(t *testing.T) {
type Config struct {
value int
}
t.Run("Success - provides environment", func(t *testing.T) {
computation := func(cfg Config) IOResult[int] {
return func() Result[int] {
return result.Of(cfg.value * 2)
}
}
res := Read[int](Config{value: 21})(computation)()
assert.Equal(t, result.Of(42), res)
})
t.Run("Failure - computation fails", func(t *testing.T) {
compError := errors.New("computation failed")
computation := func(cfg Config) IOResult[int] {
return func() Result[int] {
return result.Left[int](compError)
}
}
res := Read[int](Config{value: 21})(computation)()
assert.Equal(t, result.Left[int](compError), res)
})
}
func TestChainLeft(t *testing.T) {
type Config struct{}
t.Run("Right passes through unchanged", func(t *testing.T) {
pipeline := F.Pipe1(
Right[Config](42),
ChainLeft(func(err error) ReaderIOResult[Config, int] {
return Left[Config, int](errors.New("should not run"))
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
})
t.Run("Left transforms error", func(t *testing.T) {
originalError := errors.New("original")
newError := errors.New("transformed")
pipeline := F.Pipe1(
Left[Config, int](originalError),
ChainLeft(func(err error) ReaderIOResult[Config, int] {
return Left[Config, int](newError)
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Left[int](newError), res)
})
t.Run("Left recovers to Right", func(t *testing.T) {
pipeline := F.Pipe1(
Left[Config, int](errors.New("error")),
ChainLeft(func(err error) ReaderIOResult[Config, int] {
return Right[Config](99)
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(99), res)
})
}
func TestTapLeft(t *testing.T) {
type Config struct{}
t.Run("Right does not call function", func(t *testing.T) {
tapped := false
pipeline := F.Pipe1(
Right[Config](42),
TapLeft[int](func(err error) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of("logged")
}
}
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.False(t, tapped)
})
t.Run("Left calls function but preserves error", func(t *testing.T) {
tapped := false
originalError := errors.New("original error")
pipeline := F.Pipe1(
Left[Config, int](originalError),
TapLeft[int](func(err error) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of("side effect done")
}
}
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Left[int](originalError), res)
assert.True(t, tapped)
})
}
func TestTapLeftIOK(t *testing.T) {
type Config struct{}
t.Run("Left calls IO function but preserves error", func(t *testing.T) {
tapped := false
originalError := errors.New("original error")
pipeline := F.Pipe1(
Left[Config, int](originalError),
TapLeftIOK[int, Config](func(err error) IO[string] {
return func() string {
tapped = true
return "logged error"
}
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Left[int](originalError), res)
assert.True(t, tapped)
})
}

View File

@@ -38,51 +38,75 @@ import (
"github.com/IBM/fp-go/v2/readeroption"
)
// FromReaderOption converts a ReaderOption to a ReaderReaderIOEither.
// If the ReaderOption is None, the provided function is called to produce the error.
//
//go:inline
func FromReaderOption[R, C, A, E any](onNone Lazy[E]) Kleisli[R, C, E, ReaderOption[R, A], A] {
return reader.Map[R](RIOE.FromOption[C, A](onNone))
}
// FromReaderIOEither lifts a ReaderIOEither into a ReaderReaderIOEither context.
//
//go:inline
func FromReaderIOEither[C, E, R, A any](ma ReaderIOEither[R, E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.FromIOEither[C])
}
// FromReaderIO lifts a ReaderIO into a ReaderReaderIOEither, placing the result in the Right side.
//
//go:inline
func FromReaderIO[C, E, R, A any](ma ReaderIO[R, A]) ReaderReaderIOEither[R, C, E, A] {
return RightReaderIO[C, E](ma)
}
// RightReaderIO lifts a ReaderIO into a ReaderReaderIOEither, placing the result in the Right side.
//
//go:inline
func RightReaderIO[C, E, R, A any](ma ReaderIO[R, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.RightIO[C, E, A])
}
// LeftReaderIO lifts a ReaderIO into a ReaderReaderIOEither, placing the result in the Left (error) side.
//
//go:inline
func LeftReaderIO[C, A, R, E any](me ReaderIO[R, E]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(me, RIOE.LeftIO[C, A, E])
}
// MonadMap applies a function to the value inside a ReaderReaderIOEither context.
// If the computation is successful (Right), the function is applied to the value.
//
//go:inline
func MonadMap[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f func(A) B) ReaderReaderIOEither[R, C, E, B] {
return reader.MonadMap(fa, RIOE.Map[C, E](f))
}
// Map returns a function that applies a transformation to the success value.
// This is the curried version of MonadMap.
//
//go:inline
func Map[R, C, E, A, B any](f func(A) B) Operator[R, C, E, A, B] {
return reader.Map[R](RIOE.Map[C, E](f))
}
// MonadMapTo replaces the success value with a constant value.
//
//go:inline
func MonadMapTo[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], b B) ReaderReaderIOEither[R, C, E, B] {
return reader.MonadMap(fa, RIOE.MapTo[C, E, A](b))
}
// MapTo returns a function that replaces the success value with a constant.
// This is the curried version of MonadMapTo.
//
//go:inline
func MapTo[R, C, E, A, B any](b B) Operator[R, C, E, A, B] {
return reader.Map[R](RIOE.MapTo[C, E, A](b))
}
// MonadChain sequences two computations where the second depends on the result of the first.
//
//go:inline
func MonadChain[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadChain(
@@ -92,6 +116,9 @@ func MonadChain[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisl
)
}
// MonadChainFirst sequences two computations but keeps the result of the first.
// Useful for performing side effects while preserving the original value.
//
//go:inline
func MonadChainFirst[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return chain.MonadChainFirst(
@@ -101,11 +128,15 @@ func MonadChainFirst[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f K
f)
}
// MonadTap is an alias for MonadChainFirst, executing a side effect while preserving the original value.
//
//go:inline
func MonadTap[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirst(fa, f)
}
// MonadChainEitherK chains a computation that returns an Either into a ReaderReaderIOEither.
//
//go:inline
func MonadChainEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromeither.MonadChainEitherK(
@@ -116,6 +147,9 @@ func MonadChainEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f
)
}
// ChainEitherK returns a function that chains an Either-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, B] {
return fromeither.ChainEitherK(
@@ -125,6 +159,8 @@ func ChainEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E
)
}
// MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
//
//go:inline
func MonadChainFirstEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromeither.MonadChainFirstEitherK(
@@ -136,11 +172,16 @@ func MonadChainFirstEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E,
)
}
// MonadTapEitherK is an alias for MonadChainFirstEitherK, executing an Either side effect while preserving the original value.
//
//go:inline
func MonadTapEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstEitherK(ma, f)
}
// ChainFirstEitherK returns a function that chains an Either computation while preserving the original value.
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, A] {
return fromeither.ChainFirstEitherK(
@@ -151,11 +192,15 @@ func ChainFirstEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R,
)
}
// TapEitherK is an alias for ChainFirstEitherK, executing an Either side effect while preserving the original value.
//
//go:inline
func TapEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, A] {
return ChainFirstEitherK[R, C](f)
}
// MonadChainReaderK chains a Reader-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
@@ -166,6 +211,9 @@ func MonadChainReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f
)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
@@ -175,6 +223,8 @@ func ChainReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E
)
}
// MonadChainFirstReaderK chains a Reader computation but keeps the original value.
//
//go:inline
func MonadChainFirstReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -185,11 +235,16 @@ func MonadChainFirstReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E,
)
}
// MonadTapReaderK is an alias for MonadChainFirstReaderK, executing a Reader side effect while preserving the original value.
//
//go:inline
func MonadTapReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderK(ma, f)
}
// ChainFirstReaderK returns a function that chains a Reader computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderK.
//
//go:inline
func ChainFirstReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -199,11 +254,15 @@ func ChainFirstReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R,
)
}
// TapReaderK is an alias for ChainFirstReaderK, executing a Reader side effect while preserving the original value.
//
//go:inline
func TapReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderK[C, E](f)
}
// MonadChainReaderIOK chains a ReaderIO-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
@@ -214,6 +273,9 @@ func MonadChainReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A],
)
}
// ChainReaderIOK returns a function that chains a ReaderIO-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainReaderIOK.
//
//go:inline
func ChainReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
@@ -223,6 +285,8 @@ func ChainReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R,
)
}
// MonadChainFirstReaderIOK chains a ReaderIO computation but keeps the original value.
//
//go:inline
func MonadChainFirstReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -233,11 +297,16 @@ func MonadChainFirstReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E
)
}
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK, executing a ReaderIO side effect while preserving the original value.
//
//go:inline
func MonadTapReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderIOK(ma, f)
}
// ChainFirstReaderIOK returns a function that chains a ReaderIO computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderIOK.
//
//go:inline
func ChainFirstReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -247,11 +316,15 @@ func ChainFirstReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operato
)
}
// TapReaderIOK is an alias for ChainFirstReaderIOK, executing a ReaderIO side effect while preserving the original value.
//
//go:inline
func TapReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderIOK[C, E](f)
}
// MonadChainReaderEitherK chains a ReaderEither-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
@@ -262,6 +335,9 @@ func MonadChainReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E,
)
}
// ChainReaderEitherK returns a function that chains a ReaderEither-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainReaderEitherK.
//
//go:inline
func ChainReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
@@ -271,6 +347,8 @@ func ChainReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R,
)
}
// ChainReaderIOEitherK returns a function that chains a ReaderIOEither-returning function into ReaderReaderIOEither.
//
//go:inline
func ChainReaderIOEitherK[C, R, E, A, B any](f RIOE.Kleisli[R, E, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
@@ -280,6 +358,8 @@ func ChainReaderIOEitherK[C, R, E, A, B any](f RIOE.Kleisli[R, E, A, B]) Operato
)
}
// MonadChainFirstReaderEitherK chains a ReaderEither computation but keeps the original value.
//
//go:inline
func MonadChainFirstReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -290,11 +370,16 @@ func MonadChainFirstReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R,
)
}
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK, executing a ReaderEither side effect while preserving the original value.
//
//go:inline
func MonadTapReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderEitherK(ma, f)
}
// ChainFirstReaderEitherK returns a function that chains a ReaderEither computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderEitherK.
//
//go:inline
func ChainFirstReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -304,11 +389,15 @@ func ChainFirstReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operat
)
}
// TapReaderEitherK is an alias for ChainFirstReaderEitherK, executing a ReaderEither side effect while preserving the original value.
//
//go:inline
func TapReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderEitherK[C](f)
}
// ChainReaderOptionK returns a function that chains a ReaderOption-returning function into ReaderReaderIOEither.
// If the ReaderOption is None, the provided error function is called.
func ChainReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
fro := FromReaderOption[R, C, B](onNone)
@@ -323,6 +412,8 @@ func ChainReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kle
}
}
// ChainFirstReaderOptionK chains a ReaderOption computation while preserving the original value.
// If the ReaderOption is None, the provided error function is called.
func ChainFirstReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
fro := FromReaderOption[R, C, B](onNone)
return func(f readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
@@ -334,11 +425,15 @@ func ChainFirstReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroptio
}
}
// TapReaderOptionK is an alias for ChainFirstReaderOptionK, executing a ReaderOption side effect while preserving the original value.
//
//go:inline
func TapReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderOptionK[R, C, A, B](onNone)
}
// MonadChainIOEitherK chains an IOEither-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainIOEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f IOE.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromioeither.MonadChainIOEitherK(
@@ -349,6 +444,9 @@ func MonadChainIOEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A],
)
}
// ChainIOEitherK returns a function that chains an IOEither-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainIOEitherK.
//
//go:inline
func ChainIOEitherK[R, C, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, C, E, A, B] {
return fromioeither.ChainIOEitherK(
@@ -358,6 +456,8 @@ func ChainIOEitherK[R, C, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, C, E,
)
}
// MonadChainIOK chains an IO-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromio.MonadChainIOK(
@@ -368,6 +468,9 @@ func MonadChainIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.
)
}
// ChainIOK returns a function that chains an IO-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainIOK.
//
//go:inline
func ChainIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, B] {
return fromio.ChainIOK(
@@ -377,6 +480,8 @@ func ChainIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, B] {
)
}
// MonadChainFirstIOK chains an IO computation but keeps the original value.
//
//go:inline
func MonadChainFirstIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromio.MonadChainFirstIOK(
@@ -388,11 +493,16 @@ func MonadChainFirstIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A],
)
}
// MonadTapIOK is an alias for MonadChainFirstIOK, executing an IO side effect while preserving the original value.
//
//go:inline
func MonadTapIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstIOK(ma, f)
}
// ChainFirstIOK returns a function that chains an IO computation while preserving the original value.
// This is the curried version of MonadChainFirstIOK.
//
//go:inline
func ChainFirstIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, A] {
return fromio.ChainFirstIOK(
@@ -403,11 +513,16 @@ func ChainFirstIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, A
)
}
// TapIOK is an alias for ChainFirstIOK, executing an IO side effect while preserving the original value.
//
//go:inline
func TapIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, A] {
return ChainFirstIOK[R, C, E](f)
}
// ChainOptionK returns a function that chains an Option-returning function into ReaderReaderIOEither.
// If the Option is None, the provided error function is called.
//
//go:inline
func ChainOptionK[R, C, A, B, E any](onNone Lazy[E]) func(option.Kleisli[A, B]) Operator[R, C, E, A, B] {
return fromeither.ChainOptionK(
@@ -417,6 +532,8 @@ func ChainOptionK[R, C, A, B, E any](onNone Lazy[E]) func(option.Kleisli[A, B])
)
}
// MonadAp applies a function wrapped in a context to a value wrapped in a context.
//
//go:inline
func MonadAp[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
@@ -429,6 +546,8 @@ func MonadAp[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa
)
}
// MonadApSeq applies a function in a context to a value in a context, executing them sequentially.
//
//go:inline
func MonadApSeq[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
@@ -441,6 +560,8 @@ func MonadApSeq[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B],
)
}
// MonadApPar applies a function in a context to a value in a context, executing them in parallel.
//
//go:inline
func MonadApPar[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
@@ -453,6 +574,9 @@ func MonadApPar[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B],
)
}
// Ap returns a function that applies a function in a context to a value in a context.
// This is the curried version of MonadAp.
//
//go:inline
func Ap[B, R, C, E, A any](fa ReaderReaderIOEither[R, C, E, A]) Operator[R, C, E, func(A) B, B] {
return readert.Ap[
@@ -464,6 +588,9 @@ func Ap[B, R, C, E, A any](fa ReaderReaderIOEither[R, C, E, A]) Operator[R, C, E
)
}
// Chain returns a function that sequences computations where the second depends on the first.
// This is the curried version of MonadChain.
//
//go:inline
func Chain[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, B] {
return readert.Chain[ReaderReaderIOEither[R, C, E, A]](
@@ -472,6 +599,9 @@ func Chain[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, B]
)
}
// ChainFirst returns a function that sequences computations but keeps the first result.
// This is the curried version of MonadChainFirst.
//
//go:inline
func ChainFirst[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, A] {
return chain.ChainFirst(
@@ -480,96 +610,137 @@ func ChainFirst[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A
f)
}
// Tap is an alias for ChainFirst, executing a side effect while preserving the original value.
//
//go:inline
func Tap[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, A] {
return ChainFirst(f)
}
// Right creates a successful ReaderReaderIOEither with the given value.
//
//go:inline
func Right[R, C, E, A any](a A) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.Right[C, E](a))
}
// Left creates a failed ReaderReaderIOEither with the given error.
//
//go:inline
func Left[R, C, A, E any](e E) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.Left[C, A](e))
}
// Of creates a successful ReaderReaderIOEither with the given value.
// This is the pointed functor operation.
//
//go:inline
func Of[R, C, E, A any](a A) ReaderReaderIOEither[R, C, E, A] {
return Right[R, C, E](a)
}
// Flatten removes one level of nesting from a nested ReaderReaderIOEither.
//
//go:inline
func Flatten[R, C, E, A any](mma ReaderReaderIOEither[R, C, E, ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return MonadChain(mma, function.Identity[ReaderReaderIOEither[R, C, E, A]])
}
// FromEither lifts an Either into a ReaderReaderIOEither context.
//
//go:inline
func FromEither[R, C, E, A any](t Either[E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.FromEither[C](t))
}
// RightReader lifts a Reader into a ReaderReaderIOEither, placing the result in the Right side.
//
//go:inline
func RightReader[C, E, R, A any](ma Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.Right[C, E])
}
// LeftReader lifts a Reader into a ReaderReaderIOEither, placing the result in the Left (error) side.
//
//go:inline
func LeftReader[C, A, R, E any](ma Reader[R, E]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.Left[C, A])
}
// FromReader lifts a Reader into a ReaderReaderIOEither context.
//
//go:inline
func FromReader[C, E, R, A any](ma Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return RightReader[C, E](ma)
}
// RightIO lifts an IO into a ReaderReaderIOEither, placing the result in the Right side.
//
//go:inline
func RightIO[R, C, E, A any](ma IO[A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.RightIO[C, E](ma))
}
// LeftIO lifts an IO into a ReaderReaderIOEither, placing the result in the Left (error) side.
//
//go:inline
func LeftIO[R, C, A, E any](ma IO[E]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.LeftIO[C, A](ma))
}
// FromIO lifts an IO into a ReaderReaderIOEither context.
//
//go:inline
func FromIO[R, C, E, A any](ma IO[A]) ReaderReaderIOEither[R, C, E, A] {
return RightIO[R, C, E](ma)
}
// FromIOEither lifts an IOEither into a ReaderReaderIOEither context.
//
//go:inline
func FromIOEither[R, C, E, A any](ma IOEither[E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.FromIOEither[C](ma))
}
// FromReaderEither lifts a ReaderEither into a ReaderReaderIOEither context.
//
//go:inline
func FromReaderEither[R, C, E, A any](ma RE.ReaderEither[R, E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.FromEither[C])
}
// Ask returns a ReaderReaderIOEither that retrieves the outer context.
//
//go:inline
func Ask[R, C, E any]() ReaderReaderIOEither[R, C, E, R] {
return fromreader.Ask(FromReader[C, E, R, R])()
}
// Asks returns a ReaderReaderIOEither that retrieves a value derived from the outer context.
//
//go:inline
func Asks[C, E, R, A any](r Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.Asks(FromReader[C, E, R, A])(r)
}
// FromOption converts an Option to a ReaderReaderIOEither.
// If the Option is None, the provided function is called to produce the error.
//
//go:inline
func FromOption[R, C, A, E any](onNone Lazy[E]) func(Option[A]) ReaderReaderIOEither[R, C, E, A] {
return fromeither.FromOption(FromEither[R, C, E, A], onNone)
}
// FromPredicate creates a ReaderReaderIOEither from a predicate.
// If the predicate returns false, the onFalse function is called to produce the error.
//
//go:inline
func FromPredicate[R, C, E, A any](pred func(A) bool, onFalse func(A) E) func(A) ReaderReaderIOEither[R, C, E, A] {
return fromeither.FromPredicate(FromEither[R, C, E, A], pred, onFalse)
}
// MonadAlt tries the first computation, and if it fails, tries the second.
//
//go:inline
func MonadAlt[R, C, E, A any](first ReaderReaderIOEither[R, C, E, A], second Lazy[ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return func(r R) ReaderIOEither[C, E, A] {
@@ -579,36 +750,53 @@ func MonadAlt[R, C, E, A any](first ReaderReaderIOEither[R, C, E, A], second Laz
}
}
// Alt returns a function that tries an alternative computation if the first fails.
// This is the curried version of MonadAlt.
//
//go:inline
func Alt[R, C, E, A any](second Lazy[ReaderReaderIOEither[R, C, E, A]]) Operator[R, C, E, A, A] {
return function.Bind2nd(MonadAlt, second)
}
// MonadFlap applies a value to a function wrapped in a context.
//
//go:inline
func MonadFlap[R, C, E, B, A any](fab ReaderReaderIOEither[R, C, E, func(A) B], a A) ReaderReaderIOEither[R, C, E, B] {
return functor.MonadFlap(MonadMap[R, C, E, func(A) B, B], fab, a)
}
// Flap returns a function that applies a fixed value to a function in a context.
// This is the curried version of MonadFlap.
//
//go:inline
func Flap[R, C, E, B, A any](a A) Operator[R, C, E, func(A) B, B] {
return functor.Flap(Map[R, C, E, func(A) B, B], a)
}
// MonadMapLeft applies a function to the error value, leaving success unchanged.
//
//go:inline
func MonadMapLeft[R, C, E1, E2, A any](fa ReaderReaderIOEither[R, C, E1, A], f func(E1) E2) ReaderReaderIOEither[R, C, E2, A] {
return reader.MonadMap(fa, RIOE.MapLeft[C, A](f))
}
// MapLeft returns a function that transforms the error channel.
// This is the curried version of MonadMapLeft.
//
//go:inline
func MapLeft[R, C, A, E1, E2 any](f func(E1) E2) func(ReaderReaderIOEither[R, C, E1, A]) ReaderReaderIOEither[R, C, E2, A] {
return reader.Map[R](RIOE.MapLeft[C, A](f))
}
// Read executes a ReaderReaderIOEither by providing a concrete outer environment value.
//
//go:inline
func Read[C, E, A, R any](r R) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return reader.Read[ReaderIOEither[C, E, A]](r)
}
// ReadIOEither executes a ReaderReaderIOEither by providing an outer environment obtained from an IOEither.
//
//go:inline
func ReadIOEither[A, R, C, E any](rio IOEither[E, R]) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return func(rri ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
@@ -623,6 +811,8 @@ func ReadIOEither[A, R, C, E any](rio IOEither[E, R]) func(ReaderReaderIOEither[
}
}
// ReadIO executes a ReaderReaderIOEither by providing an outer environment obtained from an IO.
//
//go:inline
func ReadIO[C, E, A, R any](rio IO[R]) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return func(rri ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
@@ -637,6 +827,8 @@ func ReadIO[C, E, A, R any](rio IO[R]) func(ReaderReaderIOEither[R, C, E, A]) Re
}
}
// MonadChainLeft chains a computation on the error channel, allowing error recovery or transformation.
//
//go:inline
func MonadChainLeft[R, C, EA, EB, A any](fa ReaderReaderIOEither[R, C, EA, A], f Kleisli[R, C, EB, EA, A]) ReaderReaderIOEither[R, C, EB, A] {
return readert.MonadChain(
@@ -646,6 +838,9 @@ func MonadChainLeft[R, C, EA, EB, A any](fa ReaderReaderIOEither[R, C, EA, A], f
)
}
// ChainLeft returns a function that chains a computation on the error channel.
// This is the curried version of MonadChainLeft.
//
//go:inline
func ChainLeft[R, C, EA, EB, A any](f Kleisli[R, C, EB, EA, A]) func(ReaderReaderIOEither[R, C, EA, A]) ReaderReaderIOEither[R, C, EB, A] {
return readert.Chain[ReaderReaderIOEither[R, C, EA, A]](
@@ -654,16 +849,22 @@ func ChainLeft[R, C, EA, EB, A any](f Kleisli[R, C, EB, EA, A]) func(ReaderReade
)
}
// Delay creates an operation that passes in the value after some delay.
//
//go:inline
func Delay[R, C, E, A any](delay time.Duration) Operator[R, C, E, A, A] {
return reader.Map[R](RIOE.Delay[C, E, A](delay))
}
// After creates an operation that passes after the given time.Time.
//
//go:inline
func After[R, C, E, A any](timestamp time.Time) Operator[R, C, E, A, A] {
return reader.Map[R](RIOE.After[C, E, A](timestamp))
}
// Defer creates a ReaderReaderIOEither lazily via a generator function.
// The generator is called each time the ReaderReaderIOEither is executed.
func Defer[R, C, E, A any](fa Lazy[ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return func(r R) ReaderIOEither[C, E, A] {
return func(c C) RIOE.IOEither[E, A] {

View File

@@ -91,7 +91,7 @@ func TestMonadMapTo(t *testing.T) {
func TestChain(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
Chain[OuterConfig, InnerConfig, error](func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
Chain(func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
return Of[OuterConfig, InnerConfig, error](fmt.Sprintf("%d", v))
}),
)
@@ -193,7 +193,7 @@ func TestFromEither(t *testing.T) {
t.Run("Left", func(t *testing.T) {
err := errors.New("test error")
result := FromEither[OuterConfig, InnerConfig, error, int](E.Left[int](err))
result := FromEither[OuterConfig, InnerConfig](E.Left[int](err))
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
})
}
@@ -239,7 +239,7 @@ func TestLeftIO(t *testing.T) {
func TestFromIOEither(t *testing.T) {
t.Run("Right", func(t *testing.T) {
ioe := IOE.Right[error](42)
result := FromIOEither[OuterConfig, InnerConfig, error](ioe)
result := FromIOEither[OuterConfig, InnerConfig](ioe)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
})
@@ -344,7 +344,7 @@ func TestFromPredicate(t *testing.T) {
})
t.Run("Predicate false", func(t *testing.T) {
result := FromPredicate[OuterConfig, InnerConfig, error](isPositive, onFalse)(-5)
result := FromPredicate[OuterConfig, InnerConfig](isPositive, onFalse)(-5)
expected := E.Left[int](fmt.Errorf("not positive: -5"))
assert.Equal(t, expected, result(OuterConfig{})(InnerConfig{})())
})
@@ -391,7 +391,7 @@ func TestRead(t *testing.T) {
func TestChainEitherK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainEitherK[OuterConfig, InnerConfig, error](func(v int) E.Either[error, string] {
ChainEitherK[OuterConfig, InnerConfig](func(v int) E.Either[error, string] {
return E.Right[error](fmt.Sprintf("%d", v))
}),
)

View File

@@ -6,9 +6,62 @@ This folder is meant to contain examples that illustrate how to use the library.
[![introduction to fp-go](presentation/cover.jpg)](https://www.youtube.com/watch?v=Jif3jL6DRdw "introduction to fp-go")
### References
## External Documentation References
- [Ryan's Blog](https://rlee.dev/practical-guide-to-fp-ts-part-1) - practical introduction into FP concepts
- [Investigate Functional Programming Concepts in Go](https://betterprogramming.pub/investigate-functional-programming-concepts-in-go-1dada09bc913) - discussion around FP concepts in golang
- [Investigating the I/O Monad in Go](https://medium.com/better-programming/investigating-the-i-o-monad-in-go-3c0fabbb4b3d) - a closer look at I/O monads in golang
-
### Official Documentation
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
- [Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24) - Information about generic type aliases
- [Go Blog: Generating code](https://go.dev/blog/generate) - Using `go generate`
- [Go Context Package](https://pkg.go.dev/context) - Standard library context documentation
### Functional Programming Concepts
#### Introductory Resources
- [Ryan's Blog](https://rlee.dev/practical-guide-to-fp-ts-part-1) - Practical introduction into FP concepts
- [Investigate Functional Programming Concepts in Go](https://betterprogramming.pub/investigate-functional-programming-concepts-in-go-1dada09bc913) - Discussion around FP concepts in golang
- [Investigating the I/O Monad in Go](https://medium.com/better-programming/investigating-the-i-o-monad-in-go-3c0fabbb4b3d) - A closer look at I/O monads in golang
- [Professor Frisby's Mostly Adequate Guide](https://github.com/MostlyAdequate/mostly-adequate-guide) - Comprehensive FP guide
- [mostly-adequate-fp-ts](https://github.com/ChuckJonas/mostly-adequate-fp-ts/) - TypeScript companion to Frisby's guide
#### Currying and Function Composition
- [Mostly Adequate Guide - Ch. 4: Currying](https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch04) - Excellent introduction with clear examples
- [Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983) by Eric Elliott
- [Why Curry Helps](https://hughfdjackson.com/javascript/why-curry-helps/) - Practical benefits of currying
### Haskell and Type Theory
- [Haskell Wiki - Currying](https://wiki.haskell.org/Currying) - Comprehensive explanation of currying in Haskell
- [Learn You a Haskell - Higher Order Functions](http://learnyouahaskell.com/higher-order-functions) - Introduction to currying and partial application
- [Haskell's Prelude](https://hackage.haskell.org/package/base/docs/Prelude.html) - Standard library showing data-last convention
- [Haskell Pair Type](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html) - Haskell definition of Pair
- [Haskell Lens Library](https://hackage.haskell.org/package/lens) - Pioneering optics library
### Optics
- [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
### Related Libraries
- [fp-ts](https://github.com/gcanti/fp-ts) - TypeScript library that inspired fp-go
- [fp-ts Documentation](https://gcanti.github.io/fp-ts/) - TypeScript library documentation
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
- [urfave/cli/v3](https://github.com/urfave/cli) - Underlying CLI framework
### Project Resources
- [GitHub Repository](https://github.com/IBM/fp-go) - Source code and issues
- [Coverage Status](https://coveralls.io/github/IBM/fp-go?branch=main) - Test coverage reports
- [Go Report Card](https://goreportcard.com/report/github.com/IBM/fp-go/v2) - Code quality metrics
- [Apache License 2.0](https://github.com/IBM/fp-go/blob/main/LICENSE) - Project license
### Internal Documentation
- [DESIGN.md](../DESIGN.md) - Design philosophy and patterns
- [IDIOMATIC_COMPARISON.md](../IDIOMATIC_COMPARISON.md) - Performance comparison between standard and idiomatic packages
- [Optics Overview](../optics/README.md) - Complete guide to lenses, prisms, and other optics
- [CLI Package](../cli/README.md) - Command-line interface utilities
- [ReaderResult Package](../idiomatic/context/readerresult/README.md) - Context-aware result handling

View File

@@ -123,3 +123,78 @@ func Last[A any]() Semigroup[A] {
func ToMagma[A any](s Semigroup[A]) M.Magma[A] {
return s
}
// ConcatWith creates a curried version of the Concat operation with the left argument fixed first.
// It returns a function that takes the left operand and returns another function that takes
// the right operand and performs the concatenation.
//
// This is useful for partial application and function composition patterns.
//
// # Type Parameters
//
// - A: The type of elements in the semigroup
//
// # Parameters
//
// - s: The semigroup to use for concatenation
//
// # Returns
//
// - func(A) func(A) A: A curried function that takes left then right operand
//
// # Example Usage
//
// import N "github.com/IBM/fp-go/v2/number"
// sum := N.SemigroupSum[int]()
// concatWith := ConcatWith(sum)
// add5 := concatWith(5)
// result := add5(3) // 5 + 3 = 8
//
// # See Also
//
// - AppendTo: Similar but fixes the right argument first
func ConcatWith[A any](s Semigroup[A]) func(A) func(A) A {
return func(l A) func(A) A {
return func(r A) A {
return s.Concat(l, r)
}
}
}
// AppendTo creates a curried version of the Concat operation with the right argument fixed first.
// It returns a function that takes the right operand and returns another function that takes
// the left operand and performs the concatenation.
//
// This is useful for partial application where you want to fix the second argument first,
// which is common in append-style operations.
//
// # Type Parameters
//
// - A: The type of elements in the semigroup
//
// # Parameters
//
// - s: The semigroup to use for concatenation
//
// # Returns
//
// - func(A) func(A) A: A curried function that takes right then left operand
//
// # Example Usage
//
// import S "github.com/IBM/fp-go/v2/string"
// strConcat := S.Semigroup
// appendTo := AppendTo(strConcat)
// addSuffix := appendTo("!")
// result := addSuffix("Hello") // "Hello" + "!" = "Hello!"
//
// # See Also
//
// - ConcatWith: Similar but fixes the left argument first
func AppendTo[A any](s Semigroup[A]) func(A) func(A) A {
return func(r A) func(A) A {
return func(l A) A {
return s.Concat(l, r)
}
}
}

View File

@@ -444,3 +444,261 @@ func BenchmarkFunctionSemigroup(b *testing.B) {
combined("hello")
}
}
// Test ConcatWith function
func TestConcatWith(t *testing.T) {
t.Run("with integer addition", func(t *testing.T) {
add := MakeSemigroup(func(a, b int) int { return a + b })
concatWith := ConcatWith(add)
// Fix left operand to 5
add5 := concatWith(5)
assert.Equal(t, 8, add5(3)) // 5 + 3 = 8
assert.Equal(t, 15, add5(10)) // 5 + 10 = 15
assert.Equal(t, 5, add5(0)) // 5 + 0 = 5
})
t.Run("with string concatenation", func(t *testing.T) {
concat := MakeSemigroup(func(a, b string) string { return a + b })
concatWith := ConcatWith(concat)
// Fix left operand to "Hello, "
greet := concatWith("Hello, ")
assert.Equal(t, "Hello, World", greet("World"))
assert.Equal(t, "Hello, Bob", greet("Bob"))
assert.Equal(t, "Hello, ", greet(""))
})
t.Run("with subtraction (non-commutative)", func(t *testing.T) {
sub := MakeSemigroup(func(a, b int) int { return a - b })
concatWith := ConcatWith(sub)
// Fix left operand to 10
subtract10 := concatWith(10)
assert.Equal(t, 7, subtract10(3)) // 10 - 3 = 7
assert.Equal(t, 5, subtract10(5)) // 10 - 5 = 5
assert.Equal(t, -5, subtract10(15)) // 10 - 15 = -5
})
t.Run("with First semigroup", func(t *testing.T) {
first := First[int]()
concatWith := ConcatWith(first)
// Fix left operand to 42
always42 := concatWith(42)
assert.Equal(t, 42, always42(1))
assert.Equal(t, 42, always42(100))
assert.Equal(t, 42, always42(0))
})
t.Run("with Last semigroup", func(t *testing.T) {
last := Last[string]()
concatWith := ConcatWith(last)
// Fix left operand to "first"
alwaysSecond := concatWith("first")
assert.Equal(t, "second", alwaysSecond("second"))
assert.Equal(t, "other", alwaysSecond("other"))
assert.Equal(t, "", alwaysSecond(""))
})
t.Run("currying behavior", func(t *testing.T) {
mul := MakeSemigroup(func(a, b int) int { return a * b })
concatWith := ConcatWith(mul)
// Create multiple partially applied functions
double := concatWith(2)
triple := concatWith(3)
quadruple := concatWith(4)
assert.Equal(t, 10, double(5)) // 2 * 5 = 10
assert.Equal(t, 15, triple(5)) // 3 * 5 = 15
assert.Equal(t, 20, quadruple(5)) // 4 * 5 = 20
})
t.Run("with complex types", func(t *testing.T) {
type Point struct {
X, Y int
}
pointAdd := MakeSemigroup(func(a, b Point) Point {
return Point{X: a.X + b.X, Y: a.Y + b.Y}
})
concatWith := ConcatWith(pointAdd)
// Fix left operand to origin offset
offset := concatWith(Point{X: 10, Y: 20})
assert.Equal(t, Point{X: 15, Y: 25}, offset(Point{X: 5, Y: 5}))
assert.Equal(t, Point{X: 10, Y: 20}, offset(Point{X: 0, Y: 0}))
})
}
// Test AppendTo function
func TestAppendTo(t *testing.T) {
t.Run("with integer addition", func(t *testing.T) {
add := MakeSemigroup(func(a, b int) int { return a + b })
appendTo := AppendTo(add)
// Fix right operand to 5
addTo5 := appendTo(5)
assert.Equal(t, 8, addTo5(3)) // 3 + 5 = 8
assert.Equal(t, 15, addTo5(10)) // 10 + 5 = 15
assert.Equal(t, 5, addTo5(0)) // 0 + 5 = 5
})
t.Run("with string concatenation", func(t *testing.T) {
concat := MakeSemigroup(func(a, b string) string { return a + b })
appendTo := AppendTo(concat)
// Fix right operand to "!"
addExclamation := appendTo("!")
assert.Equal(t, "Hello!", addExclamation("Hello"))
assert.Equal(t, "World!", addExclamation("World"))
assert.Equal(t, "!", addExclamation(""))
})
t.Run("with subtraction (non-commutative)", func(t *testing.T) {
sub := MakeSemigroup(func(a, b int) int { return a - b })
appendTo := AppendTo(sub)
// Fix right operand to 3
subtract3 := appendTo(3)
assert.Equal(t, 7, subtract3(10)) // 10 - 3 = 7
assert.Equal(t, 2, subtract3(5)) // 5 - 3 = 2
assert.Equal(t, -3, subtract3(0)) // 0 - 3 = -3
assert.Equal(t, -8, subtract3(-5)) // -5 - 3 = -8
})
t.Run("with First semigroup", func(t *testing.T) {
first := First[string]()
appendTo := AppendTo(first)
// Fix right operand to "second"
alwaysFirst := appendTo("second")
assert.Equal(t, "first", alwaysFirst("first"))
assert.Equal(t, "other", alwaysFirst("other"))
assert.Equal(t, "", alwaysFirst(""))
})
t.Run("with Last semigroup", func(t *testing.T) {
last := Last[int]()
appendTo := AppendTo(last)
// Fix right operand to 42
always42 := appendTo(42)
assert.Equal(t, 42, always42(1))
assert.Equal(t, 42, always42(100))
assert.Equal(t, 42, always42(0))
})
t.Run("currying behavior", func(t *testing.T) {
mul := MakeSemigroup(func(a, b int) int { return a * b })
appendTo := AppendTo(mul)
// Create multiple partially applied functions
multiplyBy2 := appendTo(2)
multiplyBy3 := appendTo(3)
multiplyBy4 := appendTo(4)
assert.Equal(t, 10, multiplyBy2(5)) // 5 * 2 = 10
assert.Equal(t, 15, multiplyBy3(5)) // 5 * 3 = 15
assert.Equal(t, 20, multiplyBy4(5)) // 5 * 4 = 20
})
t.Run("with complex types", func(t *testing.T) {
type Point struct {
X, Y int
}
pointAdd := MakeSemigroup(func(a, b Point) Point {
return Point{X: a.X + b.X, Y: a.Y + b.Y}
})
appendTo := AppendTo(pointAdd)
// Fix right operand to offset
addOffset := appendTo(Point{X: 10, Y: 20})
assert.Equal(t, Point{X: 15, Y: 25}, addOffset(Point{X: 5, Y: 5}))
assert.Equal(t, Point{X: 10, Y: 20}, addOffset(Point{X: 0, Y: 0}))
})
}
// Test ConcatWith vs AppendTo difference
func TestConcatWithVsAppendTo(t *testing.T) {
t.Run("demonstrates order difference with non-commutative operation", func(t *testing.T) {
sub := MakeSemigroup(func(a, b int) int { return a - b })
concatWith := ConcatWith(sub)
appendTo := AppendTo(sub)
// ConcatWith fixes left operand first
subtract10From := concatWith(10) // 10 - x
assert.Equal(t, 7, subtract10From(3)) // 10 - 3 = 7
// AppendTo fixes right operand first
subtract3From := appendTo(3) // x - 3
assert.Equal(t, 7, subtract3From(10)) // 10 - 3 = 7
// Same result but different partial application order
assert.Equal(t, subtract10From(3), subtract3From(10))
})
t.Run("demonstrates order difference with string concatenation", func(t *testing.T) {
concat := MakeSemigroup(func(a, b string) string { return a + b })
concatWith := ConcatWith(concat)
appendTo := AppendTo(concat)
// ConcatWith: prefix is fixed
addPrefix := concatWith("Hello, ")
assert.Equal(t, "Hello, World", addPrefix("World"))
// AppendTo: suffix is fixed
addSuffix := appendTo("!")
assert.Equal(t, "Hello!", addSuffix("Hello"))
// Different results due to different order
assert.NotEqual(t, addPrefix("test"), addSuffix("test"))
})
}
// Test composition with ConcatWith and AppendTo
func TestConcatWithAppendToComposition(t *testing.T) {
t.Run("composing multiple operations", func(t *testing.T) {
add := MakeSemigroup(func(a, b int) int { return a + b })
// Create a pipeline: add 5, then add 3
concatWith := ConcatWith(add)
appendTo := AppendTo(add)
add5 := concatWith(5)
add3 := appendTo(3)
// Apply both operations
result := add3(add5(2)) // (2 + 5) + 3 = 10
assert.Equal(t, 10, result)
})
}
// Benchmark ConcatWith
func BenchmarkConcatWith(b *testing.B) {
add := MakeSemigroup(func(a, b int) int { return a + b })
concatWith := ConcatWith(add)
add5 := concatWith(5)
b.ResetTimer()
for b.Loop() {
add5(3)
}
}
// Benchmark AppendTo
func BenchmarkAppendTo(b *testing.B) {
add := MakeSemigroup(func(a, b int) int { return a + b })
appendTo := AppendTo(add)
addTo5 := appendTo(5)
b.ResetTimer()
for b.Loop() {
addTo5(3)
}
}