From 94acbe90a8b72438faac09f1acc83e61c4fc6945 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Fri, 12 Apr 2024 17:50:41 -0600 Subject: [PATCH 01/10] fn: add fundamental functional primitives --- fn/fn.go | 30 +++++++++++++++++++++++++++++ fn/t2.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 fn/fn.go create mode 100644 fn/t2.go diff --git a/fn/fn.go b/fn/fn.go new file mode 100644 index 000000000..cb8151a23 --- /dev/null +++ b/fn/fn.go @@ -0,0 +1,30 @@ +package fn + +// Unit is a type alias for the empty struct to make it a bit less noisy to +// communicate the informationaless type. +type Unit = struct{} + +// Comp is left to right function composition. Comp(f, g)(x) == g(f(x)). This +// can make it easier to create on the fly closures that we may use as +// arguments to other functions defined in this package (or otherwise). +func Comp[A, B, C any](f func(A) B, g func(B) C) func(A) C { + return func(a A) C { + return g(f(a)) + } +} + +// Iden is the left and right identity of Comp. It is a function that simply +// returns its argument. The utility of this function is only apparent in +// conjunction with other functions in this package. +func Iden[A any](a A) A { + return a +} + +// Const is a function that accepts an argument and returns a function that +// always returns that value irrespective of the returned function's argument. +// This is also quite useful in conjunction with higher order functions. +func Const[A, B any](a A) func(B) A { + return func(_ B) A { + return a + } +} diff --git a/fn/t2.go b/fn/t2.go new file mode 100644 index 000000000..6cce3b49a --- /dev/null +++ b/fn/t2.go @@ -0,0 +1,57 @@ +package fn + +// T2 is the simplest 2-tuple type. It is useful for capturing ad hoc +// type conjunctions in a single value that can be easily dot-chained. +type T2[A, B any] struct { + first A + second B +} + +// NewT2 is the canonical constructor for a T2. We include it because the fields +// themselves are unexported. +func NewT2[A, B any](a A, b B) T2[A, B] { + return T2[A, B]{ + first: a, + second: b, + } +} + +// First returns the first value in the T2. +func (t2 T2[A, B]) First() A { + return t2.first +} + +// Second returns the second value in the T2. +func (t2 T2[A, B]) Second() B { + return t2.second +} + +// Unpack ejects the 2-tuple's members into the multiple return values that +// are customary in go idiom. +func (t2 T2[A, B]) Unpack() (A, B) { + return t2.first, t2.second +} + +// Pair takes two functions that share the same argument type and runs them +// both and produces a 2-tuple of the results. +func Pair[A, B, C any](f func(A) B, g func(A) C) func(A) T2[B, C] { + return func(a A) T2[B, C] { + return NewT2[B, C](f(a), g(a)) + } +} + +// MapFirst lifts the argument function into one that applies to the first +// element of a 2-tuple. +func MapFirst[A, B, C any](f func(A) B) func(T2[A, C]) T2[B, C] { + return func(t2 T2[A, C]) T2[B, C] { + return NewT2[B, C](f(t2.First()), t2.Second()) + } +} + +// MapSecond lifts the argument function into one that applies to the second +// element of a 2-tuple. +func MapSecond[A, B, C any](f func(A) B) func(T2[C, A]) T2[C, B] { + return func(t2 T2[C, A]) T2[C, B] { + return NewT2[C, B](t2.First(), f(t2.Second())) + } +} From 5902aa51593243d4151ae41ec693a059140fcec2 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Fri, 12 Apr 2024 17:51:00 -0600 Subject: [PATCH 02/10] fn: add concurrent map operation for slices --- fn/slice.go | 38 ++++++++++++++++++++++++++++++- fn/slice_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/fn/slice.go b/fn/slice.go index f42d34544..32887bdb7 100644 --- a/fn/slice.go +++ b/fn/slice.go @@ -1,6 +1,13 @@ package fn -import "golang.org/x/exp/constraints" +import ( + "context" + "runtime" + "sync" + + "golang.org/x/exp/constraints" + "golang.org/x/sync/semaphore" +) // Number is a type constraint for all numeric types in Go (integers, // float and complex numbers) @@ -205,3 +212,32 @@ func Sum[B Number](items []B) B { func HasDuplicates[A comparable](items []A) bool { return len(NewSet(items...)) != len(items) } + +// ForEachConc maps the argument function over the slice, spawning a new +// goroutine for each element in the slice and then awaits all results before +// returning them. +func ForEachConc[A, B any](f func(A) B, + as []A) []B { + + var wait sync.WaitGroup + ctx := context.Background() + + sem := semaphore.NewWeighted(int64(runtime.NumCPU())) + + bs := make([]B, len(as)) + + for i, a := range as { + i, a := i, a + sem.Acquire(ctx, 1) + wait.Add(1) + go func() { + bs[i] = f(a) + wait.Done() + sem.Release(1) + }() + } + + wait.Wait() + + return bs +} diff --git a/fn/slice_test.go b/fn/slice_test.go index 016ef87d3..47ca1f7f8 100644 --- a/fn/slice_test.go +++ b/fn/slice_test.go @@ -4,6 +4,8 @@ import ( "fmt" "slices" "testing" + "testing/quick" + "time" "github.com/stretchr/testify/require" ) @@ -281,3 +283,60 @@ func TestHasDuplicates(t *testing.T) { }) } } + +// TestPropForEachConcMapIsomorphism ensures the property that ForEachConc and +// Map always yield the same results. +func TestPropForEachConcMapIsomorphism(t *testing.T) { + f := func(incSize int, s []int) bool { + inc := func(i int) int { return i + incSize } + mapped := Map(inc, s) + conc := ForEachConc(inc, s) + + return slices.Equal(mapped, conc) + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +// TestPropForEachConcOutperformsMapWhenExpensive ensures the property that +// ForEachConc will beat Map in a race in circumstances where the computation in +// the argument closure is expensive. +func TestPropForEachConcOutperformsMapWhenExpensive(t *testing.T) { + f := func(incSize int, s []int) bool { + if len(s) < 2 { + // Intuitively we don't expect the extra overhead of + // ForEachConc to justify itself for list sizes of 1 or + // 0. + return true + } + + inc := func(i int) int { + time.Sleep(time.Millisecond) + return i + incSize + } + c := make(chan bool, 1) + + go func() { + Map(inc, s) + select { + case c <- false: + default: + } + }() + + go func() { + ForEachConc(inc, s) + select { + case c <- true: + default: + } + }() + + return <-c + } + + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} From c3603ccf96aac448d7d323e7487444b309b31136 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Mon, 15 Apr 2024 15:34:05 -0600 Subject: [PATCH 03/10] fn: add FindIdx function --- fn/slice.go | 16 ++++++++++++++-- fn/slice_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/fn/slice.go b/fn/slice.go index 32887bdb7..8f55cc159 100644 --- a/fn/slice.go +++ b/fn/slice.go @@ -53,7 +53,7 @@ func Map[A, B any](f func(A) B, s []A) []B { // 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 { +func Filter[A any](pred Pred[A], s []A) []A { res := make([]A, 0) for _, val := range s { @@ -92,7 +92,7 @@ func Foldr[A, B any](f func(A, B) B, seed B, s []A) B { // 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] { +func Find[A any](pred Pred[A], s []A) Option[A] { for _, val := range s { if pred(val) { return Some(val) @@ -102,6 +102,18 @@ func Find[A any](pred func(A) bool, s []A) Option[A] { return None[A]() } +// FindIdx returns the first value that passes the supplied predicate along with +// its index in the slice. If no satisfactory value is found, None is returned. +func FindIdx[A any](pred Pred[A], s []A) Option[T2[int, A]] { + for i, val := range s { + if pred(val) { + return Some(NewT2[int, A](i, val)) + } + } + + return None[T2[int, A]]() +} + // Flatten takes a slice of slices and returns a concatenation of those slices. func Flatten[A any](s [][]A) []A { sz := Foldr( diff --git a/fn/slice_test.go b/fn/slice_test.go index 47ca1f7f8..b91f35e48 100644 --- a/fn/slice_test.go +++ b/fn/slice_test.go @@ -340,3 +340,37 @@ func TestPropForEachConcOutperformsMapWhenExpensive(t *testing.T) { t.Fatal(err) } } + +func TestPropFindIdxFindIdentity(t *testing.T) { + f := func(div, mod uint8, s []uint8) bool { + if div == 0 || div == 1 { + return true + } + + pred := func(i uint8) bool { + return i%div == mod + } + + foundIdx := FindIdx(pred, s) + + // onlyVal :: Option[T2[A, B]] -> Option[B] + onlyVal := MapOption(func(t2 T2[int, uint8]) uint8 { + return t2.Second() + }) + + valuesEqual := Find(pred, s) == onlyVal(foundIdx) + + idxGetsVal := ElimOption( + foundIdx, + func() bool { return true }, + func(t2 T2[int, uint8]) bool { + return s[t2.First()] == t2.Second() + }) + + return valuesEqual && idxGetsVal + } + + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} From 1dd56f1b2a59a37a59b913b4c5832be171f9d20a Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 17 Apr 2024 13:00:42 -0600 Subject: [PATCH 04/10] fn: fix and finish the either API --- fn/either.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++----- fn/option.go | 32 +++++++++++++++++++++ 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/fn/either.go b/fn/either.go index aedc9fe5b..cb4555f17 100644 --- a/fn/either.go +++ b/fn/either.go @@ -16,6 +16,17 @@ func NewRight[L any, R any](r R) Either[L, R] { return Either[L, R]{left: None[L](), right: Some(r)} } +// ElimEither is the universal Either eliminator. It can be used to safely +// handle all possible values inside the Either by supplying two continuations, +// one for each side of the Either. +func ElimEither[L, R, O any](f func(L) O, g func(R) O, e Either[L, R]) O { + if e.left.IsSome() { + return f(e.left.some) + } + + return g(e.right.some) +} + // WhenLeft executes the given function if the Either is left. func (e Either[L, R]) WhenLeft(f func(L)) { e.left.WhenSome(f) @@ -36,13 +47,69 @@ func (e Either[L, R]) IsRight() bool { return e.right.IsSome() } -// MapLeft maps the left value of the Either to a new value. -func MapLeft[L any, R any, O any](f func(L) O) func(Either[L, R]) Option[O] { - return func(e Either[L, R]) Option[O] { - if e.IsLeft() { - return MapOption(f)(e.left) - } +// LeftToOption converts a Left value to an Option, returning None if the inner +// Either value is a Right value. +func (e Either[L, R]) LeftToOption() Option[L] { + return e.left +} - return None[O]() +// RightToOption converts a Right value to an Option, returning None if the +// inner Either value is a Left value. +func (e Either[L, R]) RightToOption() Option[R] { + return e.right +} + +// UnwrapLeftOr will extract the Left value from the Either if it is present +// returning the supplied default if it is not. +func (e Either[L, R]) UnwrapLeftOr(l L) L { + return e.left.UnwrapOr(l) +} + +// UnwrapRightOr will extract the Right value from the Either if it is present +// returning the supplied default if it is not. +func (e Either[L, R]) UnwrapRightOr(r R) R { + return e.right.UnwrapOr(r) +} + +// Swap reverses the type argument order. This can be useful as an adapter +// between APIs. +func (e Either[L, R]) Swap() Either[R, L] { + return Either[R, L]{ + left: e.right, + right: e.left, + } +} + +// MapLeft maps the left value of the Either to a new value. +func MapLeft[L, R, O any](f func(L) O) func(Either[L, R]) Either[O, R] { + return func(e Either[L, R]) Either[O, R] { + if e.IsLeft() { + return Either[O, R]{ + left: MapOption(f)(e.left), + right: None[R](), + } + } + + return Either[O, R]{ + left: None[O](), + right: e.right, + } + } +} + +// MapRight maps the right value of the Either to a new value. +func MapRight[L, R, O any](f func(R) O) func(Either[L, R]) Either[L, O] { + return func(e Either[L, R]) Either[L, O] { + if e.IsRight() { + return Either[L, O]{ + left: None[L](), + right: MapOption(f)(e.right), + } + } + + return Either[L, O]{ + left: e.left, + right: None[O](), + } } } diff --git a/fn/option.go b/fn/option.go index d500359be..fbd5f2489 100644 --- a/fn/option.go +++ b/fn/option.go @@ -213,3 +213,35 @@ func (o Option[A]) UnsafeFromSome() A { } panic("Option was None()") } + +// OptionToLeft can be used to convert an Option value into an Either, by +// providing the Right value that should be used if the Option value is None. +func OptionToLeft[O, L, R any](o Option[O], r R) Either[O, R] { + if o.IsSome() { + return Either[O, R]{ + left: o, + right: None[R](), + } + } + + return Either[O, R]{ + left: None[O](), + right: Some(r), + } +} + +// OptionToRight can be used to convert an Option value into an Either, by +// providing the Left value that should be used if the Option value is None. +func OptionToRight[O, L, R any](o Option[O], l L) Either[L, O] { + if o.IsSome() { + return Either[L, O]{ + left: None[L](), + right: o, + } + } + + return Either[L, O]{ + left: Some(l), + right: None[O](), + } +} From c4df2f1dce6ee4a7f4a33d66bcf451b9d3e51a95 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Mon, 22 Apr 2024 13:52:19 -0700 Subject: [PATCH 05/10] fn: reimplement internals of either, add tests --- fn/either.go | 76 ++++++++++++++++--------- fn/either_test.go | 139 ++++++++++++++++++++++++++++++++++++++++++++++ fn/option.go | 20 ++----- fn/result.go | 60 ++++++++++++-------- 4 files changed, 228 insertions(+), 67 deletions(-) create mode 100644 fn/either_test.go diff --git a/fn/either.go b/fn/either.go index cb4555f17..f85104385 100644 --- a/fn/either.go +++ b/fn/either.go @@ -2,97 +2,119 @@ package fn // Either is a type that can be either left or right. type Either[L any, R any] struct { - left Option[L] - right Option[R] + isRight bool + left L + right R } // NewLeft returns an Either with a left value. func NewLeft[L any, R any](l L) Either[L, R] { - return Either[L, R]{left: Some(l), right: None[R]()} + return Either[L, R]{left: l} } // NewRight returns an Either with a right value. func NewRight[L any, R any](r R) Either[L, R] { - return Either[L, R]{left: None[L](), right: Some(r)} + return Either[L, R]{isRight: true, right: r} } // ElimEither is the universal Either eliminator. It can be used to safely // handle all possible values inside the Either by supplying two continuations, // one for each side of the Either. func ElimEither[L, R, O any](f func(L) O, g func(R) O, e Either[L, R]) O { - if e.left.IsSome() { - return f(e.left.some) + if !e.isRight { + return f(e.left) } - return g(e.right.some) + return g(e.right) } // WhenLeft executes the given function if the Either is left. func (e Either[L, R]) WhenLeft(f func(L)) { - e.left.WhenSome(f) + if !e.isRight { + f(e.left) + } } // WhenRight executes the given function if the Either is right. func (e Either[L, R]) WhenRight(f func(R)) { - e.right.WhenSome(f) + if e.isRight { + f(e.right) + } } // IsLeft returns true if the Either is left. func (e Either[L, R]) IsLeft() bool { - return e.left.IsSome() + return !e.isRight } // IsRight returns true if the Either is right. func (e Either[L, R]) IsRight() bool { - return e.right.IsSome() + return e.isRight } // LeftToOption converts a Left value to an Option, returning None if the inner // Either value is a Right value. func (e Either[L, R]) LeftToOption() Option[L] { - return e.left + if e.isRight { + return None[L]() + } + + return Some(e.left) } // RightToOption converts a Right value to an Option, returning None if the // inner Either value is a Left value. func (e Either[L, R]) RightToOption() Option[R] { - return e.right + if !e.isRight { + return None[R]() + } + + return Some(e.right) } // UnwrapLeftOr will extract the Left value from the Either if it is present // returning the supplied default if it is not. func (e Either[L, R]) UnwrapLeftOr(l L) L { - return e.left.UnwrapOr(l) + if e.isRight { + return l + } + + return e.left } // UnwrapRightOr will extract the Right value from the Either if it is present // returning the supplied default if it is not. func (e Either[L, R]) UnwrapRightOr(r R) R { - return e.right.UnwrapOr(r) + if !e.isRight { + return r + } + + return e.right } // Swap reverses the type argument order. This can be useful as an adapter // between APIs. func (e Either[L, R]) Swap() Either[R, L] { return Either[R, L]{ - left: e.right, - right: e.left, + isRight: !e.isRight, + left: e.right, + right: e.left, } } // MapLeft maps the left value of the Either to a new value. func MapLeft[L, R, O any](f func(L) O) func(Either[L, R]) Either[O, R] { return func(e Either[L, R]) Either[O, R] { - if e.IsLeft() { + if !e.isRight { return Either[O, R]{ - left: MapOption(f)(e.left), - right: None[R](), + isRight: false, + left: f(e.left), } } return Either[O, R]{ - left: None[O](), - right: e.right, + isRight: true, + right: e.right, } } } @@ -100,16 +122,16 @@ func MapLeft[L, R, O any](f func(L) O) func(Either[L, R]) Either[O, R] { // MapRight maps the right value of the Either to a new value. func MapRight[L, R, O any](f func(R) O) func(Either[L, R]) Either[L, O] { return func(e Either[L, R]) Either[L, O] { - if e.IsRight() { + if e.isRight { return Either[L, O]{ - left: None[L](), - right: MapOption(f)(e.right), + isRight: true, + right: f(e.right), } } return Either[L, O]{ - left: e.left, - right: None[O](), + isRight: false, + left: e.left, } } } diff --git a/fn/either_test.go b/fn/either_test.go new file mode 100644 index 000000000..dca15f8d9 --- /dev/null +++ b/fn/either_test.go @@ -0,0 +1,139 @@ +package fn + +import ( + "testing" + "testing/quick" +) + +func TestPropConstructorEliminatorDuality(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + Len := func(s string) int { return len(s) } // smh + if isRight { + v := ElimEither( + Iden[int], + Len, + NewRight[int, string](s), + ) + return v == Len(s) + } + + v := ElimEither( + Iden[int], + Len, + NewLeft[int, string](i), + ) + return v == i + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropWhenClauseExclusivity(t *testing.T) { + f := func(i int, isRight bool) bool { + var e Either[int, int] + if isRight { + e = NewRight[int, int](i) + } else { + e = NewLeft[int, int](i) + } + z := 0 + e.WhenLeft(func(x int) { z += x }) + e.WhenRight(func(x int) { z += x }) + + return z != 2*i && e.IsLeft() != e.IsRight() + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropSwapEitherSelfInverting(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + } else { + e = NewLeft[int, string](i) + } + return e.Swap().Swap() == e + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropMapLeftIdentity(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + } else { + e = NewLeft[int, string](i) + } + return MapLeft[int, string, int](Iden[int])(e) == e + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropMapRightIdentity(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + } else { + e = NewLeft[int, string](i) + } + return MapRight[int, string, string](Iden[string])(e) == e + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropToOptionIdentities(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + + r2O := e.RightToOption() == Some(s) + o2R := e == OptionToRight[string, int, string]( + Some(s), i, + ) + l2O := e.LeftToOption() == None[int]() + + return r2O && o2R && l2O + } else { + e = NewLeft[int, string](i) + l2O := e.LeftToOption() == Some(i) + o2L := e == OptionToLeft[int, int](Some(i), s) + r2O := e.RightToOption() == None[string]() + + return l2O && o2L && r2O + } + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropUnwrapIdentities(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + return e.UnwrapRightOr("") == s && + e.UnwrapLeftOr(0) == 0 + } else { + e = NewLeft[int, string](i) + return e.UnwrapLeftOr(0) == i && + e.UnwrapRightOr("") == "" + } + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} diff --git a/fn/option.go b/fn/option.go index fbd5f2489..797f3a0ff 100644 --- a/fn/option.go +++ b/fn/option.go @@ -218,30 +218,18 @@ func (o Option[A]) UnsafeFromSome() A { // providing the Right value that should be used if the Option value is None. func OptionToLeft[O, L, R any](o Option[O], r R) Either[O, R] { if o.IsSome() { - return Either[O, R]{ - left: o, - right: None[R](), - } + return NewLeft[O, R](o.some) } - return Either[O, R]{ - left: None[O](), - right: Some(r), - } + return NewRight[O, R](r) } // OptionToRight can be used to convert an Option value into an Either, by // providing the Left value that should be used if the Option value is None. func OptionToRight[O, L, R any](o Option[O], l L) Either[L, O] { if o.IsSome() { - return Either[L, O]{ - left: None[L](), - right: o, - } + return NewRight[L, O](o.some) } - return Either[L, O]{ - left: Some(l), - right: None[O](), - } + return NewLeft[L, O](l) } diff --git a/fn/result.go b/fn/result.go index cb459bdc8..93d2dd7d6 100644 --- a/fn/result.go +++ b/fn/result.go @@ -39,12 +39,17 @@ func Errf[T any](errString string, args ...any) Result[T] { // Unpack extracts the value or error from the Result. func (r Result[T]) Unpack() (T, error) { var zero T - return r.left.UnwrapOr(zero), r.right.UnwrapOr(nil) + + if r.IsErr() { + return zero, r.right + } + + return r.left, nil } // Err exposes the underlying error of the result type as a normal error type. func (r Result[T]) Err() error { - return r.right.some + return r.right } // IsOk returns true if the Result is a success value. @@ -59,66 +64,73 @@ func (r Result[T]) IsErr() bool { // Map applies a function to the success value if it exists. func (r Result[T]) Map(f func(T) T) Result[T] { - if r.IsOk() { - return Ok(f(r.left.some)) + return Result[T]{ + MapLeft[T, error](f)(r.Either), } - - return r } // MapErr applies a function to the error value if it exists. func (r Result[T]) MapErr(f func(error) error) Result[T] { - if r.IsErr() { - return Err[T](f(r.right.some)) + return Result[T]{ + MapRight[T](f)(r.Either), } - - return r } // Option returns the success value as an Option. func (r Result[T]) Option() Option[T] { - return r.left + return r.Either.LeftToOption() } // WhenResult executes the given function if the Result is a success. func (r Result[T]) WhenResult(f func(T)) { - r.left.WhenSome(func(t T) { - f(t) - }) + r.WhenLeft(f) } // WhenErr executes the given function if the Result is an error. func (r Result[T]) WhenErr(f func(error)) { - r.right.WhenSome(func(e error) { - f(e) - }) + r.WhenRight(f) } // UnwrapOr returns the success value or a default value if it's an error. func (r Result[T]) UnwrapOr(defaultValue T) T { - return r.left.UnwrapOr(defaultValue) + if r.IsErr() { + return defaultValue + } + + return r.left } // UnwrapOrElse returns the success value or computes a value from a function // if it's an error. func (r Result[T]) UnwrapOrElse(f func() T) T { - return r.left.UnwrapOrFunc(f) + if r.IsErr() { + return f() + } + + return r.left } // UnwrapOrFail returns the success value or fails the test if it's an error. func (r Result[T]) UnwrapOrFail(t *testing.T) T { t.Helper() - return r.left.UnwrapOrFail(t) + if r.IsErr() { + t.Fatalf("Result[%T] contained error: %v", r.left, r.right) + } + + var zero T + + return zero } // FlatMap applies a function that returns a Result to the success value if it // exists. func (r Result[T]) FlatMap(f func(T) Result[T]) Result[T] { if r.IsOk() { - return f(r.left.some) + return r } - return r + + return f(r.left) } // AndThen is an alias for FlatMap. This along with OrElse can be used to @@ -143,10 +155,10 @@ func (r Result[T]) OrElse(f func() Result[T]) Result[T] { // it exists. func FlatMap[A, B any](r Result[A], f func(A) Result[B]) Result[B] { if r.IsOk() { - return f(r.left.some) + return f(r.left) } - return Err[B](r.right.some) + return Err[B](r.right) } // AndThen is an alias for FlatMap. This along with OrElse can be used to From 9c30bce10c8a90700c7c2560cc9cfc917add5ffd Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Fri, 26 Apr 2024 18:52:22 -0700 Subject: [PATCH 06/10] fn: add curried (in)equality functions --- fn/fn.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/fn/fn.go b/fn/fn.go index cb8151a23..147bf7daf 100644 --- a/fn/fn.go +++ b/fn/fn.go @@ -28,3 +28,19 @@ func Const[A, B any](a A) func(B) A { return a } } + +// Eq is a curried function that returns true if its eventual two arguments are +// equal. +func Eq[A comparable](x A) func(A) bool { + return func(y A) bool { + return x == y + } +} + +// Neq is a curried function that returns true if its eventual two arguments are +// not equal. +func Neq[A comparable](x A) func(A) bool { + return func(y A) bool { + return x != y + } +} From fb1437cb6da60c99a37e745bf50b8b819bec7a88 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Fri, 26 Apr 2024 18:52:40 -0700 Subject: [PATCH 07/10] fn: add Elem function for set membership --- fn/slice.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fn/slice.go b/fn/slice.go index 8f55cc159..22f3fa6f5 100644 --- a/fn/slice.go +++ b/fn/slice.go @@ -114,6 +114,11 @@ func FindIdx[A any](pred Pred[A], s []A) Option[T2[int, A]] { return None[T2[int, A]]() } +// Elem returns true if the element in the argument is found in the slice +func Elem[A comparable](a A, s []A) bool { + return Any(Eq(a), s) +} + // Flatten takes a slice of slices and returns a concatenation of those slices. func Flatten[A any](s [][]A) []A { sz := Foldr( From 364d79e552fced143f2a565af7704826236642d2 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Mon, 29 Apr 2024 17:41:45 -0700 Subject: [PATCH 08/10] fn: add generic version of List --- fn/list.go | 302 ++++++++++++++++++++ fn/list_test.go | 729 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1031 insertions(+) create mode 100644 fn/list.go create mode 100644 fn/list_test.go diff --git a/fn/list.go b/fn/list.go new file mode 100644 index 000000000..d76df4f29 --- /dev/null +++ b/fn/list.go @@ -0,0 +1,302 @@ +// Copyright (c) 2009 The Go Authors. All rights reserved. +// Copyright (c) 2024 Lightning Labs and the Lightning Network Developers + +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: + +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +package fn + +type Node[A any] struct { + // prev is a pointer to the previous node in the List. + prev *Node[A] + + // next is a pointer to the next node in the List. + next *Node[A] + + // list is the root pointer to the List in which this node is located. + list *List[A] + + // Value is the actual data contained within the Node. + Value A +} + +// Next returns the next list node or nil. +func (e *Node[A]) Next() *Node[A] { + if e.list == nil { + return nil + } + + if e.next == &e.list.root { + return nil + } + + return e.next +} + +// Prev returns the previous list node or nil. +func (e *Node[A]) Prev() *Node[A] { + if e.list == nil { + return nil + } + + if e.prev == &e.list.root { + return nil + } + + return e.prev +} + +// List represents a doubly linked list. +// The zero value for List is an empty list ready to use. +type List[A any] struct { + // root is a sentinal Node to identify the head and tail of the list. + // root.prev is the tail, root.next is the head. For the purposes of + // elegance, the absence of a next or prev node is encoded as the + // address of the root node. + root Node[A] + + // len is the current length of the list. + len int +} + +// Init intializes or clears the List l. +func (l *List[A]) Init() *List[A] { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + + return l +} + +// lazyInit lazily initializes a zero List value. It is called by other public +// functions that could feasibly be called on a List that was initialized by the +// raw List[A]{} constructor. +func (l *List[A]) lazyInit() { + if l.root.next == nil { + l.Init() + } +} + +// insert inserts n after predecessor, increments l.len, and returns n. +func (l *List[A]) insert(n *Node[A], predecessor *Node[A]) *Node[A] { + // Make n point to correct neighborhood. + n.prev = predecessor + n.next = predecessor.next + + // Make neighborhood point to n. + n.prev.next = n + n.next.prev = n + + // Make n part of the list. + n.list = l + + // Increment list length. + l.len++ + + return n +} + +// insertVal is a convenience wrapper for +// insert(&Node[A]{Value: v}, predecessor). +func (l *List[A]) insertVal(a A, predecessor *Node[A]) *Node[A] { + return l.insert(&Node[A]{Value: a}, predecessor) +} + +// move removes n from its current position and inserts it as the successor to +// predecessor. +func (l *List[A]) move(n *Node[A], predecessor *Node[A]) { + if n == predecessor { + return // Can't move after itself. + } + + if predecessor.next == n { + return // Nothing to be done. + } + + // Bind previous and next to each other. + n.prev.next = n.next + n.next.prev = n.prev + + // Make n point to new neighborhood. + n.prev = predecessor + n.next = predecessor.next + + // Make new neighborhood point to n. + n.prev.next = n + n.next.prev = n +} + +// New returns an initialized List. +func NewList[A any]() *List[A] { + l := List[A]{} + return l.Init() +} + +// Len returns the number of elements of List l. +// The complexity is O(1). +func (l *List[A]) Len() int { + return l.len +} + +// Front returns the first Node of List l or nil if the list is empty. +func (l *List[A]) Front() *Node[A] { + if l.len == 0 { + return nil + } + + return l.root.next +} + +// Back returns the last Node of List l or nil if the list is empty. +func (l *List[A]) Back() *Node[A] { + if l.len == 0 { + return nil + } + + return l.root.prev +} + +// Remove removes Node n from List l if n is an element of List l. +// It returns the Node value e.Value. +// The Node must not be nil. +func (l *List[A]) Remove(n *Node[A]) A { + if n.list == l { + n.prev.next = n.next + n.next.prev = n.prev + l.len-- + + v := n.Value + // Set all node data to nil to prevent dangling references. + *n = Node[A]{Value: v} + + return v + } + + return n.Value +} + +// PushFront inserts a new Node n with value a at the front of List l and +// returns n. +func (l *List[A]) PushFront(a A) *Node[A] { + l.lazyInit() + return l.insertVal(a, &l.root) +} + +// PushBack inserts a new Node n with value a at the back of List l and returns +// n. +func (l *List[A]) PushBack(a A) *Node[A] { + l.lazyInit() + return l.insertVal(a, l.root.prev) +} + +// InsertBefore inserts a new Node n with value a immediately before successor +// and returns n. If successor is not an element of l, the list is not +// modified. The successor must not be nil. +func (l *List[A]) InsertBefore(a A, successor *Node[A]) *Node[A] { + if successor == nil { + return l.insertVal(a, &l.root) + } + + if successor.list != l { + return nil + } + + return l.insertVal(a, successor.prev) +} + +// InsertAfter inserts a new Node n with value a immediately after and returns +// e. If predecessor is not an element of l, the list is not modified. The +// predecessor must not be nil. +func (l *List[A]) InsertAfter(a A, predecessor *Node[A]) *Node[A] { + if predecessor == nil { + return l.insertVal(a, l.root.prev) + } + + if predecessor.list != l { + return nil + } + + return l.insertVal(a, predecessor) +} + +// MoveToFront moves Node n to the front of List l. +// If n is not an element of l, the list is not modified. +// The Node must not be nil. +func (l *List[A]) MoveToFront(n *Node[A]) { + if n.list == l { + l.move(n, &l.root) + } +} + +// MoveToBack moves Node n to the back of List l. +// If n is not an element of l, the list is not modified. +// The Node must not be nil. +func (l *List[A]) MoveToBack(n *Node[A]) { + if n.list == l { + l.move(n, l.root.prev) + } +} + +// MoveBefore moves Node n to its new position before successor. +// If n or successor is not an element of l, or n == successor, the list is not +// modified. The Node and successor must not be nil. +func (l *List[A]) MoveBefore(n, successor *Node[A]) { + if n.list == l && successor.list == l { + l.move(n, successor.prev) + } +} + +// MoveAfter moves Node n to its new position after predecessor. +// If n or predecessor is not an element of l, or n == predecessor, the list is +// not modified. The Node and predecessor must not be nil. +func (l *List[A]) MoveAfter(n, predecessor *Node[A]) { + if n.list == l && predecessor.list == l { + l.move(n, predecessor) + } +} + +// PushBackList inserts a copy of List other at the back of List l. +// The Lists l and other may be the same. They must not be nil. +func (l *List[A]) PushBackList(other *List[A]) { + l.lazyInit() + n := other.Front() + sz := other.Len() + for i := 0; i < sz; i++ { + l.insertVal(n.Value, l.root.prev) + n = n.Next() + } +} + +// PushFrontList inserts a copy of List other at the front of List l. +// The Lists l and other may be the same. They must not be nil. +func (l *List[A]) PushFrontList(other *List[A]) { + l.lazyInit() + n := other.Back() + sz := other.Len() + for i := 0; i < sz; i++ { + l.insertVal(n.Value, &l.root) + n = n.Prev() + } +} diff --git a/fn/list_test.go b/fn/list_test.go new file mode 100644 index 000000000..c08122cc4 --- /dev/null +++ b/fn/list_test.go @@ -0,0 +1,729 @@ +package fn + +import ( + "math/rand" + "reflect" + "testing" + "testing/quick" +) + +func GenList(r *rand.Rand) *List[uint32] { + size := int(r.Uint32() >> 24) + l := NewList[uint32]() + for i := 0; i < size; i++ { + l.PushBack(rand.Uint32()) + } + return l +} + +func GetRandNode(l *List[uint32], r *rand.Rand) *Node[uint32] { + if l.Len() == 0 { + return nil + } + + idx := r.Uint32() % uint32(l.Len()) + n := l.Front() + for i := 0; i < int(idx); i++ { + n = n.Next() + } + + return n +} + +func TestPushLenIncrement(t *testing.T) { + err := quick.Check( + func(l *List[uint32], x uint32, front bool) bool { + sz := l.Len() + if front { + l.PushFront(x) + } else { + l.PushBack(x) + } + sz2 := l.Len() + + return sz2 == sz+1 + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + vs[0] = reflect.ValueOf(GenList(r)) + vs[1] = reflect.ValueOf(r.Uint32()) + vs[2] = reflect.ValueOf(r.Uint32()%2 == 0) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestRemoveLenDecrement(t *testing.T) { + err := quick.Check( + func(l *List[uint32], n *Node[uint32]) bool { + if l.Len() == 0 { + return true + } + + sz := l.Len() + l.Remove(n) + sz2 := l.Len() + + return sz2 == sz-1 + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(GetRandNode(l, r)) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestPushGetCongruence(t *testing.T) { + err := quick.Check( + func(l *List[uint32], x uint32, front bool) bool { + if front { + l.PushFront(x) + return l.Front().Value == x + } else { + l.PushBack(x) + return l.Back().Value == x + } + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + vs[0] = reflect.ValueOf(GenList(r)) + vs[1] = reflect.ValueOf(r.Uint32()) + vs[2] = reflect.ValueOf(r.Uint32()%2 == 0) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestInsertBeforeFrontIdentity(t *testing.T) { + err := quick.Check( + func(l *List[uint32], x uint32) bool { + if l == nil { + return true + } + + nodeX := l.InsertBefore(x, l.Front()) + + return nodeX == l.Front() + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(r.Uint32()) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestInsertAfterBackIdentity(t *testing.T) { + err := quick.Check( + func(l *List[uint32], x uint32) bool { + if l == nil { + return true + } + + nodeX := l.InsertAfter(x, l.Back()) + + return nodeX == l.Back() + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(r.Uint32()) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestInsertBeforeNextIdentity(t *testing.T) { + err := quick.Check( + func(l *List[uint32], n *Node[uint32], x uint32) bool { + if n == nil { + return true + } + + nodeX := l.InsertBefore(x, n) + return nodeX.Next() == n + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(GetRandNode(l, r)) + vs[2] = reflect.ValueOf(r.Uint32()) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestInsertAfterPrevIdentity(t *testing.T) { + err := quick.Check( + func(l *List[uint32], n *Node[uint32], x uint32) bool { + if n == nil { + return true + } + + nodeX := l.InsertAfter(x, n) + return nodeX.Prev() == n + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(GetRandNode(l, r)) + vs[2] = reflect.ValueOf(r.Uint32()) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestMoveToFrontFrontIdentity(t *testing.T) { + err := quick.Check( + func(l *List[uint32], n *Node[uint32]) bool { + if n == nil { + return true + } + + l.MoveToFront(n) + return l.Front() == n + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(GetRandNode(l, r)) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestMoveToBackBackIdentity(t *testing.T) { + err := quick.Check( + func(l *List[uint32], n *Node[uint32]) bool { + if n == nil { + return true + } + + l.MoveToBack(n) + return l.Back() == n + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(GetRandNode(l, r)) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestMoveBeforeFrontIsFront(t *testing.T) { + err := quick.Check( + func(l *List[uint32], n *Node[uint32]) bool { + if n == nil { + return true + } + + l.MoveBefore(n, l.Front()) + return l.Front() == n + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(GetRandNode(l, r)) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestMoveAfterBackIsBack(t *testing.T) { + err := quick.Check( + func(l *List[uint32], n *Node[uint32]) bool { + if n == nil { + return true + } + + l.MoveAfter(n, l.Back()) + return l.Back() == n + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(GetRandNode(l, r)) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestMultiMoveErasure(t *testing.T) { + err := quick.Check( + func(l *List[uint32], n *Node[uint32], m *Node[uint32]) bool { + if n == nil || m == nil || n == m { + return true + } + + l.MoveToFront(n) + l.MoveToBack(n) + l.MoveBefore(n, m) + l.MoveAfter(n, m) + return m.Next() == n + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(GetRandNode(l, r)) + vs[2] = reflect.ValueOf(GetRandNode(l, r)) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestPushListSymmetry(t *testing.T) { + copyList := func(l *List[uint32]) *List[uint32] { + c := NewList[uint32]() + for n := l.Front(); n != nil; n = n.Next() { + c.PushBack(n.Value) + } + return c + } + + err := quick.Check( + func(l1 *List[uint32], l2 *List[uint32]) bool { + if l1.Len() == 0 || l2.Len() == 0 { + return true + } + + l1Copy := copyList(l1) + l2Copy := copyList(l2) + + l1.PushBackList(l2Copy) + l2.PushFrontList(l1Copy) + + iter1 := l1.Front() + iter2 := l2.Front() + + for i := 0; i < l1Copy.Len()+l2Copy.Len()-1; i++ { + if iter1.Value != iter2.Value { + return false + } + + iter1 = iter1.Next() + iter2 = iter2.Next() + } + + return true + }, + &quick.Config{ + Values: func(vs []reflect.Value, r *rand.Rand) { + l := GenList(r) + l2 := GenList(r) + vs[0] = reflect.ValueOf(l) + vs[1] = reflect.ValueOf(l2) + }, + }, + ) + + if err != nil { + t.Fatal(err) + } +} + +func TestIssue4103(t *testing.T) { + l1 := NewList[int]() + l1.PushBack(1) + l1.PushBack(2) + + l2 := NewList[int]() + l2.PushBack(3) + l2.PushBack(4) + + e := l1.Front() + l2.Remove(e) // l2 should not change because e is not an element of l2 + if n := l2.Len(); n != 2 { + t.Errorf("l2.Len() = %d, want 2", n) + } + + l1.InsertBefore(8, e) + if n := l1.Len(); n != 3 { + t.Errorf("l1.Len() = %d, want 3", n) + } +} + +func TestIssue6349(t *testing.T) { + l := NewList[int]() + l.PushBack(1) + l.PushBack(2) + + e := l.Front() + l.Remove(e) + if e.Value != 1 { + t.Errorf("e.value = %d, want 1", e.Value) + } + if e.Next() != nil { + t.Errorf("e.Next() != nil") + } + if e.Prev() != nil { + t.Errorf("e.Prev() != nil") + } +} + +func checkListLen[V any](t *testing.T, l *List[V], length int) bool { + if n := l.Len(); n != length { + t.Errorf("l.Len() = %d, want %d", n, length) + return false + } + return true +} + +func checkListPointers[V any](t *testing.T, l *List[V], es []*Node[V]) { + root := &l.root + + if !checkListLen(t, l, len(es)) { + return + } + + // zero length lists must be the zero value or properly initialized + // (sentinel circle) + if len(es) == 0 { + if l.root.next != nil && l.root.next != root || + l.root.prev != nil && l.root.prev != root { + + t.Errorf("l.root.next = %p, l.root.prev = %p;"+ + "both should both be nil or %p", l.root.next, + l.root.prev, root) + } + return + } + // len(es) > 0 + + // check internal and external prev/next connections + for i, e := range es { + prev := root + Prev := (*Node[V])(nil) + if i > 0 { + prev = es[i-1] + Prev = prev + } + if p := e.prev; p != prev { + t.Errorf("elt[%d](%p).prev = %p, want %p", i, e, p, + prev) + } + if p := e.Prev(); p != Prev { + t.Errorf("elt[%d](%p).Prev() = %p, want %p", i, e, p, + Prev) + } + + next := root + Next := (*Node[V])(nil) + if i < len(es)-1 { + next = es[i+1] + Next = next + } + if n := e.next; n != next { + t.Errorf("elt[%d](%p).next = %p, want %p", i, e, n, + next) + } + if n := e.Next(); n != Next { + t.Errorf("elt[%d](%p).Next() = %p, want %p", i, e, n, + Next) + } + } +} + +func TestList(t *testing.T) { + l := NewList[int]() + checkListPointers(t, l, []*Node[int]{}) + + // Single element list + e := l.PushFront(5) + checkListPointers(t, l, []*Node[int]{e}) + l.MoveToFront(e) + checkListPointers(t, l, []*Node[int]{e}) + l.MoveToBack(e) + checkListPointers(t, l, []*Node[int]{e}) + l.Remove(e) + checkListPointers(t, l, []*Node[int]{}) + + // Bigger list + e2 := l.PushFront(2) + e1 := l.PushFront(1) + e3 := l.PushBack(3) + e4 := l.PushBack(0) + checkListPointers(t, l, []*Node[int]{e1, e2, e3, e4}) + + l.Remove(e2) + checkListPointers(t, l, []*Node[int]{e1, e3, e4}) + + l.MoveToFront(e3) // move from middle + checkListPointers(t, l, []*Node[int]{e3, e1, e4}) + + l.MoveToFront(e1) + l.MoveToBack(e3) // move from middle + checkListPointers(t, l, []*Node[int]{e1, e4, e3}) + + l.MoveToFront(e3) // move from back + checkListPointers(t, l, []*Node[int]{e3, e1, e4}) + l.MoveToFront(e3) // should be no-op + checkListPointers(t, l, []*Node[int]{e3, e1, e4}) + + l.MoveToBack(e3) // move from front + checkListPointers(t, l, []*Node[int]{e1, e4, e3}) + l.MoveToBack(e3) // should be no-op + checkListPointers(t, l, []*Node[int]{e1, e4, e3}) + + e2 = l.InsertBefore(2, e1) // insert before front + checkListPointers(t, l, []*Node[int]{e2, e1, e4, e3}) + l.Remove(e2) + e2 = l.InsertBefore(2, e4) // insert before middle + checkListPointers(t, l, []*Node[int]{e1, e2, e4, e3}) + l.Remove(e2) + e2 = l.InsertBefore(2, e3) // insert before back + checkListPointers(t, l, []*Node[int]{e1, e4, e2, e3}) + l.Remove(e2) + + e2 = l.InsertAfter(2, e1) // insert after front + checkListPointers(t, l, []*Node[int]{e1, e2, e4, e3}) + l.Remove(e2) + e2 = l.InsertAfter(2, e4) // insert after middle + checkListPointers(t, l, []*Node[int]{e1, e4, e2, e3}) + l.Remove(e2) + e2 = l.InsertAfter(2, e3) // insert after back + checkListPointers(t, l, []*Node[int]{e1, e4, e3, e2}) + l.Remove(e2) + + // Check standard iteration. + sum := 0 + for e := l.Front(); e != nil; e = e.Next() { + sum += e.Value + } + if sum != 4 { + t.Errorf("sum over l = %d, want 4", sum) + } + + // Clear all elements by iterating + var next *Node[int] + for e := l.Front(); e != nil; e = next { + next = e.Next() + l.Remove(e) + } + checkListPointers(t, l, []*Node[int]{}) +} + +func checkList[V comparable](t *testing.T, l *List[V], es []V) { + if !checkListLen(t, l, len(es)) { + return + } + + i := 0 + for e := l.Front(); e != nil; e = e.Next() { + le := e.Value + if le != es[i] { + t.Errorf("elt[%d].Value = %v, want %v", i, le, es[i]) + } + i++ + } +} + +func TestExtending(t *testing.T) { + l1 := NewList[int]() + l2 := NewList[int]() + + l1.PushBack(1) + l1.PushBack(2) + l1.PushBack(3) + + l2.PushBack(4) + l2.PushBack(5) + + l3 := NewList[int]() + l3.PushBackList(l1) + checkList(t, l3, []int{1, 2, 3}) + l3.PushBackList(l2) + checkList(t, l3, []int{1, 2, 3, 4, 5}) + + l3 = NewList[int]() + l3.PushFrontList(l2) + checkList(t, l3, []int{4, 5}) + l3.PushFrontList(l1) + checkList(t, l3, []int{1, 2, 3, 4, 5}) + + checkList(t, l1, []int{1, 2, 3}) + checkList(t, l2, []int{4, 5}) + + l3 = NewList[int]() + l3.PushBackList(l1) + checkList(t, l3, []int{1, 2, 3}) + l3.PushBackList(l3) + checkList(t, l3, []int{1, 2, 3, 1, 2, 3}) + + l3 = NewList[int]() + l3.PushFrontList(l1) + checkList(t, l3, []int{1, 2, 3}) + l3.PushFrontList(l3) + checkList(t, l3, []int{1, 2, 3, 1, 2, 3}) + + l3 = NewList[int]() + l1.PushBackList(l3) + checkList(t, l1, []int{1, 2, 3}) + l1.PushFrontList(l3) + checkList(t, l1, []int{1, 2, 3}) +} + +func TestRemove(t *testing.T) { + l := NewList[int]() + e1 := l.PushBack(1) + e2 := l.PushBack(2) + checkListPointers(t, l, []*Node[int]{e1, e2}) + e := l.Front() + l.Remove(e) + checkListPointers(t, l, []*Node[int]{e2}) + l.Remove(e) + checkListPointers(t, l, []*Node[int]{e2}) +} + +func TestMove(t *testing.T) { + l := NewList[int]() + e1 := l.PushBack(1) + e2 := l.PushBack(2) + e3 := l.PushBack(3) + e4 := l.PushBack(4) + + l.MoveAfter(e3, e3) + checkListPointers(t, l, []*Node[int]{e1, e2, e3, e4}) + l.MoveBefore(e2, e2) + checkListPointers(t, l, []*Node[int]{e1, e2, e3, e4}) + + l.MoveAfter(e3, e2) + checkListPointers(t, l, []*Node[int]{e1, e2, e3, e4}) + l.MoveBefore(e2, e3) + checkListPointers(t, l, []*Node[int]{e1, e2, e3, e4}) + + l.MoveBefore(e2, e4) + checkListPointers(t, l, []*Node[int]{e1, e3, e2, e4}) + e2, e3 = e3, e2 + + l.MoveBefore(e4, e1) + checkListPointers(t, l, []*Node[int]{e4, e1, e2, e3}) + e1, e2, e3, e4 = e4, e1, e2, e3 + + l.MoveAfter(e4, e1) + checkListPointers(t, l, []*Node[int]{e1, e4, e2, e3}) + e2, e3, e4 = e4, e2, e3 + + l.MoveAfter(e2, e3) + checkListPointers(t, l, []*Node[int]{e1, e3, e2, e4}) +} + +// Test PushFront, PushBack, PushFrontList, PushBackList with uninitialized List +func TestZeroList(t *testing.T) { + var l1 = new(List[int]) + l1.PushFront(1) + checkList(t, l1, []int{1}) + + var l2 = new(List[int]) + l2.PushBack(1) + checkList(t, l2, []int{1}) + + var l3 = new(List[int]) + l3.PushFrontList(l1) + checkList(t, l3, []int{1}) + + var l4 = new(List[int]) + l4.PushBackList(l2) + checkList(t, l4, []int{1}) +} + +// Test that a list l is not modified when calling InsertBefore with a mark +// that is not an element of l. +func TestInsertBeforeUnknownMark(t *testing.T) { + var l List[int] + l.PushBack(1) + l.PushBack(2) + l.PushBack(3) + l.InsertBefore(1, new(Node[int])) + checkList(t, &l, []int{1, 2, 3}) +} + +// Test that a list l is not modified when calling InsertAfter with a mark that +// is not an element of l. +func TestInsertAfterUnknownMark(t *testing.T) { + var l List[int] + l.PushBack(1) + l.PushBack(2) + l.PushBack(3) + l.InsertAfter(1, new(Node[int])) + checkList(t, &l, []int{1, 2, 3}) +} + +// Test that a list l is not modified when calling MoveAfter or MoveBefore with +// a mark that is not an element of l. +func TestMoveUnknownMark(t *testing.T) { + var l1 List[int] + e1 := l1.PushBack(1) + + var l2 List[int] + e2 := l2.PushBack(2) + + l1.MoveAfter(e1, e2) + checkList(t, &l1, []int{1}) + checkList(t, &l2, []int{2}) + + l1.MoveBefore(e1, e2) + checkList(t, &l1, []int{1}) + checkList(t, &l2, []int{2}) +} From fa2e25d5f437071dfed312a5a9b5e6c8e9a3d416 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 10 Jul 2024 16:57:59 -0700 Subject: [PATCH 09/10] fn: remove lru dependency --- fn/conc_queue.go | 8 +++----- fn/go.mod | 2 +- fn/go.sum | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/fn/conc_queue.go b/fn/conc_queue.go index 0d0dde207..0c52c4491 100644 --- a/fn/conc_queue.go +++ b/fn/conc_queue.go @@ -2,8 +2,6 @@ package fn import ( "sync" - - "github.com/lightninglabs/neutrino/cache/lru" ) // ConcurrentQueue is a typed concurrent-safe FIFO queue with unbounded @@ -17,7 +15,7 @@ type ConcurrentQueue[T any] struct { chanIn chan T chanOut chan T - overflow *lru.List[T] + overflow *List[T] wg sync.WaitGroup quit chan struct{} @@ -25,13 +23,13 @@ type ConcurrentQueue[T any] struct { // NewConcurrentQueue constructs a ConcurrentQueue. The bufferSize parameter is // the capacity of the output channel. When the size of the queue is below this -// threshold, pushes do n[?12;4$yot incur the overhead of the less efficient overflow +// threshold, pushes do not incur the overhead of the less efficient overflow // structure. func NewConcurrentQueue[T any](bufferSize int) *ConcurrentQueue[T] { return &ConcurrentQueue[T]{ chanIn: make(chan T), chanOut: make(chan T, bufferSize), - overflow: lru.NewList[T](), + overflow: NewList[T](), quit: make(chan struct{}), } } diff --git a/fn/go.mod b/fn/go.mod index e14cb555a..1780b9968 100644 --- a/fn/go.mod +++ b/fn/go.mod @@ -3,9 +3,9 @@ module github.com/lightningnetwork/lnd/fn 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 + golang.org/x/sync v0.7.0 ) require ( diff --git a/fn/go.sum b/fn/go.sum index 73345fe5e..86f138bc5 100644 --- a/fn/go.sum +++ b/fn/go.sum @@ -1,8 +1,6 @@ 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= @@ -14,6 +12,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs 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= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= From eaa5e4a039488cb6fd230579a6426bbb5782cb70 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 10 Jul 2024 16:59:43 -0700 Subject: [PATCH 10/10] fn: remove redundant Reduce function This commit removes Reduce since we already have both Foldl and Foldr. --- fn/func.go | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 fn/func.go diff --git a/fn/func.go b/fn/func.go deleted file mode 100644 index 056f84aef..000000000 --- a/fn/func.go +++ /dev/null @@ -1,17 +0,0 @@ -package fn - -// Reducer represents a function that takes an accumulator and the value, then -// returns a new accumulator. -type Reducer[T, V any] func(accum T, value V) T - -// Reduce takes a slice of something, and a reducer, and produces a final -// accumulated value. -func Reduce[T any, V any, S []V](s S, f Reducer[T, V]) T { - var accum T - - for _, x := range s { - accum = f(accum, x) - } - - return accum -}