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

Compare commits

...

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
b2d111e8ec fix: more doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 14:04:44 +01:00
Dr. Carsten Leue
ae141c85c6 fix: add tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 13:40:12 +01:00
Dr. Carsten Leue
1230b4581b doc: add doc links
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 10:12:20 +01:00
9 changed files with 1051 additions and 15 deletions

View File

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

View File

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

View File

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

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

@@ -0,0 +1,52 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package monoid
import "github.com/IBM/fp-go/v2/function"
// Void is an alias for function.Void, representing the unit type.
//
// The Void type (also known as Unit in functional programming) has exactly one value,
// making it useful for representing the absence of meaningful information. It's similar
// to void in other languages, but as a value rather than the absence of a value.
//
// This type alias is provided in the monoid package for convenience when working with
// VoidMonoid and other monoid operations that may use the unit type.
//
// Common use cases:
// - As a return type for functions that perform side effects but don't return meaningful data
// - As a placeholder type parameter when a type is required but no data needs to be passed
// - In monoid operations where you need to track that operations occurred without caring about results
//
// See also:
// - function.Void: The underlying type definition
// - function.VOID: The single inhabitant of the Void type
// - VoidMonoid: A monoid instance for the Void type
//
// Example:
//
// // Function that performs an action but returns no meaningful data
// func logMessage(msg string) Void {
// fmt.Println(msg)
// return function.VOID
// }
//
// // Using Void in monoid operations
// m := VoidMonoid()
// result := m.Concat(function.VOID, function.VOID) // function.VOID
type (
Void = function.Void
)

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

@@ -0,0 +1,65 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package monoid
import (
"github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
// VoidMonoid creates a Monoid for the Void (unit) type.
//
// The Void type has exactly one value (function.VOID), making it trivial to define
// a monoid. This monoid uses the Last semigroup, which always returns the second
// argument, though since all Void values are identical, the choice of semigroup
// doesn't affect the result.
//
// This monoid is useful in contexts where:
// - A monoid instance is required but no meaningful data needs to be combined
// - You need to track that an operation occurred without caring about its result
// - Building generic abstractions that work with any monoid, including the trivial case
//
// # Monoid Laws
//
// The VoidMonoid satisfies all monoid laws trivially:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always VOID
// - Left Identity: Concat(Empty(), x) = x - always VOID
// - Right Identity: Concat(x, Empty()) = x - always VOID
//
// Returns:
// - A Monoid[Void] instance
//
// Example:
//
// m := VoidMonoid()
// result := m.Concat(function.VOID, function.VOID) // function.VOID
// empty := m.Empty() // function.VOID
//
// // Useful for tracking operations without data
// type Action = func() Void
// actions := []Action{
// func() Void { fmt.Println("Action 1"); return function.VOID },
// func() Void { fmt.Println("Action 2"); return function.VOID },
// }
// // Execute all actions and combine results
// results := A.Map(func(a Action) Void { return a() })(actions)
// _ = ConcatAll(m)(results) // All actions executed, result is VOID
func VoidMonoid() Monoid[Void] {
return MakeMonoid(
S.Last[Void]().Concat,
function.VOID,
)
}

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

@@ -0,0 +1,292 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package monoid
import (
"testing"
"github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestVoidMonoid_Basic tests basic VoidMonoid functionality
func TestVoidMonoid_Basic(t *testing.T) {
m := VoidMonoid()
// Test Empty returns VOID
empty := m.Empty()
assert.Equal(t, function.VOID, empty)
// Test Concat returns VOID (since all Void values are identical)
result := m.Concat(function.VOID, function.VOID)
assert.Equal(t, function.VOID, result)
}
// TestVoidMonoid_Laws verifies VoidMonoid satisfies monoid laws
func TestVoidMonoid_Laws(t *testing.T) {
m := VoidMonoid()
// Since Void has only one value, we test with that value
v := function.VOID
// Left Identity: Concat(Empty(), x) = x
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), v)
assert.Equal(t, v, result, "Left identity law failed")
})
// Right Identity: Concat(x, Empty()) = x
t.Run("right identity", func(t *testing.T) {
result := m.Concat(v, m.Empty())
assert.Equal(t, v, result, "Right identity law failed")
})
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
t.Run("associativity", func(t *testing.T) {
left := m.Concat(m.Concat(v, v), v)
right := m.Concat(v, m.Concat(v, v))
assert.Equal(t, left, right, "Associativity law failed")
})
// All results should be VOID
t.Run("all operations return VOID", func(t *testing.T) {
assert.Equal(t, function.VOID, m.Concat(v, v))
assert.Equal(t, function.VOID, m.Empty())
assert.Equal(t, function.VOID, m.Concat(m.Empty(), v))
assert.Equal(t, function.VOID, m.Concat(v, m.Empty()))
})
}
// TestVoidMonoid_ConcatAll tests combining multiple Void values
func TestVoidMonoid_ConcatAll(t *testing.T) {
m := VoidMonoid()
concatAll := ConcatAll(m)
tests := []struct {
name string
input []Void
expected Void
}{
{
name: "empty slice",
input: []Void{},
expected: function.VOID,
},
{
name: "single element",
input: []Void{function.VOID},
expected: function.VOID,
},
{
name: "multiple elements",
input: []Void{function.VOID, function.VOID, function.VOID},
expected: function.VOID,
},
{
name: "many elements",
input: make([]Void, 100),
expected: function.VOID,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize slice with VOID values
for i := range tt.input {
tt.input[i] = function.VOID
}
result := concatAll(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestVoidMonoid_Fold tests the Fold function with VoidMonoid
func TestVoidMonoid_Fold(t *testing.T) {
m := VoidMonoid()
fold := Fold(m)
// Fold should behave identically to ConcatAll
voids := []Void{function.VOID, function.VOID, function.VOID}
result := fold(voids)
assert.Equal(t, function.VOID, result)
// Empty fold
emptyResult := fold([]Void{})
assert.Equal(t, function.VOID, emptyResult)
}
// TestVoidMonoid_Reverse tests that Reverse doesn't affect VoidMonoid
func TestVoidMonoid_Reverse(t *testing.T) {
m := VoidMonoid()
reversed := Reverse(m)
// Since all Void values are identical, reverse should have no effect
v := function.VOID
assert.Equal(t, m.Concat(v, v), reversed.Concat(v, v))
assert.Equal(t, m.Empty(), reversed.Empty())
// Test identity laws still hold
assert.Equal(t, v, reversed.Concat(reversed.Empty(), v))
assert.Equal(t, v, reversed.Concat(v, reversed.Empty()))
}
// TestVoidMonoid_ToSemigroup tests conversion to Semigroup
func TestVoidMonoid_ToSemigroup(t *testing.T) {
m := VoidMonoid()
sg := ToSemigroup(m)
// Should work as a semigroup
result := sg.Concat(function.VOID, function.VOID)
assert.Equal(t, function.VOID, result)
// Verify it's the same underlying operation
assert.Equal(t, m.Concat(function.VOID, function.VOID), sg.Concat(function.VOID, function.VOID))
}
// TestVoidMonoid_FunctionMonoid tests VoidMonoid with FunctionMonoid
func TestVoidMonoid_FunctionMonoid(t *testing.T) {
m := VoidMonoid()
funcMonoid := FunctionMonoid[string](m)
// Create functions that return Void
f1 := func(s string) Void { return function.VOID }
f2 := func(s string) Void { return function.VOID }
// Combine functions
combined := funcMonoid.Concat(f1, f2)
// Test combined function
result := combined("test")
assert.Equal(t, function.VOID, result)
// Test empty function
emptyFunc := funcMonoid.Empty()
assert.Equal(t, function.VOID, emptyFunc("anything"))
}
// TestVoidMonoid_PracticalUsage demonstrates practical usage patterns
func TestVoidMonoid_PracticalUsage(t *testing.T) {
m := VoidMonoid()
// Simulate tracking that operations occurred without caring about results
type Action func() Void
actions := []Action{
func() Void { return function.VOID }, // Action 1
func() Void { return function.VOID }, // Action 2
func() Void { return function.VOID }, // Action 3
}
// Execute all actions and collect results
results := make([]Void, len(actions))
for i, action := range actions {
results[i] = action()
}
// Combine all results (all are VOID)
finalResult := ConcatAll(m)(results)
assert.Equal(t, function.VOID, finalResult)
}
// TestVoidMonoid_EdgeCases tests edge cases
func TestVoidMonoid_EdgeCases(t *testing.T) {
m := VoidMonoid()
t.Run("multiple concatenations", func(t *testing.T) {
// Chain multiple Concat operations
result := m.Concat(
m.Concat(
m.Concat(function.VOID, function.VOID),
function.VOID,
),
function.VOID,
)
assert.Equal(t, function.VOID, result)
})
t.Run("concat with empty", func(t *testing.T) {
// Various combinations with Empty()
assert.Equal(t, function.VOID, m.Concat(m.Empty(), m.Empty()))
assert.Equal(t, function.VOID, m.Concat(m.Concat(m.Empty(), function.VOID), m.Empty()))
})
t.Run("large slice", func(t *testing.T) {
// Test with a large number of elements
largeSlice := make([]Void, 10000)
for i := range largeSlice {
largeSlice[i] = function.VOID
}
result := ConcatAll(m)(largeSlice)
assert.Equal(t, function.VOID, result)
})
}
// TestVoidMonoid_TypeSafety verifies type safety
func TestVoidMonoid_TypeSafety(t *testing.T) {
m := VoidMonoid()
// Verify it implements Monoid interface
var _ Monoid[Void] = m
// Verify Empty returns correct type
empty := m.Empty()
var _ Void = empty
// Verify Concat returns correct type
result := m.Concat(function.VOID, function.VOID)
var _ Void = result
}
// BenchmarkVoidMonoid_Concat benchmarks the Concat operation
func BenchmarkVoidMonoid_Concat(b *testing.B) {
m := VoidMonoid()
v := function.VOID
b.ResetTimer()
for b.Loop() {
_ = m.Concat(v, v)
}
}
// BenchmarkVoidMonoid_ConcatAll benchmarks combining multiple Void values
func BenchmarkVoidMonoid_ConcatAll(b *testing.B) {
m := VoidMonoid()
concatAll := ConcatAll(m)
voids := make([]Void, 1000)
for i := range voids {
voids[i] = function.VOID
}
b.ResetTimer()
for b.Loop() {
_ = concatAll(voids)
}
}
// BenchmarkVoidMonoid_Empty benchmarks the Empty operation
func BenchmarkVoidMonoid_Empty(b *testing.B) {
m := VoidMonoid()
b.ResetTimer()
for b.Loop() {
_ = m.Empty()
}
}
// Made with Bob

View File

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

View File

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

View File

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