mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-15 00:53:10 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6445ac021 |
14
v2/DESIGN.md
14
v2/DESIGN.md
@@ -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
|
||||
|
||||
@@ -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
91
v2/either/profunctor.go
Normal 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)
|
||||
}
|
||||
377
v2/either/profunctor_test.go
Normal file
377
v2/either/profunctor_test.go
Normal 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
89
v2/file/doc.go
Normal 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
|
||||
@@ -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
367
v2/file/getters_test.go
Normal 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
45
v2/file/types.go
Normal 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
322
v2/ord/monoid_test.go
Normal 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
|
||||
@@ -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
61
v2/ord/types.go
Normal 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
205
v2/ord/types_test.go
Normal 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
|
||||
Reference in New Issue
Block a user