diff --git a/fn/go.mod b/fn/go.mod index 4d1528efe..e14cb555a 100644 --- a/fn/go.mod +++ b/fn/go.mod @@ -4,5 +4,12 @@ go 1.19 require ( github.com/lightninglabs/neutrino/cache v1.1.2 + github.com/stretchr/testify v1.8.1 golang.org/x/exp v0.0.0-20231226003508-02704c960a9b ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/fn/go.sum b/fn/go.sum index d066b9820..73345fe5e 100644 --- a/fn/go.sum +++ b/fn/go.sum @@ -1,8 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g= github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/fn/slice.go b/fn/slice.go new file mode 100644 index 000000000..fe821bff7 --- /dev/null +++ b/fn/slice.go @@ -0,0 +1,170 @@ +package fn + +// All returns true when the supplied predicate evaluates to true for all of +// the values in the slice. +func All[A any](pred func(A) bool, s []A) bool { + for _, val := range s { + if !pred(val) { + return false + } + } + + return true +} + +// Any returns true when the supplied predicate evaluates to true for any of +// the values in the slice. +func Any[A any](pred func(A) bool, s []A) bool { + for _, val := range s { + if pred(val) { + return true + } + } + + return false +} + +// Map applies the function argument to all members of the slice and returns a +// slice of those return values. +func Map[A, B any](f func(A) B, s []A) []B { + res := make([]B, 0, len(s)) + + for _, val := range s { + res = append(res, f(val)) + } + + return res +} + +// Filter creates a new slice of values where all the members of the returned +// slice pass the predicate that is supplied in the argument. +func Filter[A any](pred func(A) bool, s []A) []A { + res := make([]A, 0) + + for _, val := range s { + if pred(val) { + res = append(res, val) + } + } + + return res +} + +// Foldl iterates through all members of the slice left to right and reduces +// them pairwise with an accumulator value that is seeded with the seed value in +// the argument. +func Foldl[A, B any](f func(B, A) B, seed B, s []A) B { + acc := seed + + for _, val := range s { + acc = f(acc, val) + } + + return acc +} + +// Foldr, is exactly like Foldl except that it iterates over the slice from +// right to left. +func Foldr[A, B any](f func(A, B) B, seed B, s []A) B { + acc := seed + + for i := range s { + acc = f(s[len(s)-1-i], acc) + } + + return acc +} + +// Find returns the first value that passes the supplied predicate, or None if +// the value wasn't found. +func Find[A any](pred func(A) bool, s []A) Option[A] { + for _, val := range s { + if pred(val) { + return Some(val) + } + } + + return None[A]() +} + +// Flatten takes a slice of slices and returns a concatenation of those slices. +func Flatten[A any](s [][]A) []A { + sz := Foldr( + func(l []A, acc uint64) uint64 { + return uint64(len(l)) + acc + }, 0, s, + ) + + res := make([]A, 0, sz) + + for _, val := range s { + res = append(res, val...) + } + + return res +} + +// Replicate generates a slice of values initialized by the prototype value. +func Replicate[A any](n uint, val A) []A { + res := make([]A, n) + + for i := range res { + res[i] = val + } + + return res +} + +// Span, applied to a predicate and a slice, returns two slices where the first +// element is the longest prefix (possibly empty) of slice elements that +// satisfy the predicate and second element is the remainder of the slice. +func Span[A any](pred func(A) bool, s []A) ([]A, []A) { + for i := range s { + if !pred(s[i]) { + fst := make([]A, i) + snd := make([]A, len(s)-i) + + copy(fst, s[:i]) + copy(snd, s[i:]) + + return fst, snd + } + } + + res := make([]A, len(s)) + copy(res, s) + + return res, []A{} +} + +// SplitAt(n, s) returns a tuple where first element is s prefix of length n +// and second element is the remainder of the list. +func SplitAt[A any](n uint, s []A) ([]A, []A) { + fst := make([]A, n) + snd := make([]A, len(s)-int(n)) + + copy(fst, s[:n]) + copy(snd, s[n:]) + + return fst, snd +} + +// ZipWith combines slice elements with the same index using the function +// argument, returning a slice of the results. +func ZipWith[A, B, C any](f func(A, B) C, a []A, b []B) []C { + var l uint + + if la, lb := len(a), len(b); la < lb { + l = uint(la) + } else { + l = uint(lb) + } + + res := make([]C, l) + + for i := 0; i < int(l); i++ { + res[i] = f(a[i], b[i]) + } + + return res +} diff --git a/fn/slice_test.go b/fn/slice_test.go new file mode 100644 index 000000000..0178cc946 --- /dev/null +++ b/fn/slice_test.go @@ -0,0 +1,138 @@ +package fn + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/require" +) + +func even(a int) bool { return a%2 == 0 } +func odd(a int) bool { return a%2 != 0 } + +func TestAll(t *testing.T) { + x := []int{0, 2, 4, 6, 8} + require.True(t, All(even, x)) + require.False(t, All(odd, x)) + + y := []int{1, 3, 5, 7, 9} + require.False(t, All(even, y)) + require.True(t, All(odd, y)) + + z := []int{0, 2, 4, 6, 9} + require.False(t, All(even, z)) + require.False(t, All(odd, z)) +} + +func TestAny(t *testing.T) { + x := []int{1, 3, 5, 7, 9} + require.False(t, Any(even, x)) + require.True(t, Any(odd, x)) + + y := []int{0, 3, 5, 7, 9} + require.True(t, Any(even, y)) + require.True(t, Any(odd, y)) + + z := []int{0, 2, 4, 6, 8} + require.True(t, Any(even, z)) + require.False(t, Any(odd, z)) +} + +func TestMap(t *testing.T) { + inc := func(i int) int { return i + 1 } + + x := []int{0, 2, 4, 6, 8} + + y := Map(inc, x) + + z := []int{1, 3, 5, 7, 9} + + require.True(t, slices.Equal(y, z)) +} + +func TestFilter(t *testing.T) { + x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + y := Filter(even, x) + + require.True(t, All(even, y)) + + z := Filter(odd, y) + + require.Zero(t, len(z)) +} + +func TestFoldl(t *testing.T) { + seed := []int{} + stupid := func(s []int, a int) []int { return append(s, a) } + + x := []int{0, 1, 2, 3, 4} + + r := Foldl(stupid, seed, x) + + require.True(t, slices.Equal(x, r)) +} + +func TestFoldr(t *testing.T) { + seed := []int{} + stupid := func(a int, s []int) []int { return append(s, a) } + + x := []int{0, 1, 2, 3, 4} + + z := Foldr(stupid, seed, x) + + slices.Reverse[[]int](x) + + require.True(t, slices.Equal(x, z)) +} + +func TestFind(t *testing.T) { + x := []int{10, 11, 12, 13, 14, 15} + + div3 := func(a int) bool { return a%3 == 0 } + div8 := func(a int) bool { return a%8 == 0 } + + require.Equal(t, Find(div3, x), Some(12)) + + require.Equal(t, Find(div8, x), None[int]()) +} + +func TestFlatten(t *testing.T) { + x := [][]int{{0}, {1}, {2}} + + y := Flatten(x) + + require.True(t, slices.Equal(y, []int{0, 1, 2})) +} + +func TestReplicate(t *testing.T) { + require.True(t, slices.Equal([]int{1, 1, 1, 1, 1}, Replicate(5, 1))) +} + +func TestSpan(t *testing.T) { + x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + lt5 := func(a int) bool { return a < 5 } + + low, high := Span(lt5, x) + + require.True(t, slices.Equal(low, []int{0, 1, 2, 3, 4})) + require.True(t, slices.Equal(high, []int{5, 6, 7, 8, 9})) +} + +func TestSplitAt(t *testing.T) { + x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + fst, snd := SplitAt(5, x) + + require.True(t, slices.Equal(fst, []int{0, 1, 2, 3, 4})) + require.True(t, slices.Equal(snd, []int{5, 6, 7, 8, 9})) +} + +func TestZipWith(t *testing.T) { + eq := func(a, b int) bool { return a == b } + x := []int{0, 1, 2, 3, 4} + y := Replicate(5, 1) + z := ZipWith(eq, x, y) + require.True(t, slices.Equal( + z, []bool{false, true, false, false, false}, + )) +}