mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-15 00:53:10 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
840ffbb51d |
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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:]) })
|
||||
}
|
||||
}
|
||||
|
||||
298
v2/array/generic/array_test.go
Normal file
298
v2/array/generic/array_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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:]) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
164
v2/string/generic/string_test.go
Normal file
164
v2/string/generic/string_test.go
Normal 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"))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user