1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-15 00:53:10 +02:00

Compare commits

...

1 Commits

Author SHA1 Message Date
Dr. Carsten Leue
c6445ac021 fix: better tests and docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-14 12:09:01 +01:00
12 changed files with 1652 additions and 6 deletions

View File

@@ -14,6 +14,8 @@ This document explains the key design decisions and principles behind fp-go's AP
fp-go follows the **"data last"** principle, where the data being operated on is always the last parameter in a function. This design choice enables powerful function composition and partial application patterns.
This principle is deeply rooted in functional programming tradition, particularly in **Haskell's design philosophy**. Haskell functions are automatically curried and follow the data-last convention, making function composition natural and elegant. For example, Haskell's `map` function has the signature `(a -> b) -> [a] -> [b]`, where the transformation function comes before the list.
### What is "Data Last"?
In the "data last" style, functions are structured so that:
@@ -31,6 +33,8 @@ The "data last" principle enables:
3. **Point-Free Style**: Write transformations without explicitly mentioning the data
4. **Reusability**: Create reusable transformation pipelines
This design aligns with Haskell's approach where all functions are curried by default, enabling elegant composition patterns that have proven effective over decades of functional programming practice.
### Examples
#### Basic Transformation
@@ -181,8 +185,18 @@ result := O.MonadMap(O.Some("hello"), strings.ToUpper)
The data-last currying pattern is well-documented in the functional programming community:
#### Haskell Design Philosophy
- [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 throughout
#### General Functional Programming
- [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
#### Related Libraries
- [fp-ts Documentation](https://gcanti.github.io/fp-ts/) - TypeScript library that inspired fp-go's design
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
## Kleisli and Operator Types

View File

@@ -446,6 +446,7 @@ func process() IOResult[string] {
## 📚 Documentation
- **[Design Decisions](./DESIGN.md)** - Key design principles and patterns explained
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
- **[Code Samples](./samples/)** - Practical examples and use cases
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases

91
v2/either/profunctor.go Normal file
View File

@@ -0,0 +1,91 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package either
import F "github.com/IBM/fp-go/v2/function"
// MonadExtend applies a function to an Either value, where the function receives the entire Either as input.
// This is the Extend (or Comonad) operation that allows computations to depend on the context.
//
// If the Either is Left, it returns Left unchanged without applying the function.
// If the Either is Right, it applies the function to the entire Either and wraps the result in a Right.
//
// This operation is useful when you need to perform computations that depend on whether
// a value is present (Right) or absent (Left), not just on the value itself.
//
// Type Parameters:
// - E: The error type (Left channel)
// - A: The input value type (Right channel)
// - B: The output value type
//
// Parameters:
// - fa: The Either value to extend
// - f: Function that takes the entire Either[E, A] and produces a value of type B
//
// Returns:
// - Either[E, B]: Left if input was Left, otherwise Right containing the result of f(fa)
//
// Example:
//
// // Count how many times we've seen a Right value
// counter := func(e either.Either[error, int]) int {
// return either.Fold(
// func(err error) int { return 0 },
// func(n int) int { return 1 },
// )(e)
// }
// result := either.MonadExtend(either.Right[error](42), counter) // Right(1)
// result := either.MonadExtend(either.Left[int](errors.New("err")), counter) // Left(error)
//
//go:inline
func MonadExtend[E, A, B any](fa Either[E, A], f func(Either[E, A]) B) Either[E, B] {
if fa.isLeft {
return Left[B](fa.l)
}
return Of[E](f(fa))
}
// Extend is the curried version of [MonadExtend].
// It returns a function that applies the given function to an Either value.
//
// This is useful for creating reusable transformations that depend on the Either context.
//
// Type Parameters:
// - E: The error type (Left channel)
// - A: The input value type (Right channel)
// - B: The output value type
//
// Parameters:
// - f: Function that takes the entire Either[E, A] and produces a value of type B
//
// Returns:
// - Operator[E, A, B]: A function that transforms Either[E, A] to Either[E, B]
//
// Example:
//
// // Create a reusable extender that extracts metadata
// getMetadata := either.Extend(func(e either.Either[error, string]) string {
// return either.Fold(
// func(err error) string { return "error: " + err.Error() },
// func(s string) string { return "value: " + s },
// )(e)
// })
// result := getMetadata(either.Right[error]("hello")) // Right("value: hello")
//
//go:inline
func Extend[E, A, B any](f func(Either[E, A]) B) Operator[E, A, B] {
return F.Bind2nd(MonadExtend[E, A, B], f)
}

View File

@@ -0,0 +1,377 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package either
import (
"errors"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestMonadExtendWithRight tests MonadExtend with Right values
func TestMonadExtendWithRight(t *testing.T) {
t.Run("applies function to Right value", func(t *testing.T) {
input := Right[error](42)
// Function that extracts and doubles the value if Right
f := func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, 84, GetOrElse(F.Constant1[error](0))(result))
})
t.Run("function receives entire Either context", func(t *testing.T) {
input := Right[error]("hello")
// Function that creates metadata about the Either
f := func(e Either[error, string]) string {
return Fold(
func(err error) string { return "error: " + err.Error() },
S.Prepend("value: "),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, "value: hello", GetOrElse(func(error) string { return "" })(result))
})
t.Run("can count Right occurrences", func(t *testing.T) {
input := Right[error](100)
counter := func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
F.Constant1[int](1),
)(e)
}
result := MonadExtend(input, counter)
assert.True(t, IsRight(result))
assert.Equal(t, 1, GetOrElse(func(error) int { return -1 })(result))
})
}
// TestMonadExtendWithLeft tests MonadExtend with Left values
func TestMonadExtendWithLeft(t *testing.T) {
t.Run("returns Left without applying function", func(t *testing.T) {
testErr := errors.New("test error")
input := Left[int](testErr)
// Function should not be called
called := false
f := func(e Either[error, int]) int {
called = true
return 42
}
result := MonadExtend(input, f)
assert.False(t, called, "function should not be called for Left")
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
t.Run("preserves Left error type", func(t *testing.T) {
input := Left[string](errors.New("original error"))
f := func(e Either[error, string]) string {
return "should not be called"
}
result := MonadExtend(input, f)
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, "original error", leftVal.Error())
})
}
// TestMonadExtendEdgeCases tests edge cases for MonadExtend
func TestMonadExtendEdgeCases(t *testing.T) {
t.Run("function returns zero value", func(t *testing.T) {
input := Right[error](42)
f := func(e Either[error, int]) int {
return 0
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, 0, GetOrElse(func(error) int { return -1 })(result))
})
t.Run("function changes type", func(t *testing.T) {
input := Right[error](42)
f := func(e Either[error, int]) string {
return Fold(
F.Constant1[error]("error"),
S.Format[int]("number: %d"),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, "number: 42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("nested Either handling", func(t *testing.T) {
inner := Right[error](10)
outer := Right[error](inner)
// Extract the inner value
f := func(e Either[error, Either[error, int]]) int {
return Fold(
F.Constant1[error](-1),
func(innerEither Either[error, int]) int {
return GetOrElse(F.Constant1[error](-2))(innerEither)
},
)(e)
}
result := MonadExtend(outer, f)
assert.True(t, IsRight(result))
assert.Equal(t, 10, GetOrElse(F.Constant1[error](-3))(result))
})
}
// TestExtendWithRight tests Extend (curried version) with Right values
func TestExtendWithRight(t *testing.T) {
t.Run("creates reusable extender", func(t *testing.T) {
// Create a reusable extender
doubler := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
})
result1 := doubler(Right[error](21))
result2 := doubler(Right[error](50))
assert.True(t, IsRight(result1))
assert.Equal(t, 42, GetOrElse(F.Constant1[error](0))(result1))
assert.True(t, IsRight(result2))
assert.Equal(t, 100, GetOrElse(F.Constant1[error](0))(result2))
})
t.Run("metadata extractor", func(t *testing.T) {
getMetadata := Extend(func(e Either[error, string]) string {
return Fold(
func(err error) string { return "error: " + err.Error() },
S.Prepend("value: "),
)(e)
})
result := getMetadata(Right[error]("test"))
assert.True(t, IsRight(result))
assert.Equal(t, "value: test", GetOrElse(func(error) string { return "" })(result))
})
t.Run("composition with other operations", func(t *testing.T) {
// Create an extender that counts characters
charCounter := Extend(func(e Either[error, string]) int {
return Fold(
F.Constant1[error](0),
S.Size,
)(e)
})
// Apply to a Right value
input := Right[error]("hello")
result := charCounter(input)
assert.True(t, IsRight(result))
assert.Equal(t, 5, GetOrElse(func(error) int { return -1 })(result))
})
}
// TestExtendWithLeft tests Extend with Left values
func TestExtendWithLeft(t *testing.T) {
t.Run("returns Left without calling function", func(t *testing.T) {
testErr := errors.New("test error")
called := false
extender := Extend(func(e Either[error, int]) int {
called = true
return 42
})
result := extender(Left[int](testErr))
assert.False(t, called, "function should not be called for Left")
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
t.Run("preserves error through multiple applications", func(t *testing.T) {
originalErr := errors.New("original")
extender := Extend(func(e Either[error, string]) string {
return "transformed"
})
result := extender(Left[string](originalErr))
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, originalErr, leftVal)
})
}
// TestExtendChaining tests chaining multiple Extend operations
func TestExtendChaining(t *testing.T) {
t.Run("chain multiple extenders", func(t *testing.T) {
// First extender: double the value
doubler := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
})
// Second extender: add 10
adder := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Add(10),
)(e)
})
input := Right[error](5)
result := adder(doubler(input))
assert.True(t, IsRight(result))
assert.Equal(t, 20, GetOrElse(F.Constant1[error](0))(result))
})
t.Run("short-circuits on Left", func(t *testing.T) {
testErr := errors.New("error")
extender1 := Extend(func(e Either[error, int]) int { return 1 })
extender2 := Extend(func(e Either[error, int]) int { return 2 })
input := Left[int](testErr)
result := extender2(extender1(input))
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
}
// TestExtendTypeTransformations tests type transformations with Extend
func TestExtendTypeTransformations(t *testing.T) {
t.Run("int to string transformation", func(t *testing.T) {
toString := Extend(func(e Either[error, int]) string {
return Fold(
F.Constant1[error]("error"),
strconv.Itoa,
)(e)
})
result := toString(Right[error](42))
assert.True(t, IsRight(result))
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("string to bool transformation", func(t *testing.T) {
isEmpty := Extend(func(e Either[error, string]) bool {
return Fold(
func(err error) bool { return true },
func(s string) bool { return len(s) == 0 },
)(e)
})
result1 := isEmpty(Right[error](""))
result2 := isEmpty(Right[error]("hello"))
assert.True(t, IsRight(result1))
assert.True(t, GetOrElse(func(error) bool { return false })(result1))
assert.True(t, IsRight(result2))
assert.False(t, GetOrElse(func(error) bool { return true })(result2))
})
}
// TestExtendWithComplexTypes tests Extend with complex types
func TestExtendWithComplexTypes(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("extract field from struct", func(t *testing.T) {
getName := Extend(func(e Either[error, User]) string {
return Fold(
func(err error) string { return "unknown" },
func(u User) string { return u.Name },
)(e)
})
user := User{Name: "Alice", Age: 30}
result := getName(Right[error](user))
assert.True(t, IsRight(result))
assert.Equal(t, "Alice", GetOrElse(func(error) string { return "" })(result))
})
t.Run("compute derived value", func(t *testing.T) {
isAdult := Extend(func(e Either[error, User]) bool {
return Fold(
func(err error) bool { return false },
func(u User) bool { return u.Age >= 18 },
)(e)
})
user1 := User{Name: "Bob", Age: 25}
user2 := User{Name: "Charlie", Age: 15}
result1 := isAdult(Right[error](user1))
result2 := isAdult(Right[error](user2))
assert.True(t, IsRight(result1))
assert.True(t, GetOrElse(func(error) bool { return false })(result1))
assert.True(t, IsRight(result2))
assert.False(t, GetOrElse(func(error) bool { return true })(result2))
})
}
// Made with Bob

89
v2/file/doc.go Normal file
View File

@@ -0,0 +1,89 @@
// 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 file provides functional programming utilities for working with file paths
// and I/O interfaces in Go.
//
// # Overview
//
// This package offers a collection of utility functions designed to work seamlessly
// with functional programming patterns, particularly with the fp-go library's pipe
// and composition utilities.
//
// # Path Manipulation
//
// The Join function provides a curried approach to path joining, making it easy to
// create reusable path builders:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/file"
// )
//
// // Create a reusable path builder
// addConfig := file.Join("config.json")
// configPath := addConfig("/etc/myapp")
// // Result: "/etc/myapp/config.json"
//
// // Use in a functional pipeline
// logPath := F.Pipe1("/var/log", file.Join("app.log"))
// // Result: "/var/log/app.log"
//
// // Chain multiple joins
// deepPath := F.Pipe2(
// "/root",
// file.Join("subdir"),
// file.Join("file.txt"),
// )
// // Result: "/root/subdir/file.txt"
//
// # I/O Interface Conversions
//
// The package provides generic type conversion functions for common I/O interfaces.
// These are useful for type erasure when you need to work with interface types
// rather than concrete implementations:
//
// import (
// "bytes"
// "io"
// "github.com/IBM/fp-go/v2/file"
// )
//
// // Convert concrete types to interfaces
// buf := bytes.NewBuffer([]byte("hello"))
// var reader io.Reader = file.ToReader(buf)
//
// writer := &bytes.Buffer{}
// var w io.Writer = file.ToWriter(writer)
//
// f, _ := os.Open("file.txt")
// var closer io.Closer = file.ToCloser(f)
// defer closer.Close()
//
// # Design Philosophy
//
// The functions in this package follow functional programming principles:
//
// - Currying: Functions like Join return functions, enabling partial application
// - Type Safety: Generic functions maintain type safety while providing flexibility
// - Composability: All functions work well with fp-go's pipe and composition utilities
// - Immutability: Functions don't modify their inputs
//
// # Performance
//
// The type conversion functions (ToReader, ToWriter, ToCloser) have zero overhead
// as they simply return their input cast to the interface type. The Join function
// uses Go's standard filepath.Join internally, ensuring cross-platform compatibility.
package file

View File

@@ -13,6 +13,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides utility functions for working with file paths and I/O interfaces.
// It offers functional programming utilities for path manipulation and type conversions
// for common I/O interfaces.
package file
import (
@@ -20,24 +23,93 @@ import (
"path/filepath"
)
// Join appends a filename to a root path
func Join(name string) func(root string) string {
// Join appends a filename to a root path using the operating system's path separator.
// Returns a curried function that takes a root path and joins it with the provided name.
//
// This function follows the "data last" principle, where the data (root path) is provided
// last, making it ideal for use in functional pipelines and partial application. The name
// parameter is fixed first, creating a reusable path builder function.
//
// This is useful for creating reusable path builders in functional pipelines.
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Data last: fix the filename first, apply root path later
// addConfig := file.Join("config.json")
// path := addConfig("/etc/myapp")
// // path is "/etc/myapp/config.json" on Unix
// // path is "\etc\myapp\config.json" on Windows
//
// // Using with Pipe (data flows through the pipeline)
// result := F.Pipe1("/var/log", file.Join("app.log"))
// // result is "/var/log/app.log" on Unix
//
// // Chain multiple joins
// result := F.Pipe2(
// "/root",
// file.Join("subdir"),
// file.Join("file.txt"),
// )
// // result is "/root/subdir/file.txt"
func Join(name string) Endomorphism[string] {
return func(root string) string {
return filepath.Join(root, name)
}
}
// ToReader converts a [io.Reader]
// ToReader converts any type that implements io.Reader to the io.Reader interface.
// This is useful for type erasure when you need to work with the interface type
// rather than a concrete implementation.
//
// Example:
//
// import (
// "bytes"
// "io"
// )
//
// buf := bytes.NewBuffer([]byte("hello"))
// var reader io.Reader = file.ToReader(buf)
// // reader is now of type io.Reader
func ToReader[R io.Reader](r R) io.Reader {
return r
}
// ToWriter converts a [io.Writer]
// ToWriter converts any type that implements io.Writer to the io.Writer interface.
// This is useful for type erasure when you need to work with the interface type
// rather than a concrete implementation.
//
// Example:
//
// import (
// "bytes"
// "io"
// )
//
// buf := &bytes.Buffer{}
// var writer io.Writer = file.ToWriter(buf)
// // writer is now of type io.Writer
func ToWriter[W io.Writer](w W) io.Writer {
return w
}
// ToCloser converts a [io.Closer]
// ToCloser converts any type that implements io.Closer to the io.Closer interface.
// This is useful for type erasure when you need to work with the interface type
// rather than a concrete implementation.
//
// Example:
//
// import (
// "os"
// "io"
// )
//
// f, _ := os.Open("file.txt")
// var closer io.Closer = file.ToCloser(f)
// defer closer.Close()
// // closer is now of type io.Closer
func ToCloser[C io.Closer](c C) io.Closer {
return c
}

367
v2/file/getters_test.go Normal file
View File

@@ -0,0 +1,367 @@
// 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 file
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestJoin(t *testing.T) {
t.Run("joins simple paths", func(t *testing.T) {
result := Join("config.json")("/etc/myapp")
expected := filepath.Join("/etc/myapp", "config.json")
assert.Equal(t, expected, result)
})
t.Run("joins with subdirectories", func(t *testing.T) {
result := Join("logs/app.log")("/var")
expected := filepath.Join("/var", "logs/app.log")
assert.Equal(t, expected, result)
})
t.Run("handles empty root", func(t *testing.T) {
result := Join("file.txt")("")
assert.Equal(t, "file.txt", result)
})
t.Run("handles empty name", func(t *testing.T) {
result := Join("")("/root")
expected := filepath.Join("/root", "")
assert.Equal(t, expected, result)
})
t.Run("handles relative paths", func(t *testing.T) {
result := Join("config.json")("./app")
expected := filepath.Join("./app", "config.json")
assert.Equal(t, expected, result)
})
t.Run("normalizes path separators", func(t *testing.T) {
result := Join("file.txt")("/root/path")
// Should use OS-specific separator
assert.Contains(t, result, "file.txt")
assert.Contains(t, result, "root")
assert.Contains(t, result, "path")
})
t.Run("works with Pipe", func(t *testing.T) {
result := F.Pipe1("/var/log", Join("app.log"))
expected := filepath.Join("/var/log", "app.log")
assert.Equal(t, expected, result)
})
t.Run("chains multiple joins", func(t *testing.T) {
result := F.Pipe2(
"/root",
Join("subdir"),
Join("file.txt"),
)
expected := filepath.Join("/root", "subdir", "file.txt")
assert.Equal(t, expected, result)
})
t.Run("handles special characters", func(t *testing.T) {
result := Join("my file.txt")("/path with spaces")
expected := filepath.Join("/path with spaces", "my file.txt")
assert.Equal(t, expected, result)
})
t.Run("handles dots in path", func(t *testing.T) {
result := Join("../config.json")("/app/current")
expected := filepath.Join("/app/current", "../config.json")
assert.Equal(t, expected, result)
})
}
func TestToReader(t *testing.T) {
t.Run("converts bytes.Buffer to io.Reader", func(t *testing.T) {
buf := bytes.NewBuffer([]byte("hello world"))
reader := ToReader(buf)
// Verify it's an io.Reader
var _ io.Reader = reader
// Verify it works
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "hello world", string(data))
})
t.Run("converts bytes.Reader to io.Reader", func(t *testing.T) {
bytesReader := bytes.NewReader([]byte("test data"))
reader := ToReader(bytesReader)
var _ io.Reader = reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "test data", string(data))
})
t.Run("converts strings.Reader to io.Reader", func(t *testing.T) {
strReader := strings.NewReader("string content")
reader := ToReader(strReader)
var _ io.Reader = reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "string content", string(data))
})
t.Run("preserves reader functionality", func(t *testing.T) {
original := bytes.NewBuffer([]byte("test"))
reader := ToReader(original)
// Read once
buf1 := make([]byte, 2)
n, err := reader.Read(buf1)
assert.NoError(t, err)
assert.Equal(t, 2, n)
assert.Equal(t, "te", string(buf1))
// Read again
buf2 := make([]byte, 2)
n, err = reader.Read(buf2)
assert.NoError(t, err)
assert.Equal(t, 2, n)
assert.Equal(t, "st", string(buf2))
})
t.Run("handles empty reader", func(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
reader := ToReader(buf)
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "", string(data))
})
}
func TestToWriter(t *testing.T) {
t.Run("converts bytes.Buffer to io.Writer", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
// Verify it's an io.Writer
var _ io.Writer = writer
// Verify it works
n, err := writer.Write([]byte("hello"))
assert.NoError(t, err)
assert.Equal(t, 5, n)
assert.Equal(t, "hello", buf.String())
})
t.Run("preserves writer functionality", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
// Write multiple times
writer.Write([]byte("hello "))
writer.Write([]byte("world"))
assert.Equal(t, "hello world", buf.String())
})
t.Run("handles empty writes", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
n, err := writer.Write([]byte{})
assert.NoError(t, err)
assert.Equal(t, 0, n)
assert.Equal(t, "", buf.String())
})
t.Run("handles large writes", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
data := make([]byte, 10000)
for i := range data {
data[i] = byte('A' + (i % 26))
}
n, err := writer.Write(data)
assert.NoError(t, err)
assert.Equal(t, 10000, n)
assert.Equal(t, 10000, buf.Len())
})
}
func TestToCloser(t *testing.T) {
t.Run("converts file to io.Closer", func(t *testing.T) {
// Create a temporary file
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
closer := ToCloser(tmpfile)
// Verify it's an io.Closer
var _ io.Closer = closer
// Verify it works
err = closer.Close()
assert.NoError(t, err)
})
t.Run("converts nopCloser to io.Closer", func(t *testing.T) {
// Use io.NopCloser which is a standard implementation
reader := strings.NewReader("test")
nopCloser := io.NopCloser(reader)
closer := ToCloser(nopCloser)
var _ io.Closer = closer
err := closer.Close()
assert.NoError(t, err)
})
t.Run("preserves close functionality", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
closer := ToCloser(tmpfile)
// Close should work
err = closer.Close()
assert.NoError(t, err)
// Subsequent operations should fail
_, err = tmpfile.Write([]byte("test"))
assert.Error(t, err)
})
}
// Test type conversions work together
func TestIntegration(t *testing.T) {
t.Run("reader and closer together", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// Write some data
tmpfile.Write([]byte("test content"))
tmpfile.Seek(0, 0)
// Convert to interfaces
reader := ToReader(tmpfile)
closer := ToCloser(tmpfile)
// Use as reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "test content", string(data))
// Close
err = closer.Close()
assert.NoError(t, err)
})
t.Run("writer and closer together", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// Convert to interfaces
writer := ToWriter(tmpfile)
closer := ToCloser(tmpfile)
// Use as writer
n, err := writer.Write([]byte("test data"))
assert.NoError(t, err)
assert.Equal(t, 9, n)
// Close
err = closer.Close()
assert.NoError(t, err)
// Verify data was written
data, err := os.ReadFile(tmpfile.Name())
assert.NoError(t, err)
assert.Equal(t, "test data", string(data))
})
t.Run("all conversions with file", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// File implements Reader, Writer, and Closer
var reader io.Reader = ToReader(tmpfile)
var writer io.Writer = ToWriter(tmpfile)
var closer io.Closer = ToCloser(tmpfile)
// All should be non-nil
assert.NotNil(t, reader)
assert.NotNil(t, writer)
assert.NotNil(t, closer)
// Write, read, close
writer.Write([]byte("hello"))
tmpfile.Seek(0, 0)
data, _ := io.ReadAll(reader)
assert.Equal(t, "hello", string(data))
closer.Close()
})
}
// Benchmark tests
func BenchmarkJoin(b *testing.B) {
joiner := Join("config.json")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = joiner("/etc/myapp")
}
}
func BenchmarkToReader(b *testing.B) {
buf := bytes.NewBuffer([]byte("test data"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToReader(buf)
}
}
func BenchmarkToWriter(b *testing.B) {
buf := &bytes.Buffer{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToWriter(buf)
}
}
func BenchmarkToCloser(b *testing.B) {
tmpfile, _ := os.CreateTemp("", "bench")
defer os.Remove(tmpfile.Name())
defer tmpfile.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToCloser(tmpfile)
}
}

45
v2/file/types.go Normal file
View File

@@ -0,0 +1,45 @@
// 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 file
import "github.com/IBM/fp-go/v2/endomorphism"
type (
// Endomorphism represents a function from a type to itself: A -> A.
// This is a type alias for endomorphism.Endomorphism[A].
//
// In the context of the file package, this is used for functions that
// transform strings (paths) into strings (paths), such as the Join function.
//
// An endomorphism has useful algebraic properties:
// - Identity: There exists an identity endomorphism (the identity function)
// - Composition: Endomorphisms can be composed to form new endomorphisms
// - Associativity: Composition is associative
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Join returns an Endomorphism[string]
// addConfig := file.Join("config.json") // Endomorphism[string]
// addLogs := file.Join("logs") // Endomorphism[string]
//
// // Compose endomorphisms
// addConfigLogs := F.Flow2(addLogs, addConfig)
// result := addConfigLogs("/var")
// // result is "/var/logs/config.json"
Endomorphism[A any] = endomorphism.Endomorphism[A]
)

322
v2/ord/monoid_test.go Normal file
View File

@@ -0,0 +1,322 @@
// 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 ord
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Test Semigroup laws
func TestSemigroup_Associativity(t *testing.T) {
type Person struct {
LastName string
FirstName string
MiddleName string
}
stringOrd := FromStrictCompare[string]()
byLastName := Contramap(func(p Person) string { return p.LastName })(stringOrd)
byFirstName := Contramap(func(p Person) string { return p.FirstName })(stringOrd)
byMiddleName := Contramap(func(p Person) string { return p.MiddleName })(stringOrd)
sg := Semigroup[Person]()
// Test associativity: (a <> b) <> c == a <> (b <> c)
left := sg.Concat(sg.Concat(byLastName, byFirstName), byMiddleName)
right := sg.Concat(byLastName, sg.Concat(byFirstName, byMiddleName))
p1 := Person{LastName: "Smith", FirstName: "John", MiddleName: "A"}
p2 := Person{LastName: "Smith", FirstName: "John", MiddleName: "B"}
assert.Equal(t, left.Compare(p1, p2), right.Compare(p1, p2), "Associativity should hold")
}
// Test Semigroup with three levels
func TestSemigroup_ThreeLevels(t *testing.T) {
type Employee struct {
Department string
Level int
Name string
}
stringOrd := FromStrictCompare[string]()
intOrd := FromStrictCompare[int]()
byDept := Contramap(func(e Employee) string { return e.Department })(stringOrd)
byLevel := Contramap(func(e Employee) int { return e.Level })(intOrd)
byName := Contramap(func(e Employee) string { return e.Name })(stringOrd)
sg := Semigroup[Employee]()
employeeOrd := sg.Concat(sg.Concat(byDept, byLevel), byName)
e1 := Employee{Department: "IT", Level: 3, Name: "Alice"}
e2 := Employee{Department: "IT", Level: 3, Name: "Bob"}
e3 := Employee{Department: "IT", Level: 2, Name: "Charlie"}
e4 := Employee{Department: "HR", Level: 3, Name: "David"}
// Same dept, same level, different name
assert.Equal(t, -1, employeeOrd.Compare(e1, e2), "Alice < Bob")
// Same dept, different level
assert.Equal(t, 1, employeeOrd.Compare(e1, e3), "Level 3 > Level 2")
// Different dept
assert.Equal(t, -1, employeeOrd.Compare(e4, e1), "HR < IT")
}
// Test Monoid identity laws
func TestMonoid_IdentityLaws(t *testing.T) {
m := Monoid[int]()
intOrd := FromStrictCompare[int]()
emptyOrd := m.Empty()
// Left identity: empty <> x == x
leftIdentity := m.Concat(emptyOrd, intOrd)
assert.Equal(t, -1, leftIdentity.Compare(3, 5), "Left identity: 3 < 5")
assert.Equal(t, 1, leftIdentity.Compare(5, 3), "Left identity: 5 > 3")
// Right identity: x <> empty == x
rightIdentity := m.Concat(intOrd, emptyOrd)
assert.Equal(t, -1, rightIdentity.Compare(3, 5), "Right identity: 3 < 5")
assert.Equal(t, 1, rightIdentity.Compare(5, 3), "Right identity: 5 > 3")
}
// Test Monoid with multiple empty concatenations
func TestMonoid_MultipleEmpty(t *testing.T) {
m := Monoid[int]()
emptyOrd := m.Empty()
// Concatenating multiple empty orderings should still be empty
combined := m.Concat(m.Concat(emptyOrd, emptyOrd), emptyOrd)
assert.Equal(t, 0, combined.Compare(5, 3), "Multiple empties: always equal")
assert.Equal(t, 0, combined.Compare(3, 5), "Multiple empties: always equal")
assert.True(t, combined.Equals(5, 3), "Multiple empties: always equal")
}
// Test MaxSemigroup with edge cases
func TestMaxSemigroup_EdgeCases(t *testing.T) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
tests := []struct {
name string
a int
b int
expected int
}{
{"both positive", 5, 3, 5},
{"both negative", -5, -3, -3},
{"mixed signs", -5, 3, 3},
{"zero and positive", 0, 5, 5},
{"zero and negative", 0, -5, 0},
{"both zero", 0, 0, 0},
{"equal positive", 5, 5, 5},
{"equal negative", -5, -5, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := maxSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MinSemigroup with edge cases
func TestMinSemigroup_EdgeCases(t *testing.T) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
tests := []struct {
name string
a int
b int
expected int
}{
{"both positive", 5, 3, 3},
{"both negative", -5, -3, -5},
{"mixed signs", -5, 3, -5},
{"zero and positive", 0, 5, 0},
{"zero and negative", 0, -5, -5},
{"both zero", 0, 0, 0},
{"equal positive", 5, 5, 5},
{"equal negative", -5, -5, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := minSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MaxSemigroup with strings
func TestMaxSemigroup_Strings(t *testing.T) {
stringOrd := FromStrictCompare[string]()
maxSg := MaxSemigroup(stringOrd)
tests := []struct {
name string
a string
b string
expected string
}{
{"alphabetical", "apple", "banana", "banana"},
{"same string", "apple", "apple", "apple"},
{"empty and non-empty", "", "apple", "apple"},
{"both empty", "", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := maxSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MinSemigroup with strings
func TestMinSemigroup_Strings(t *testing.T) {
stringOrd := FromStrictCompare[string]()
minSg := MinSemigroup(stringOrd)
tests := []struct {
name string
a string
b string
expected string
}{
{"alphabetical", "apple", "banana", "apple"},
{"same string", "apple", "apple", "apple"},
{"empty and non-empty", "", "apple", ""},
{"both empty", "", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := minSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MaxSemigroup associativity
func TestMaxSemigroup_Associativity(t *testing.T) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
// (a <> b) <> c == a <> (b <> c)
a, b, c := 5, 3, 7
left := maxSg.Concat(maxSg.Concat(a, b), c)
right := maxSg.Concat(a, maxSg.Concat(b, c))
assert.Equal(t, left, right, "MaxSemigroup should be associative")
assert.Equal(t, 7, left, "Should return maximum value")
}
// Test MinSemigroup associativity
func TestMinSemigroup_Associativity(t *testing.T) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
// (a <> b) <> c == a <> (b <> c)
a, b, c := 5, 3, 7
left := minSg.Concat(minSg.Concat(a, b), c)
right := minSg.Concat(a, minSg.Concat(b, c))
assert.Equal(t, left, right, "MinSemigroup should be associative")
assert.Equal(t, 3, left, "Should return minimum value")
}
// Test Semigroup with reversed ordering
func TestSemigroup_WithReverse(t *testing.T) {
type Person struct {
Age int
Name string
}
intOrd := FromStrictCompare[int]()
stringOrd := FromStrictCompare[string]()
// Order by age descending, then by name ascending
byAge := Contramap(func(p Person) int { return p.Age })(Reverse(intOrd))
byName := Contramap(func(p Person) string { return p.Name })(stringOrd)
sg := Semigroup[Person]()
personOrd := sg.Concat(byAge, byName)
p1 := Person{Age: 30, Name: "Alice"}
p2 := Person{Age: 30, Name: "Bob"}
p3 := Person{Age: 25, Name: "Charlie"}
// Same age, different name
assert.Equal(t, -1, personOrd.Compare(p1, p2), "Alice < Bob (same age)")
// Different age (descending)
assert.Equal(t, -1, personOrd.Compare(p1, p3), "30 > 25 (descending)")
}
// Benchmark MaxSemigroup
func BenchmarkMaxSemigroup(b *testing.B) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = maxSg.Concat(i, i+1)
}
}
// Benchmark MinSemigroup
func BenchmarkMinSemigroup(b *testing.B) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = minSg.Concat(i, i+1)
}
}
// Benchmark Semigroup concatenation
func BenchmarkSemigroup_Concat(b *testing.B) {
type Person struct {
LastName string
FirstName string
}
stringOrd := FromStrictCompare[string]()
byLastName := Contramap(func(p Person) string { return p.LastName })(stringOrd)
byFirstName := Contramap(func(p Person) string { return p.FirstName })(stringOrd)
sg := Semigroup[Person]()
personOrd := sg.Concat(byLastName, byFirstName)
p1 := Person{LastName: "Smith", FirstName: "Alice"}
p2 := Person{LastName: "Smith", FirstName: "Bob"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = personOrd.Compare(p1, p2)
}
}
// Made with Bob

View File

@@ -171,7 +171,7 @@ func Reverse[T any](o Ord[T]) Ord[T] {
// return p.Age
// })(intOrd)
// // Now persons are ordered by age
func Contramap[A, B any](f func(B) A) func(Ord[A]) Ord[B] {
func Contramap[A, B any](f func(B) A) Operator[A, B] {
return func(o Ord[A]) Ord[B] {
return MakeOrd(func(x, y B) int {
return o.Compare(f(x), f(y))
@@ -373,6 +373,8 @@ func Between[A any](o Ord[A]) func(A, A) func(A) bool {
}
}
// compareTime is a helper function that compares two time.Time values.
// Returns -1 if a is before b, 1 if a is after b, and 0 if they are equal.
func compareTime(a, b time.Time) int {
if a.Before(b) {
return -1

61
v2/ord/types.go Normal file
View File

@@ -0,0 +1,61 @@
// 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 ord
type (
// Kleisli represents a function that takes a value of type A and returns an Ord[B].
// This is useful for creating orderings that depend on input values.
//
// Type Parameters:
// - A: The input type
// - B: The type for which ordering is produced
//
// Example:
//
// // Create a Kleisli that produces different orderings based on input
// var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
// if mode == "ascending" {
// return ord.FromStrictCompare[int]()
// }
// return ord.Reverse(ord.FromStrictCompare[int]())
// }
// ascOrd := orderingFactory("ascending")
// descOrd := orderingFactory("descending")
Kleisli[A, B any] = func(A) Ord[B]
// Operator represents a function that transforms an Ord[A] into a value of type B.
// This is commonly used for operations that modify or combine orderings.
//
// Type Parameters:
// - A: The type for which ordering is defined
// - B: The result type of the operation
//
// This is equivalent to Kleisli[Ord[A], B] and is used for operations like
// Contramap, which takes an Ord[A] and produces an Ord[B].
//
// Example:
//
// // Contramap is an Operator that transforms Ord[A] to Ord[B]
// type Person struct { Age int }
// var ageOperator Operator[int, Person] = ord.Contramap(func(p Person) int {
// return p.Age
// })
// intOrd := ord.FromStrictCompare[int]()
// personOrd := ageOperator(intOrd)
Operator[A, B any] = Kleisli[Ord[A], B]
)
// Made with Bob

205
v2/ord/types_test.go Normal file
View File

@@ -0,0 +1,205 @@
// 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 ord
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Test Kleisli type
func TestKleisli(t *testing.T) {
// Create a Kleisli that produces different orderings based on input
var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
if mode == "ascending" {
return FromStrictCompare[int]()
}
return Reverse(FromStrictCompare[int]())
}
// Test ascending order
ascOrd := orderingFactory("ascending")
assert.Equal(t, -1, ascOrd.Compare(3, 5), "ascending: 3 < 5")
assert.Equal(t, 1, ascOrd.Compare(5, 3), "ascending: 5 > 3")
assert.Equal(t, 0, ascOrd.Compare(5, 5), "ascending: 5 == 5")
// Test descending order
descOrd := orderingFactory("descending")
assert.Equal(t, 1, descOrd.Compare(3, 5), "descending: 3 > 5")
assert.Equal(t, -1, descOrd.Compare(5, 3), "descending: 5 < 3")
assert.Equal(t, 0, descOrd.Compare(5, 5), "descending: 5 == 5")
}
// Test Kleisli with complex types
func TestKleisli_ComplexType(t *testing.T) {
type Person struct {
Name string
Age int
}
// Kleisli that creates orderings based on a field selector
var personOrderingFactory Kleisli[string, Person] = func(field string) Ord[Person] {
stringOrd := FromStrictCompare[string]()
intOrd := FromStrictCompare[int]()
switch field {
case "name":
return Contramap(func(p Person) string { return p.Name })(stringOrd)
case "age":
return Contramap(func(p Person) int { return p.Age })(intOrd)
default:
// Default to name ordering
return Contramap(func(p Person) string { return p.Name })(stringOrd)
}
}
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
// Order by name
nameOrd := personOrderingFactory("name")
assert.Equal(t, -1, nameOrd.Compare(p1, p2), "Alice < Bob by name")
// Order by age
ageOrd := personOrderingFactory("age")
assert.Equal(t, 1, ageOrd.Compare(p1, p2), "30 > 25 by age")
}
// Test Operator type
func TestOperator(t *testing.T) {
type Person struct {
Name string
Age int
}
// Operator that transforms Ord[int] to Ord[Person] by age
var ageOperator Operator[int, Person] = Contramap(func(p Person) int {
return p.Age
})
intOrd := FromStrictCompare[int]()
personOrd := ageOperator(intOrd)
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
p3 := Person{Name: "Charlie", Age: 30}
assert.Equal(t, 1, personOrd.Compare(p1, p2), "30 > 25")
assert.Equal(t, -1, personOrd.Compare(p2, p1), "25 < 30")
assert.Equal(t, 0, personOrd.Compare(p1, p3), "30 == 30")
assert.True(t, personOrd.Equals(p1, p3), "same age")
assert.False(t, personOrd.Equals(p1, p2), "different age")
}
// Test Operator composition
func TestOperator_Composition(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
// Create operators for different transformations
stringOrd := FromStrictCompare[string]()
// Operator to order Person by city
var cityOperator Operator[string, Person] = Contramap(func(p Person) string {
return p.Address.City
})
personOrd := cityOperator(stringOrd)
p1 := Person{Name: "Alice", Address: Address{Street: "Main St", City: "Boston"}}
p2 := Person{Name: "Bob", Address: Address{Street: "Oak Ave", City: "Austin"}}
assert.Equal(t, 1, personOrd.Compare(p1, p2), "Boston > Austin")
assert.Equal(t, -1, personOrd.Compare(p2, p1), "Austin < Boston")
}
// Test Operator with multiple transformations
func TestOperator_MultipleTransformations(t *testing.T) {
type Product struct {
Name string
Price float64
}
floatOrd := FromStrictCompare[float64]()
// Operator to order by price
var priceOperator Operator[float64, Product] = Contramap(func(p Product) float64 {
return p.Price
})
// Operator to reverse the ordering
var reverseOperator Operator[float64, Product] = func(o Ord[float64]) Ord[Product] {
return priceOperator(Reverse(o))
}
// Order by price descending
productOrd := reverseOperator(floatOrd)
prod1 := Product{Name: "Widget", Price: 19.99}
prod2 := Product{Name: "Gadget", Price: 29.99}
assert.Equal(t, 1, productOrd.Compare(prod1, prod2), "19.99 > 29.99 (reversed)")
assert.Equal(t, -1, productOrd.Compare(prod2, prod1), "29.99 < 19.99 (reversed)")
}
// Example test for Kleisli
func ExampleKleisli() {
// Create a Kleisli that produces different orderings based on input
var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
if mode == "ascending" {
return FromStrictCompare[int]()
}
return Reverse(FromStrictCompare[int]())
}
ascOrd := orderingFactory("ascending")
descOrd := orderingFactory("descending")
println(ascOrd.Compare(5, 3)) // 1
println(descOrd.Compare(5, 3)) // -1
}
// Example test for Operator
func ExampleOperator() {
type Person struct {
Name string
Age int
}
// Operator that transforms Ord[int] to Ord[Person] by age
var ageOperator Operator[int, Person] = Contramap(func(p Person) int {
return p.Age
})
intOrd := FromStrictCompare[int]()
personOrd := ageOperator(intOrd)
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
result := personOrd.Compare(p1, p2)
println(result) // 1 (30 > 25)
}
// Made with Bob