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

Compare commits

...

1 Commits

Author SHA1 Message Date
Dr. Carsten Leue
840ffbb51d fix: documentation and missing tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-13 20:27:46 +01:00
10 changed files with 1973 additions and 7 deletions

View File

@@ -622,3 +622,128 @@ func Prepend[A any](head A) Operator[A, A] {
func Reverse[A any](as []A) []A {
return G.Reverse(as)
}
// Extend applies a function to every suffix of an array, creating a new array of results.
// This is the comonad extend operation for arrays.
//
// The function f is applied to progressively smaller suffixes of the input array:
// - f(as[0:]) for the first element
// - f(as[1:]) for the second element
// - f(as[2:]) for the third element
// - and so on...
//
// Type Parameters:
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// Parameters:
// - f: A function that takes an array suffix and returns a value
//
// Returns:
// - A function that transforms an array of A into an array of B
//
// Behavior:
// - Creates a new array with the same length as the input
// - For each position i, applies f to the suffix starting at i
// - Returns an empty array if the input is empty
//
// Example:
//
// // Sum all elements from current position to end
// sumSuffix := array.Extend(func(as []int) int {
// return array.Reduce(func(acc, x int) int { return acc + x }, 0)(as)
// })
// result := sumSuffix([]int{1, 2, 3, 4})
// // result: []int{10, 9, 7, 4}
// // Explanation: [1+2+3+4, 2+3+4, 3+4, 4]
//
// Example with length:
//
// // Get remaining length at each position
// lengths := array.Extend(array.Size[int])
// result := lengths([]int{10, 20, 30})
// // result: []int{3, 2, 1}
//
// Example with head:
//
// // Duplicate each element (extract head of each suffix)
// duplicate := array.Extend(func(as []int) int {
// return F.Pipe1(as, array.Head[int], O.GetOrElse(F.Constant(0)))
// })
// result := duplicate([]int{1, 2, 3})
// // result: []int{1, 2, 3}
//
// Use cases:
// - Computing cumulative or rolling operations
// - Implementing sliding window algorithms
// - Creating context-aware transformations
// - Building comonadic computations
//
// Comonad laws:
// - Left identity: Extend(Extract) == Identity
// - Right identity: Extract ∘ Extend(f) == f
// - Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))
//
//go:inline
func Extend[A, B any](f func([]A) B) Operator[A, B] {
return func(as []A) []B {
return G.MakeBy[[]B](len(as), func(i int) B { return f(as[i:]) })
}
}
// Extract returns the first element of an array, or a zero value if empty.
// This is the comonad extract operation for arrays.
//
// Extract is the dual of the monadic return/of operation. While Of wraps a value
// in a context, Extract unwraps a value from its context.
//
// Type Parameters:
// - A: The type of elements in the array
//
// Parameters:
// - as: The input array
//
// Returns:
// - The first element if the array is non-empty, otherwise the zero value of type A
//
// Behavior:
// - Returns as[0] if the array has at least one element
// - Returns the zero value of A if the array is empty
// - Does not modify the input array
//
// Example:
//
// result := array.Extract([]int{1, 2, 3})
// // result: 1
//
// Example with empty array:
//
// result := array.Extract([]int{})
// // result: 0 (zero value for int)
//
// Example with strings:
//
// result := array.Extract([]string{"hello", "world"})
// // result: "hello"
//
// Example with empty string array:
//
// result := array.Extract([]string{})
// // result: "" (zero value for string)
//
// Use cases:
// - Extracting the current focus from a comonadic context
// - Getting the head element with a default zero value
// - Implementing comonad-based computations
//
// Comonad laws:
// - Extract ∘ Of == Identity (extracting from a singleton returns the value)
// - Extract ∘ Extend(f) == f (extract after extend equals applying f)
//
// Note: For a safer alternative that handles empty arrays explicitly,
// consider using Head which returns an Option[A].
//
//go:inline
func Extract[A any](as []A) A {
return G.Extract(as)
}

View File

@@ -474,3 +474,293 @@ func TestReverseProperties(t *testing.T) {
}
})
}
// TestExtract tests the Extract function
func TestExtract(t *testing.T) {
t.Run("Extract from non-empty array", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extract(input)
assert.Equal(t, 1, result)
})
t.Run("Extract from single element array", func(t *testing.T) {
input := []string{"hello"}
result := Extract(input)
assert.Equal(t, "hello", result)
})
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
input := []int{}
result := Extract(input)
assert.Equal(t, 0, result)
})
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
input := []string{}
result := Extract(input)
assert.Equal(t, "", result)
})
t.Run("Extract does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extract(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extract with floats", func(t *testing.T) {
input := []float64{3.14, 2.71, 1.41}
result := Extract(input)
assert.Equal(t, 3.14, result)
})
t.Run("Extract with structs", func(t *testing.T) {
type Person struct {
Name string
Age int
}
input := []Person{
{"Alice", 30},
{"Bob", 25},
}
result := Extract(input)
assert.Equal(t, Person{"Alice", 30}, result)
})
}
// TestExtractComonadLaws tests comonad laws for Extract
func TestExtractComonadLaws(t *testing.T) {
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
value := 42
result := Extract(Of(value))
assert.Equal(t, value, result)
})
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
extended := Extend(f)(input)
result := Extract(extended)
expected := f(input)
assert.Equal(t, expected, result)
})
}
// TestExtend tests the Extend function
func TestExtend(t *testing.T) {
t.Run("Extend with sum of suffixes", func(t *testing.T) {
input := []int{1, 2, 3, 4}
sumSuffix := Extend(func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := sumSuffix(input)
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
assert.Equal(t, expected, result)
})
t.Run("Extend with length of suffixes", func(t *testing.T) {
input := []int{10, 20, 30}
lengths := Extend(Size[int])
result := lengths(input)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Extend with head extraction", func(t *testing.T) {
input := []int{1, 2, 3}
duplicate := Extend(func(as []int) int {
return F.Pipe2(as, Head[int], O.GetOrElse(F.Constant(0)))
})
result := duplicate(input)
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
t.Run("Extend with empty array", func(t *testing.T) {
input := []int{}
result := Extend(Size[int])(input)
assert.Equal(t, []int{}, result)
})
t.Run("Extend with single element", func(t *testing.T) {
input := []string{"hello"}
result := Extend(func(as []string) int { return len(as) })(input)
expected := []int{1}
assert.Equal(t, expected, result)
})
t.Run("Extend does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extend(Size[int])(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extend with string concatenation", func(t *testing.T) {
input := []string{"a", "b", "c"}
concat := Extend(func(as []string) string {
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
})
result := concat(input)
expected := []string{"abc", "bc", "c"}
assert.Equal(t, expected, result)
})
t.Run("Extend with max of suffixes", func(t *testing.T) {
input := []int{3, 1, 4, 1, 5}
maxSuffix := Extend(func(as []int) int {
if len(as) == 0 {
return 0
}
max := as[0]
for _, v := range as[1:] {
if v > max {
max = v
}
}
return max
})
result := maxSuffix(input)
expected := []int{5, 5, 5, 5, 5}
assert.Equal(t, expected, result)
})
}
// TestExtendComonadLaws tests comonad laws for Extend
func TestExtendComonadLaws(t *testing.T) {
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extend(Extract[int])(input)
assert.Equal(t, input, result)
})
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
result := F.Pipe2(input, Extend(f), Extract[int])
expected := f(input)
assert.Equal(t, expected, result)
})
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
input := []int{1, 2, 3}
// f: sum of array
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// g: length of array
g := func(as []int) int {
return len(as)
}
// Left side: Extend(f) ∘ Extend(g)
left := F.Pipe2(input, Extend(g), Extend(f))
// Right side: Extend(f ∘ Extend(g))
right := Extend(func(as []int) int {
return f(Extend(g)(as))
})(input)
assert.Equal(t, left, right)
})
}
// TestExtendComposition tests Extend with other array operations
func TestExtendComposition(t *testing.T) {
t.Run("Extend after Map", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Map(N.Mul(2)),
Extend(func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}),
)
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
assert.Equal(t, expected, result)
})
t.Run("Map after Extend", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Extend(Size[int]),
Map(N.Mul(10)),
)
expected := []int{30, 20, 10}
assert.Equal(t, expected, result)
})
t.Run("Extend with Filter", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5, 6}
result := F.Pipe2(
input,
Filter(func(n int) bool { return n%2 == 0 }),
Extend(Size[int]),
)
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
assert.Equal(t, expected, result)
})
}
// TestExtendUseCases demonstrates practical use cases for Extend
func TestExtendUseCases(t *testing.T) {
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
runningSum := Extend(func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := runningSum(input)
expected := []int{15, 14, 12, 9, 5}
assert.Equal(t, expected, result)
})
t.Run("Sliding window average", func(t *testing.T) {
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
windowAvg := Extend(func(as []float64) float64 {
if len(as) == 0 {
return 0
}
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
return sum / float64(len(as))
})
result := windowAvg(input)
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
assert.Equal(t, expected, result)
})
t.Run("Check if suffix is sorted", func(t *testing.T) {
input := []int{1, 2, 3, 2, 1}
isSorted := Extend(func(as []int) bool {
for i := 1; i < len(as); i++ {
if as[i] < as[i-1] {
return false
}
}
return true
})
result := isSorted(input)
expected := []bool{false, false, false, false, true}
assert.Equal(t, expected, result)
})
t.Run("Count remaining elements", func(t *testing.T) {
events := []string{"start", "middle", "end"}
remaining := Extend(Size[string])
result := remaining(events)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
}

View File

@@ -375,3 +375,102 @@ func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
func Reverse[GT ~[]T, T any](as GT) GT {
return array.Reverse(as)
}
// Extract returns the first element of an array, or a zero value if empty.
// This is the comonad extract operation for arrays.
//
// Extract is the dual of the monadic return/of operation. While Of wraps a value
// in a context, Extract unwraps a value from its context.
//
// Type Parameters:
// - GA: The array type constraint
// - A: The type of elements in the array
//
// Parameters:
// - as: The input array
//
// Returns:
// - The first element if the array is non-empty, otherwise the zero value of type A
//
// Behavior:
// - Returns as[0] if the array has at least one element
// - Returns the zero value of A if the array is empty
// - Does not modify the input array
//
// Example:
//
// result := Extract([]int{1, 2, 3})
// // result: 1
//
// Example with empty array:
//
// result := Extract([]int{})
// // result: 0 (zero value for int)
//
// Comonad laws:
// - Extract ∘ Of == Identity (extracting from a singleton returns the value)
// - Extract ∘ Extend(f) == f (extract after extend equals applying f)
//
//go:inline
func Extract[GA ~[]A, A any](as GA) A {
if len(as) > 0 {
return as[0]
}
var zero A
return zero
}
// Extend applies a function to every suffix of an array, creating a new array of results.
// This is the comonad extend operation for arrays.
//
// The function f is applied to progressively smaller suffixes of the input array:
// - f(as[0:]) for the first element
// - f(as[1:]) for the second element
// - f(as[2:]) for the third element
// - and so on...
//
// Type Parameters:
// - GA: The input array type constraint
// - GB: The output array type constraint
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// Parameters:
// - f: A function that takes an array suffix and returns a value
//
// Returns:
// - A function that transforms an array of A into an array of B
//
// Behavior:
// - Creates a new array with the same length as the input
// - For each position i, applies f to the suffix starting at i
// - Returns an empty array if the input is empty
//
// Example:
//
// // Sum all elements from current position to end
// sumSuffix := Extend[[]int, []int](func(as []int) int {
// return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
// })
// result := sumSuffix([]int{1, 2, 3, 4})
// // result: []int{10, 9, 7, 4}
// // Explanation: [1+2+3+4, 2+3+4, 3+4, 4]
//
// Example with length:
//
// // Get remaining length at each position
// lengths := Extend[[]int, []int](Size[[]int, int])
// result := lengths([]int{10, 20, 30})
// // result: []int{3, 2, 1}
//
// Comonad laws:
// - Left identity: Extend(Extract) == Identity
// - Right identity: Extract ∘ Extend(f) == f
// - Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))
//
//go:inline
func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
return func(as GA) GB {
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
}
}

View File

@@ -0,0 +1,298 @@
// 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 generic
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestExtract tests the Extract function
func TestExtract(t *testing.T) {
t.Run("Extract from non-empty array", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extract(input)
assert.Equal(t, 1, result)
})
t.Run("Extract from single element array", func(t *testing.T) {
input := []string{"hello"}
result := Extract(input)
assert.Equal(t, "hello", result)
})
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
input := []int{}
result := Extract(input)
assert.Equal(t, 0, result)
})
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
input := []string{}
result := Extract(input)
assert.Equal(t, "", result)
})
t.Run("Extract does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extract(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extract with floats", func(t *testing.T) {
input := []float64{3.14, 2.71, 1.41}
result := Extract(input)
assert.Equal(t, 3.14, result)
})
t.Run("Extract with custom slice type", func(t *testing.T) {
type IntSlice []int
input := IntSlice{10, 20, 30}
result := Extract(input)
assert.Equal(t, 10, result)
})
}
// TestExtractComonadLaws tests comonad laws for Extract
func TestExtractComonadLaws(t *testing.T) {
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
value := 42
result := Extract(Of[[]int](value))
assert.Equal(t, value, result)
})
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
extended := Extend[[]int, []int](f)(input)
result := Extract(extended)
expected := f(input)
assert.Equal(t, expected, result)
})
}
// TestExtend tests the Extend function
func TestExtend(t *testing.T) {
t.Run("Extend with sum of suffixes", func(t *testing.T) {
input := []int{1, 2, 3, 4}
sumSuffix := Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := sumSuffix(input)
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
assert.Equal(t, expected, result)
})
t.Run("Extend with length of suffixes", func(t *testing.T) {
input := []int{10, 20, 30}
lengths := Extend[[]int, []int](Size[[]int, int])
result := lengths(input)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Extend with head extraction", func(t *testing.T) {
input := []int{1, 2, 3}
duplicate := Extend[[]int, []int](Extract[[]int, int])
result := duplicate(input)
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
t.Run("Extend with empty array", func(t *testing.T) {
input := []int{}
result := Extend[[]int, []int](Size[[]int, int])(input)
assert.Equal(t, []int{}, result)
})
t.Run("Extend with single element", func(t *testing.T) {
input := []string{"hello"}
result := Extend[[]string, []int](func(as []string) int { return len(as) })(input)
expected := []int{1}
assert.Equal(t, expected, result)
})
t.Run("Extend does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extend[[]int, []int](Size[[]int, int])(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extend with string concatenation", func(t *testing.T) {
input := []string{"a", "b", "c"}
concat := Extend[[]string, []string](func(as []string) string {
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
})
result := concat(input)
expected := []string{"abc", "bc", "c"}
assert.Equal(t, expected, result)
})
t.Run("Extend with custom slice types", func(t *testing.T) {
type IntSlice []int
type ResultSlice []int
input := IntSlice{1, 2, 3}
sumSuffix := Extend[IntSlice, ResultSlice](func(as IntSlice) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := sumSuffix(input)
expected := ResultSlice{6, 5, 3}
assert.Equal(t, expected, result)
})
}
// TestExtendComonadLaws tests comonad laws for Extend
func TestExtendComonadLaws(t *testing.T) {
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extend[[]int, []int](Extract[[]int, int])(input)
assert.Equal(t, input, result)
})
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
result := F.Pipe2(input, Extend[[]int, []int](f), Extract[[]int, int])
expected := f(input)
assert.Equal(t, expected, result)
})
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
input := []int{1, 2, 3}
// f: sum of array
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// g: length of array
g := func(as []int) int {
return len(as)
}
// Left side: Extend(f) ∘ Extend(g)
left := F.Pipe2(input, Extend[[]int, []int](g), Extend[[]int, []int](f))
// Right side: Extend(f ∘ Extend(g))
right := Extend[[]int, []int](func(as []int) int {
return f(Extend[[]int, []int](g)(as))
})(input)
assert.Equal(t, left, right)
})
}
// TestExtendComposition tests Extend with other array operations
func TestExtendComposition(t *testing.T) {
t.Run("Extend after Map", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Map[[]int, []int](func(x int) int { return x * 2 }),
Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}),
)
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
assert.Equal(t, expected, result)
})
t.Run("Map after Extend", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Extend[[]int, []int](Size[[]int, int]),
Map[[]int, []int](func(x int) int { return x * 10 }),
)
expected := []int{30, 20, 10}
assert.Equal(t, expected, result)
})
t.Run("Extend with Filter", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5, 6}
result := F.Pipe2(
input,
Filter[[]int](func(n int) bool { return n%2 == 0 }),
Extend[[]int, []int](Size[[]int, int]),
)
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
assert.Equal(t, expected, result)
})
}
// TestExtendUseCases demonstrates practical use cases for Extend
func TestExtendUseCases(t *testing.T) {
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
runningSum := Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := runningSum(input)
expected := []int{15, 14, 12, 9, 5}
assert.Equal(t, expected, result)
})
t.Run("Sliding window average", func(t *testing.T) {
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
windowAvg := Extend[[]float64, []float64](func(as []float64) float64 {
if len(as) == 0 {
return 0
}
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
return sum / float64(len(as))
})
result := windowAvg(input)
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
assert.Equal(t, expected, result)
})
t.Run("Check if suffix is sorted", func(t *testing.T) {
input := []int{1, 2, 3, 2, 1}
isSorted := Extend[[]int, []bool](func(as []int) bool {
for i := 1; i < len(as); i++ {
if as[i] < as[i-1] {
return false
}
}
return true
})
result := isSorted(input)
expected := []bool{false, false, false, false, true}
assert.Equal(t, expected, result)
})
t.Run("Count remaining elements", func(t *testing.T) {
events := []string{"start", "middle", "end"}
remaining := Extend[[]string, []int](Size[[]string, string])
result := remaining(events)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
}

View File

@@ -23,12 +23,45 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Of constructs a single element array
// Of constructs a single element NonEmptyArray.
// This is the simplest way to create a NonEmptyArray with exactly one element.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - first: The single element to include in the array
//
// Returns:
// - NonEmptyArray[A]: A NonEmptyArray containing only the provided element
//
// Example:
//
// arr := Of(42) // NonEmptyArray[int]{42}
// str := Of("hello") // NonEmptyArray[string]{"hello"}
func Of[A any](first A) NonEmptyArray[A] {
return G.Of[NonEmptyArray[A]](first)
}
// From constructs a [NonEmptyArray] from a set of variadic arguments
// From constructs a NonEmptyArray from a set of variadic arguments.
// The first argument is required to ensure the array is non-empty, and additional
// elements can be provided as variadic arguments.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - first: The first element (required to ensure non-emptiness)
// - data: Additional elements (optional)
//
// Returns:
// - NonEmptyArray[A]: A NonEmptyArray containing all provided elements
//
// Example:
//
// arr1 := From(1) // NonEmptyArray[int]{1}
// arr2 := From(1, 2, 3) // NonEmptyArray[int]{1, 2, 3}
// arr3 := From("a", "b", "c") // NonEmptyArray[string]{"a", "b", "c"}
func From[A any](first A, data ...A) NonEmptyArray[A] {
count := len(data)
if count == 0 {
@@ -41,79 +74,358 @@ func From[A any](first A, data ...A) NonEmptyArray[A] {
return buffer
}
// IsEmpty always returns false for NonEmptyArray since it's guaranteed to have at least one element.
// This function exists for API consistency with regular arrays.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - _: The NonEmptyArray (unused, as the result is always false)
//
// Returns:
// - bool: Always false
//
//go:inline
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
return false
}
// IsNonEmpty always returns true for NonEmptyArray since it's guaranteed to have at least one element.
// This function exists for API consistency with regular arrays.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - _: The NonEmptyArray (unused, as the result is always true)
//
// Returns:
// - bool: Always true
//
//go:inline
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
return true
}
// MonadMap applies a function to each element of a NonEmptyArray, returning a new NonEmptyArray with the results.
// This is the monadic version of Map that takes the array as the first parameter.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - as: The input NonEmptyArray
// - f: The function to apply to each element
//
// Returns:
// - NonEmptyArray[B]: A new NonEmptyArray with the transformed elements
//
// Example:
//
// arr := From(1, 2, 3)
// doubled := MonadMap(arr, func(x int) int { return x * 2 }) // NonEmptyArray[int]{2, 4, 6}
//
//go:inline
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
}
// Map applies a function to each element of a NonEmptyArray, returning a new NonEmptyArray with the results.
// This is the curried version that returns a function.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: The function to apply to each element
//
// Returns:
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
//
// Example:
//
// double := Map(func(x int) int { return x * 2 })
// result := double(From(1, 2, 3)) // NonEmptyArray[int]{2, 4, 6}
//
//go:inline
func Map[A, B any](f func(a A) B) Operator[A, B] {
return G.Map[NonEmptyArray[A], NonEmptyArray[B]](f)
}
// Reduce applies a function to each element of a NonEmptyArray from left to right,
// accumulating a result starting from an initial value.
//
// Type Parameters:
// - A: The element type of the array
// - B: The accumulator type
//
// Parameters:
// - f: The reducer function that takes (accumulator, element) and returns a new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - func(NonEmptyArray[A]) B: A function that reduces the array to a single value
//
// Example:
//
// sum := Reduce(func(acc int, x int) int { return acc + x }, 0)
// result := sum(From(1, 2, 3, 4)) // 10
//
// concat := Reduce(func(acc string, x string) string { return acc + x }, "")
// result := concat(From("a", "b", "c")) // "abc"
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
return func(as NonEmptyArray[A]) B {
return array.Reduce(as, f, initial)
}
}
// ReduceRight applies a function to each element of a NonEmptyArray from right to left,
// accumulating a result starting from an initial value.
//
// Type Parameters:
// - A: The element type of the array
// - B: The accumulator type
//
// Parameters:
// - f: The reducer function that takes (element, accumulator) and returns a new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - func(NonEmptyArray[A]) B: A function that reduces the array to a single value
//
// Example:
//
// concat := ReduceRight(func(x string, acc string) string { return acc + x }, "")
// result := concat(From("a", "b", "c")) // "cba"
func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
return func(as NonEmptyArray[A]) B {
return array.ReduceRight(as, f, initial)
}
}
// Tail returns all elements of a NonEmptyArray except the first one.
// Returns an empty slice if the array has only one element.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - []A: A slice containing all elements except the first (may be empty)
//
// Example:
//
// arr := From(1, 2, 3, 4)
// tail := Tail(arr) // []int{2, 3, 4}
//
// single := From(1)
// tail := Tail(single) // []int{}
//
//go:inline
func Tail[A any](as NonEmptyArray[A]) []A {
return as[1:]
}
// Head returns the first element of a NonEmptyArray.
// This operation is always safe since NonEmptyArray is guaranteed to have at least one element.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - A: The first element
//
// Example:
//
// arr := From(1, 2, 3)
// first := Head(arr) // 1
//
//go:inline
func Head[A any](as NonEmptyArray[A]) A {
return as[0]
}
// First returns the first element of a NonEmptyArray.
// This is an alias for Head.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - A: The first element
//
// Example:
//
// arr := From(1, 2, 3)
// first := First(arr) // 1
//
//go:inline
func First[A any](as NonEmptyArray[A]) A {
return as[0]
}
// Last returns the last element of a NonEmptyArray.
// This operation is always safe since NonEmptyArray is guaranteed to have at least one element.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - A: The last element
//
// Example:
//
// arr := From(1, 2, 3)
// last := Last(arr) // 3
//
//go:inline
func Last[A any](as NonEmptyArray[A]) A {
return as[len(as)-1]
}
// Size returns the number of elements in a NonEmptyArray.
// The result is always at least 1.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - int: The number of elements (always >= 1)
//
// Example:
//
// arr := From(1, 2, 3)
// size := Size(arr) // 3
//
//go:inline
func Size[A any](as NonEmptyArray[A]) int {
return G.Size(as)
}
// Flatten flattens a NonEmptyArray of NonEmptyArrays into a single NonEmptyArray.
// This operation concatenates all inner arrays into one.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - mma: A NonEmptyArray of NonEmptyArrays
//
// Returns:
// - NonEmptyArray[A]: A flattened NonEmptyArray containing all elements
//
// Example:
//
// nested := From(From(1, 2), From(3, 4), From(5))
// flat := Flatten(nested) // NonEmptyArray[int]{1, 2, 3, 4, 5}
func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
return G.Flatten(mma)
}
// MonadChain applies a function that returns a NonEmptyArray to each element and flattens the results.
// This is the monadic bind operation (flatMap) that takes the array as the first parameter.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - fa: The input NonEmptyArray
// - f: A function that takes an element and returns a NonEmptyArray
//
// Returns:
// - NonEmptyArray[B]: The flattened result
//
// Example:
//
// arr := From(1, 2, 3)
// result := MonadChain(arr, func(x int) NonEmptyArray[int] {
// return From(x, x*10)
// }) // NonEmptyArray[int]{1, 10, 2, 20, 3, 30}
func MonadChain[A, B any](fa NonEmptyArray[A], f Kleisli[A, B]) NonEmptyArray[B] {
return G.MonadChain(fa, f)
}
// Chain applies a function that returns a NonEmptyArray to each element and flattens the results.
// This is the curried version of MonadChain.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: A function that takes an element and returns a NonEmptyArray
//
// Returns:
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
//
// Example:
//
// duplicate := Chain(func(x int) NonEmptyArray[int] { return From(x, x) })
// result := duplicate(From(1, 2, 3)) // NonEmptyArray[int]{1, 1, 2, 2, 3, 3}
func Chain[A, B any](f func(A) NonEmptyArray[B]) Operator[A, B] {
return G.Chain[NonEmptyArray[A]](f)
}
// MonadAp applies a NonEmptyArray of functions to a NonEmptyArray of values.
// Each function is applied to each value, producing a cartesian product of results.
//
// Type Parameters:
// - B: The output element type
// - A: The input element type
//
// Parameters:
// - fab: A NonEmptyArray of functions
// - fa: A NonEmptyArray of values
//
// Returns:
// - NonEmptyArray[B]: The result of applying all functions to all values
//
// Example:
//
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
// vals := From(1, 2)
// result := MonadAp(fns, vals) // NonEmptyArray[int]{2, 4, 11, 12}
func MonadAp[B, A any](fab NonEmptyArray[func(A) B], fa NonEmptyArray[A]) NonEmptyArray[B] {
return G.MonadAp[NonEmptyArray[B]](fab, fa)
}
// Ap applies a NonEmptyArray of functions to a NonEmptyArray of values.
// This is the curried version of MonadAp.
//
// Type Parameters:
// - B: The output element type
// - A: The input element type
//
// Parameters:
// - fa: A NonEmptyArray of values
//
// Returns:
// - func(NonEmptyArray[func(A) B]) NonEmptyArray[B]: A function that applies functions to the values
//
// Example:
//
// vals := From(1, 2)
// applyTo := Ap[int](vals)
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
// result := applyTo(fns) // NonEmptyArray[int]{2, 4, 11, 12}
func Ap[B, A any](fa NonEmptyArray[A]) func(NonEmptyArray[func(A) B]) NonEmptyArray[B] {
return G.Ap[NonEmptyArray[B], NonEmptyArray[func(A) B]](fa)
}
@@ -136,7 +448,23 @@ func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
}
}
// Prepend prepends a single value to an array
// Prepend prepends a single value to the beginning of a NonEmptyArray.
// Returns a new NonEmptyArray with the value at the front.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - head: The value to prepend
//
// Returns:
// - EM.Endomorphism[NonEmptyArray[A]]: A function that prepends the value to a NonEmptyArray
//
// Example:
//
// arr := From(2, 3, 4)
// prepend1 := Prepend(1)
// result := prepend1(arr) // NonEmptyArray[int]{1, 2, 3, 4}
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
}
@@ -226,3 +554,59 @@ func ToNonEmptyArray[A any](as []A) Option[NonEmptyArray[A]] {
}
return option.Some(NonEmptyArray[A](as))
}
// Extract returns the first element of a NonEmptyArray.
// This is an alias for Head and is part of the Comonad interface.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - A: The first element
//
// Example:
//
// arr := From(1, 2, 3)
// first := Extract(arr) // 1
//
//go:inline
func Extract[A any](as NonEmptyArray[A]) A {
return Head(as)
}
// Extend applies a function to all suffixes of a NonEmptyArray.
// For each position i, it applies the function to the subarray starting at position i.
// This is part of the Comonad interface.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: A function that takes a NonEmptyArray and returns a value
//
// Returns:
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
//
// Example:
//
// arr := From(1, 2, 3, 4)
// sumSuffix := Extend(func(xs NonEmptyArray[int]) int {
// sum := 0
// for _, x := range xs {
// sum += x
// }
// return sum
// })
// result := sumSuffix(arr) // NonEmptyArray[int]{10, 9, 7, 4}
// // [1,2,3,4] -> 10, [2,3,4] -> 9, [3,4] -> 7, [4] -> 4
//
//go:inline
func Extend[A, B any](f func(NonEmptyArray[A]) B) Operator[A, B] {
return func(as NonEmptyArray[A]) NonEmptyArray[B] {
return G.MakeBy[NonEmptyArray[B]](len(as), func(i int) B { return f(as[i:]) })
}
}

View File

@@ -16,10 +16,13 @@
package nonempty
import (
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
STR "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -368,3 +371,522 @@ func TestToNonEmptyArrayUseCases(t *testing.T) {
assert.Equal(t, "default", result2)
})
}
// TestOf tests the Of function
func TestOf(t *testing.T) {
t.Run("Create single element array with int", func(t *testing.T) {
arr := Of(42)
assert.Equal(t, 1, Size(arr))
assert.Equal(t, 42, Head(arr))
})
t.Run("Create single element array with string", func(t *testing.T) {
arr := Of("hello")
assert.Equal(t, 1, Size(arr))
assert.Equal(t, "hello", Head(arr))
})
t.Run("Create single element array with struct", func(t *testing.T) {
type Person struct {
Name string
Age int
}
person := Person{Name: "Alice", Age: 30}
arr := Of(person)
assert.Equal(t, 1, Size(arr))
assert.Equal(t, "Alice", Head(arr).Name)
})
}
// TestFrom tests the From function
func TestFrom(t *testing.T) {
t.Run("Create array with single element", func(t *testing.T) {
arr := From(1)
assert.Equal(t, 1, Size(arr))
assert.Equal(t, 1, Head(arr))
})
t.Run("Create array with multiple elements", func(t *testing.T) {
arr := From(1, 2, 3, 4, 5)
assert.Equal(t, 5, Size(arr))
assert.Equal(t, 1, Head(arr))
assert.Equal(t, 5, Last(arr))
})
t.Run("Create array with strings", func(t *testing.T) {
arr := From("a", "b", "c")
assert.Equal(t, 3, Size(arr))
assert.Equal(t, "a", Head(arr))
assert.Equal(t, "c", Last(arr))
})
}
// TestIsEmpty tests the IsEmpty function
func TestIsEmpty(t *testing.T) {
t.Run("IsEmpty always returns false", func(t *testing.T) {
arr := From(1, 2, 3)
assert.False(t, IsEmpty(arr))
})
t.Run("IsEmpty returns false for single element", func(t *testing.T) {
arr := Of(1)
assert.False(t, IsEmpty(arr))
})
}
// TestIsNonEmpty tests the IsNonEmpty function
func TestIsNonEmpty(t *testing.T) {
t.Run("IsNonEmpty always returns true", func(t *testing.T) {
arr := From(1, 2, 3)
assert.True(t, IsNonEmpty(arr))
})
t.Run("IsNonEmpty returns true for single element", func(t *testing.T) {
arr := Of(1)
assert.True(t, IsNonEmpty(arr))
})
}
// TestMonadMap tests the MonadMap function
func TestMonadMap(t *testing.T) {
t.Run("Map integers to doubles", func(t *testing.T) {
arr := From(1, 2, 3, 4)
result := MonadMap(arr, func(x int) int { return x * 2 })
assert.Equal(t, 4, Size(result))
assert.Equal(t, 2, Head(result))
assert.Equal(t, 8, Last(result))
})
t.Run("Map strings to lengths", func(t *testing.T) {
arr := From("a", "bb", "ccc")
result := MonadMap(arr, func(s string) int { return len(s) })
assert.Equal(t, 3, Size(result))
assert.Equal(t, 1, Head(result))
assert.Equal(t, 3, Last(result))
})
t.Run("Map single element", func(t *testing.T) {
arr := Of(5)
result := MonadMap(arr, func(x int) int { return x * 10 })
assert.Equal(t, 1, Size(result))
assert.Equal(t, 50, Head(result))
})
}
// TestMap tests the Map function
func TestMap(t *testing.T) {
t.Run("Curried map with integers", func(t *testing.T) {
double := Map(func(x int) int { return x * 2 })
arr := From(1, 2, 3)
result := double(arr)
assert.Equal(t, 3, Size(result))
assert.Equal(t, 2, Head(result))
assert.Equal(t, 6, Last(result))
})
t.Run("Curried map with strings", func(t *testing.T) {
toUpper := Map(func(s string) string { return s + "!" })
arr := From("hello", "world")
result := toUpper(arr)
assert.Equal(t, 2, Size(result))
assert.Equal(t, "hello!", Head(result))
assert.Equal(t, "world!", Last(result))
})
}
// TestReduce tests the Reduce function
func TestReduce(t *testing.T) {
t.Run("Sum integers", func(t *testing.T) {
sum := Reduce(func(acc int, x int) int { return acc + x }, 0)
arr := From(1, 2, 3, 4, 5)
result := sum(arr)
assert.Equal(t, 15, result)
})
t.Run("Concatenate strings", func(t *testing.T) {
concat := Reduce(func(acc string, x string) string { return acc + x }, "")
arr := From("a", "b", "c")
result := concat(arr)
assert.Equal(t, "abc", result)
})
t.Run("Product of numbers", func(t *testing.T) {
product := Reduce(func(acc int, x int) int { return acc * x }, 1)
arr := From(2, 3, 4)
result := product(arr)
assert.Equal(t, 24, result)
})
t.Run("Reduce single element", func(t *testing.T) {
sum := Reduce(func(acc int, x int) int { return acc + x }, 10)
arr := Of(5)
result := sum(arr)
assert.Equal(t, 15, result)
})
}
// TestReduceRight tests the ReduceRight function
func TestReduceRight(t *testing.T) {
t.Run("Concatenate strings right to left", func(t *testing.T) {
concat := ReduceRight(func(x string, acc string) string { return acc + x }, "")
arr := From("a", "b", "c")
result := concat(arr)
assert.Equal(t, "cba", result)
})
t.Run("Build list right to left", func(t *testing.T) {
buildList := ReduceRight(func(x int, acc []int) []int { return append(acc, x) }, []int{})
arr := From(1, 2, 3)
result := buildList(arr)
assert.Equal(t, []int{3, 2, 1}, result)
})
}
// TestTail tests the Tail function
func TestTail(t *testing.T) {
t.Run("Get tail of multi-element array", func(t *testing.T) {
arr := From(1, 2, 3, 4)
tail := Tail(arr)
assert.Equal(t, 3, len(tail))
assert.Equal(t, []int{2, 3, 4}, tail)
})
t.Run("Get tail of single element array", func(t *testing.T) {
arr := Of(1)
tail := Tail(arr)
assert.Equal(t, 0, len(tail))
assert.Equal(t, []int{}, tail)
})
t.Run("Get tail of two element array", func(t *testing.T) {
arr := From(1, 2)
tail := Tail(arr)
assert.Equal(t, 1, len(tail))
assert.Equal(t, []int{2}, tail)
})
}
// TestHead tests the Head function
func TestHead(t *testing.T) {
t.Run("Get head of multi-element array", func(t *testing.T) {
arr := From(1, 2, 3)
head := Head(arr)
assert.Equal(t, 1, head)
})
t.Run("Get head of single element array", func(t *testing.T) {
arr := Of(42)
head := Head(arr)
assert.Equal(t, 42, head)
})
t.Run("Get head of string array", func(t *testing.T) {
arr := From("first", "second", "third")
head := Head(arr)
assert.Equal(t, "first", head)
})
}
// TestFirst tests the First function
func TestFirst(t *testing.T) {
t.Run("First is alias for Head", func(t *testing.T) {
arr := From(1, 2, 3)
assert.Equal(t, Head(arr), First(arr))
})
t.Run("Get first element", func(t *testing.T) {
arr := From("a", "b", "c")
first := First(arr)
assert.Equal(t, "a", first)
})
}
// TestLast tests the Last function
func TestLast(t *testing.T) {
t.Run("Get last of multi-element array", func(t *testing.T) {
arr := From(1, 2, 3, 4, 5)
last := Last(arr)
assert.Equal(t, 5, last)
})
t.Run("Get last of single element array", func(t *testing.T) {
arr := Of(42)
last := Last(arr)
assert.Equal(t, 42, last)
})
t.Run("Get last of string array", func(t *testing.T) {
arr := From("first", "second", "third")
last := Last(arr)
assert.Equal(t, "third", last)
})
}
// TestSize tests the Size function
func TestSize(t *testing.T) {
t.Run("Size of multi-element array", func(t *testing.T) {
arr := From(1, 2, 3, 4, 5)
size := Size(arr)
assert.Equal(t, 5, size)
})
t.Run("Size of single element array", func(t *testing.T) {
arr := Of(1)
size := Size(arr)
assert.Equal(t, 1, size)
})
t.Run("Size of large array", func(t *testing.T) {
elements := make([]int, 1000)
arr := From(1, elements...)
size := Size(arr)
assert.Equal(t, 1001, size)
})
}
// TestFlatten tests the Flatten function
func TestFlatten(t *testing.T) {
t.Run("Flatten nested arrays", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5))
flat := Flatten(nested)
assert.Equal(t, 5, Size(flat))
assert.Equal(t, 1, Head(flat))
assert.Equal(t, 5, Last(flat))
})
t.Run("Flatten single nested array", func(t *testing.T) {
nested := Of(From(1, 2, 3))
flat := Flatten(nested)
assert.Equal(t, 3, Size(flat))
assert.Equal(t, []int{1, 2, 3}, []int(flat))
})
t.Run("Flatten arrays of different sizes", func(t *testing.T) {
nested := From(Of(1), From(2, 3, 4), From(5, 6))
flat := Flatten(nested)
assert.Equal(t, 6, Size(flat))
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, []int(flat))
})
}
// TestMonadChain tests the MonadChain function
func TestMonadChain(t *testing.T) {
t.Run("Chain with duplication", func(t *testing.T) {
arr := From(1, 2, 3)
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
return From(x, x*10)
})
assert.Equal(t, 6, Size(result))
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, []int(result))
})
t.Run("Chain with expansion", func(t *testing.T) {
arr := From(1, 2)
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
return From(x, x+1, x+2)
})
assert.Equal(t, 6, Size(result))
assert.Equal(t, []int{1, 2, 3, 2, 3, 4}, []int(result))
})
t.Run("Chain single element", func(t *testing.T) {
arr := Of(5)
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
return From(x, x*2)
})
assert.Equal(t, 2, Size(result))
assert.Equal(t, []int{5, 10}, []int(result))
})
}
// TestChain tests the Chain function
func TestChain(t *testing.T) {
t.Run("Curried chain with duplication", func(t *testing.T) {
duplicate := Chain(func(x int) NonEmptyArray[int] {
return From(x, x)
})
arr := From(1, 2, 3)
result := duplicate(arr)
assert.Equal(t, 6, Size(result))
assert.Equal(t, []int{1, 1, 2, 2, 3, 3}, []int(result))
})
t.Run("Curried chain with transformation", func(t *testing.T) {
expand := Chain(func(x int) NonEmptyArray[string] {
return Of(fmt.Sprintf("%d", x))
})
arr := From(1, 2, 3)
result := expand(arr)
assert.Equal(t, 3, Size(result))
assert.Equal(t, "1", Head(result))
})
}
// TestMonadAp tests the MonadAp function
func TestMonadAp(t *testing.T) {
t.Run("Apply functions to values", func(t *testing.T) {
fns := From(
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
)
vals := From(1, 2)
result := MonadAp(fns, vals)
assert.Equal(t, 4, Size(result))
assert.Equal(t, []int{2, 4, 11, 12}, []int(result))
})
t.Run("Apply single function to multiple values", func(t *testing.T) {
fns := Of(func(x int) int { return x * 3 })
vals := From(1, 2, 3)
result := MonadAp(fns, vals)
assert.Equal(t, 3, Size(result))
assert.Equal(t, []int{3, 6, 9}, []int(result))
})
}
// TestAp tests the Ap function
func TestAp(t *testing.T) {
t.Run("Curried apply", func(t *testing.T) {
vals := From(1, 2)
applyTo := Ap[int](vals)
fns := From(
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
)
result := applyTo(fns)
assert.Equal(t, 4, Size(result))
assert.Equal(t, []int{2, 4, 11, 12}, []int(result))
})
}
// TestFoldMap tests the FoldMap function
func TestFoldMap(t *testing.T) {
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := From(1, 2, 3, 4)
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
})
t.Run("FoldMap with string concatenation", func(t *testing.T) {
concatSemigroup := STR.Semigroup
arr := From(1, 2, 3)
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
assert.Equal(t, "123", result)
})
}
// TestFold tests the Fold function
func TestFold(t *testing.T) {
t.Run("Fold with sum semigroup", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := From(1, 2, 3, 4, 5)
result := Fold(sumSemigroup)(arr)
assert.Equal(t, 15, result)
})
t.Run("Fold with string concatenation", func(t *testing.T) {
concatSemigroup := STR.Semigroup
arr := From("a", "b", "c")
result := Fold(concatSemigroup)(arr)
assert.Equal(t, "abc", result)
})
t.Run("Fold single element", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := Of(42)
result := Fold(sumSemigroup)(arr)
assert.Equal(t, 42, result)
})
}
// TestPrepend tests the Prepend function
func TestPrepend(t *testing.T) {
t.Run("Prepend to multi-element array", func(t *testing.T) {
arr := From(2, 3, 4)
prepend1 := Prepend(1)
result := prepend1(arr)
assert.Equal(t, 4, Size(result))
assert.Equal(t, 1, Head(result))
assert.Equal(t, 4, Last(result))
})
t.Run("Prepend to single element array", func(t *testing.T) {
arr := Of(2)
prepend1 := Prepend(1)
result := prepend1(arr)
assert.Equal(t, 2, Size(result))
assert.Equal(t, []int{1, 2}, []int(result))
})
t.Run("Prepend string", func(t *testing.T) {
arr := From("world")
prependHello := Prepend("hello")
result := prependHello(arr)
assert.Equal(t, 2, Size(result))
assert.Equal(t, "hello", Head(result))
})
}
// TestExtract tests the Extract function
func TestExtract(t *testing.T) {
t.Run("Extract from multi-element array", func(t *testing.T) {
arr := From(1, 2, 3)
result := Extract(arr)
assert.Equal(t, 1, result)
})
t.Run("Extract from single element array", func(t *testing.T) {
arr := Of(42)
result := Extract(arr)
assert.Equal(t, 42, result)
})
t.Run("Extract is same as Head", func(t *testing.T) {
arr := From("a", "b", "c")
assert.Equal(t, Head(arr), Extract(arr))
})
}
// TestExtend tests the Extend function
func TestExtend(t *testing.T) {
t.Run("Extend with sum of suffixes", func(t *testing.T) {
arr := From(1, 2, 3, 4)
sumSuffix := Extend(func(xs NonEmptyArray[int]) int {
sum := 0
for _, x := range xs {
sum += x
}
return sum
})
result := sumSuffix(arr)
assert.Equal(t, 4, Size(result))
assert.Equal(t, []int{10, 9, 7, 4}, []int(result))
})
t.Run("Extend with head of suffixes", func(t *testing.T) {
arr := From(1, 2, 3)
getHeads := Extend(Head[int])
result := getHeads(arr)
assert.Equal(t, 3, Size(result))
assert.Equal(t, []int{1, 2, 3}, []int(result))
})
t.Run("Extend with size of suffixes", func(t *testing.T) {
arr := From("a", "b", "c", "d")
getSizes := Extend(Size[string])
result := getSizes(arr)
assert.Equal(t, 4, Size(result))
assert.Equal(t, []int{4, 3, 2, 1}, []int(result))
})
t.Run("Extend single element", func(t *testing.T) {
arr := Of(5)
double := Extend(func(xs NonEmptyArray[int]) int {
return Head(xs) * 2
})
result := double(arr)
assert.Equal(t, 1, Size(result))
assert.Equal(t, 10, Head(result))
})
}

View File

@@ -13,6 +13,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package generic provides generic string utility functions that work with any type
// that has string as its underlying type (using the ~string constraint).
// This allows these functions to work with custom string types while maintaining type safety.
package generic
// ToBytes converts the string to bytes

View File

@@ -0,0 +1,164 @@
// 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 generic
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Custom string type for testing generic constraints
type MyString string
func TestToBytes(t *testing.T) {
t.Run("regular string", func(t *testing.T) {
result := ToBytes("hello")
expected := []byte{'h', 'e', 'l', 'l', 'o'}
assert.Equal(t, expected, result)
})
t.Run("empty string", func(t *testing.T) {
result := ToBytes("")
assert.Equal(t, []byte{}, result)
})
t.Run("custom string type", func(t *testing.T) {
result := ToBytes(MyString("test"))
expected := []byte{'t', 'e', 's', 't'}
assert.Equal(t, expected, result)
})
t.Run("unicode string", func(t *testing.T) {
result := ToBytes("你好")
// UTF-8 encoding: 你 = E4 BD A0, 好 = E5 A5 BD
assert.Equal(t, 6, len(result))
})
}
func TestToRunes(t *testing.T) {
t.Run("regular string", func(t *testing.T) {
result := ToRunes("hello")
expected := []rune{'h', 'e', 'l', 'l', 'o'}
assert.Equal(t, expected, result)
})
t.Run("empty string", func(t *testing.T) {
result := ToRunes("")
assert.Equal(t, []rune{}, result)
})
t.Run("custom string type", func(t *testing.T) {
result := ToRunes(MyString("test"))
expected := []rune{'t', 'e', 's', 't'}
assert.Equal(t, expected, result)
})
t.Run("unicode string", func(t *testing.T) {
result := ToRunes("你好")
assert.Equal(t, 2, len(result))
assert.Equal(t, '你', result[0])
assert.Equal(t, '好', result[1])
})
t.Run("mixed ascii and unicode", func(t *testing.T) {
result := ToRunes("Hello世界")
assert.Equal(t, 7, len(result))
})
}
func TestIsEmpty(t *testing.T) {
t.Run("empty string", func(t *testing.T) {
assert.True(t, IsEmpty(""))
})
t.Run("non-empty string", func(t *testing.T) {
assert.False(t, IsEmpty("hello"))
})
t.Run("whitespace string", func(t *testing.T) {
assert.False(t, IsEmpty(" "))
assert.False(t, IsEmpty("\t"))
assert.False(t, IsEmpty("\n"))
})
t.Run("custom string type empty", func(t *testing.T) {
assert.True(t, IsEmpty(MyString("")))
})
t.Run("custom string type non-empty", func(t *testing.T) {
assert.False(t, IsEmpty(MyString("test")))
})
}
func TestIsNonEmpty(t *testing.T) {
t.Run("empty string", func(t *testing.T) {
assert.False(t, IsNonEmpty(""))
})
t.Run("non-empty string", func(t *testing.T) {
assert.True(t, IsNonEmpty("hello"))
})
t.Run("whitespace string", func(t *testing.T) {
assert.True(t, IsNonEmpty(" "))
assert.True(t, IsNonEmpty("\t"))
assert.True(t, IsNonEmpty("\n"))
})
t.Run("custom string type empty", func(t *testing.T) {
assert.False(t, IsNonEmpty(MyString("")))
})
t.Run("custom string type non-empty", func(t *testing.T) {
assert.True(t, IsNonEmpty(MyString("test")))
})
t.Run("single character", func(t *testing.T) {
assert.True(t, IsNonEmpty("a"))
})
}
func TestSize(t *testing.T) {
t.Run("empty string", func(t *testing.T) {
assert.Equal(t, 0, Size(""))
})
t.Run("ascii string", func(t *testing.T) {
assert.Equal(t, 5, Size("hello"))
assert.Equal(t, 11, Size("hello world"))
})
t.Run("custom string type", func(t *testing.T) {
assert.Equal(t, 4, Size(MyString("test")))
})
t.Run("unicode string - returns byte count", func(t *testing.T) {
// Note: Size returns byte length, not rune count
assert.Equal(t, 6, Size("你好")) // 2 Chinese characters = 6 bytes in UTF-8
assert.Equal(t, 5, Size("café")) // 'c', 'a', 'f' = 3 bytes, 'é' = 2 bytes in UTF-8
})
t.Run("single character", func(t *testing.T) {
assert.Equal(t, 1, Size("a"))
})
t.Run("whitespace", func(t *testing.T) {
assert.Equal(t, 1, Size(" "))
assert.Equal(t, 1, Size("\t"))
assert.Equal(t, 1, Size("\n"))
})
}

View File

@@ -19,12 +19,46 @@ import (
"testing"
M "github.com/IBM/fp-go/v2/monoid/testing"
"github.com/stretchr/testify/assert"
)
func TestMonoid(t *testing.T) {
M.AssertLaws(t, Monoid)([]string{"", "a", "some value"})
}
func TestMonoidConcat(t *testing.T) {
t.Run("basic concatenation", func(t *testing.T) {
result := Monoid.Concat("hello", " world")
assert.Equal(t, "hello world", result)
})
t.Run("empty identity", func(t *testing.T) {
empty := Monoid.Empty()
assert.Equal(t, "", empty)
})
t.Run("left identity", func(t *testing.T) {
// empty • a = a
result := Monoid.Concat(Monoid.Empty(), "test")
assert.Equal(t, "test", result)
})
t.Run("right identity", func(t *testing.T) {
// a • empty = a
result := Monoid.Concat("test", Monoid.Empty())
assert.Equal(t, "test", result)
})
t.Run("associativity", func(t *testing.T) {
// (a • b) • c = a • (b • c)
a, b, c := "foo", "bar", "baz"
left := Monoid.Concat(Monoid.Concat(a, b), c)
right := Monoid.Concat(a, Monoid.Concat(b, c))
assert.Equal(t, left, right)
assert.Equal(t, "foobarbaz", left)
})
}
func TestIntersperseMonoid(t *testing.T) {
// Test with comma separator
commaMonoid := IntersperseMonoid(", ")
@@ -34,3 +68,51 @@ func TestIntersperseMonoid(t *testing.T) {
dashMonoid := IntersperseMonoid("-")
M.AssertLaws(t, dashMonoid)([]string{"", "x", "y", "test"})
}
func TestIntersperseMonoidConcat(t *testing.T) {
t.Run("comma separator", func(t *testing.T) {
commaMonoid := IntersperseMonoid(", ")
result := commaMonoid.Concat("a", "b")
assert.Equal(t, "a, b", result)
})
t.Run("empty identity", func(t *testing.T) {
commaMonoid := IntersperseMonoid(", ")
empty := commaMonoid.Empty()
assert.Equal(t, "", empty)
})
t.Run("left identity with separator", func(t *testing.T) {
// empty • a = a (no separator added)
commaMonoid := IntersperseMonoid(", ")
result := commaMonoid.Concat(commaMonoid.Empty(), "test")
assert.Equal(t, "test", result)
})
t.Run("right identity with separator", func(t *testing.T) {
// a • empty = a (no separator added)
commaMonoid := IntersperseMonoid(", ")
result := commaMonoid.Concat("test", commaMonoid.Empty())
assert.Equal(t, "test", result)
})
t.Run("associativity with separator", func(t *testing.T) {
// (a • b) • c = a • (b • c)
commaMonoid := IntersperseMonoid(", ")
a, b, c := "x", "y", "z"
left := commaMonoid.Concat(commaMonoid.Concat(a, b), c)
right := commaMonoid.Concat(a, commaMonoid.Concat(b, c))
assert.Equal(t, left, right)
assert.Equal(t, "x, y, z", left)
})
t.Run("multiple separators", func(t *testing.T) {
dashMonoid := IntersperseMonoid("-")
result := dashMonoid.Concat("foo", "bar")
assert.Equal(t, "foo-bar", result)
spaceMonoid := IntersperseMonoid(" ")
result = spaceMonoid.Concat("hello", "world")
assert.Equal(t, "hello world", result)
})
}

View File

@@ -16,14 +16,13 @@
package string
import (
"fmt"
S "github.com/IBM/fp-go/v2/semigroup"
)
// concat concatenates two strings
// concat concatenates two strings using simple string concatenation.
// This is an internal helper function used by the Semigroup and Monoid implementations.
func concat(left, right string) string {
return fmt.Sprintf("%s%s", left, right)
return left + right
}
// Semigroup is the semigroup implementing string concatenation