diff --git a/README.md b/README.md index 6470cf7..c24b507 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Fp-go is a collection of Functional Programming helpers powered by Golang [1.18] - [Curry](#curry) - [Structs](#structs) - [Option](#option) + - [Pair](#pair) ## Install @@ -243,3 +244,10 @@ Option represents encapsulation of an optional value, it might be used as the re You could instanciate an `opt.Option[T]` with a value with `opt.Some(val)`. If the value is missing you can use `opt.None[T]()`. Option exports `Some`, `None`, `IsSome`, `IsNone`, `GetOrElse`, `Match`, `Map`, `Chain`. + + +#### Pair + +Pair allows you to group 2 values into a single struct. Can be used to make a function with multiple returns return a sigle wrapped value + +Pair exports `New`, `Fst`, `Snd`, `Get`, `MapFst`, `MapSnd`, `MapBoth`, `CheckFst`, `CheckSnd`, `CheckBoth`, `Merge`, `MergeC`, `Eq`, `Zip`. diff --git a/pair/pair.go b/pair/pair.go new file mode 100644 index 0000000..4be59ee --- /dev/null +++ b/pair/pair.go @@ -0,0 +1,105 @@ +package pair + +// Base Pair struct +type Pair[A, B any] struct { + a A + b B +} + +// Constructor for a Pair +func New[A, B any](a A, b B) Pair[A, B] { + return Pair[A, B]{a, b} +} + +// Getter for the first value of the Pair +func Fst[A, B any](t Pair[A, B]) A { + return t.a +} + +// Getter for the second value of the Pair +func Snd[A, B any](t Pair[A, B]) B { + return t.b +} + +// Getter for both values of the Pair +func Get[A, B any](t Pair[A, B]) (A, B) { + return t.a, t.b +} + +// Execute the function on the first value of the Pair +func MapFst[B, A, R any](fn func(A) R) func(Pair[A, B]) Pair[R, B] { + return func(t Pair[A, B]) Pair[R, B] { + return Pair[R, B]{fn(t.a), t.b} + } +} + +// Execute the function on the second value of the Pair +func MapSnd[A, B, R any](fn func(B) R) func(Pair[A, B]) Pair[A, R] { + return func(t Pair[A, B]) Pair[A, R] { + return Pair[A, R]{t.a, fn(t.b)} + } +} + +// Execute the functions on both the first and second values of the Pair +func MapBoth[A, B, R, S any](fnF func(A) R, fnS func(B) S) func(Pair[A, B]) Pair[R, S] { + return func(t Pair[A, B]) Pair[R, S] { + return Pair[R, S]{fnF(t.a), fnS(t.b)} + } +} + +// Helper to chcek if the first value of the Pair satisfies a predicate +func CheckFst[B, A any](fn func(A) bool) func(Pair[A, B]) bool { + return func(t Pair[A, B]) bool { + return fn(t.a) + } +} + +// Helper to chcek if the second value of the Pair satisfies a predicate +func CheckSnd[A, B any](fn func(B) bool) func(Pair[A, B]) bool { + return func(t Pair[A, B]) bool { + return fn(t.b) + } +} + +// Helper to chcek if both the first and the second values of the Pair satisfies their respective predicate +func CheckBoth[A, B any](fnF func(A) bool, fnS func(B) bool) func(Pair[A, B]) bool { + return func(t Pair[A, B]) bool { + return fnF(t.a) && fnS(t.b) + } +} + +// Merge the elements of a Pair with a Curried function +func MergeC[A, B, C any](fn func(A) func(B) C) func(Pair[A, B]) C { + return func(t Pair[A, B]) C { + return fn(t.a)(t.b) + } +} + +// Merge the elements of a Pair with a non-Curried function +func Merge[A, B, C any](fn func(A, B) C) func(Pair[A, B]) C { + return func(t Pair[A, B]) C { + return fn(t.a, t.b) + } +} + +// Check two Pairs for element-by-element equality. The types must be comparable +func Eq[A, B comparable](p1 Pair[A, B]) func(Pair[A, B]) bool { + return func(p2 Pair[A, B]) bool { + return p1.a == p2.a && p1.b == p2.b + } +} + +// Take 2 lists of A and B and merge them into a single list of Pair[A, B] +// If the lists don't have the same size, the final list will have the same size as the smaller one +func Zip[B, A any](lstA []A) func([]B) []Pair[A, B] { + return func(lstB []B) (res []Pair[A, B]) { + i := 0 + + for i < len(lstA) && i < len(lstB) { + res = append(res, New(lstA[i], lstB[i])) + i++ + } + + return + } +} diff --git a/pair/pair_test.go b/pair/pair_test.go new file mode 100644 index 0000000..554ae9b --- /dev/null +++ b/pair/pair_test.go @@ -0,0 +1,227 @@ +package pair + +import "testing" + +func eq[A, B comparable](ps1, ps2 []Pair[A, B]) bool { + if len(ps1) != len(ps2) { + return false + } + + for i := 0; i < len(ps1); i++ { + if !Eq(ps1[i])(ps2[i]) { + return false + } + } + + return true +} + +func TestNew(t *testing.T) { + res := New(42, "42") + if res.a != 42 { + t.Error("New should return a struct with 42 as the first value. Received:", res.a) + } + if res.b != "42" { + t.Error("New should return a struct with \"42\" as the second value. Received:", res.b) + } +} + +func TestFst(t *testing.T) { + res := Fst(New(42, "42")) + if res != 42 { + t.Error("Fst should return 42. Received:", res) + } +} + +func TestSnd(t *testing.T) { + res := Snd(New(42, "42")) + if res != "42" { + t.Error("Fst should return \"42\". Received:", res) + } +} + +func TestGet(t *testing.T) { + res1, res2 := Get(New(42, "42")) + if res1 != 42 { + t.Error("Get should return 42 as the first value. Received:", res1) + } + if res2 != "42" { + t.Error("Get should return \"42\" as the second value. Received:", res2) + } +} + +func TestMapFst(t *testing.T) { + res := MapFst[string](func(x int) int { return x + 1 })(New(41, "42")) + if Fst(res) != 42 { + t.Error("MapFirst should return 42 as a first value. Received:", Fst(res)) + } + if Snd(res) != "42" { + t.Error("MapFst should leave the second value \"42\" alone. Received:", Snd(res)) + } +} + +func TestMapSnd(t *testing.T) { + res := MapSnd[string](func(x int) int { return x + 1 })(New("42", 41)) + if Fst(res) != "42" { + t.Error("MapSnd should leave the first value \"42\" alone. Received:", Fst(res)) + } + if Snd(res) != 42 { + t.Error("MapFirst should return 42 as a second value. Received:", Snd(res)) + } +} + +func TestMapBoth(t *testing.T) { + res := MapBoth( + func(x bool) bool { return !x }, + func(x int) int { return x + 1 }, + )(New(false, 41)) + + if Fst(res) != true { + t.Error("MapBoth should return true as the first value. Received:", Fst(res)) + } + if Snd(res) != 42 { + t.Error("MapBoth should return 42 as the second value. Received:", Snd(res)) + } +} + +func TestCheckFst_True(t *testing.T) { + res := CheckFst[string](func(x int) bool { return x > 10 })(New(42, "42")) + if res != true { + t.Error("CheckFst should return true. Received:", res) + } +} + +func TestCheckFst_False(t *testing.T) { + res := CheckFst[string](func(x int) bool { return x < 10 })(New(42, "42")) + if res != false { + t.Error("CheckFst should return false. Received:", res) + } +} + +func TestCheckSnd_True(t *testing.T) { + res := CheckSnd[int](func(x string) bool { return x == "42" })(New(42, "42")) + if res != true { + t.Error("CheckFst should return true. Received:", res) + } +} + +func TestCheckSnd_False(t *testing.T) { + res := CheckSnd[int](func(x string) bool { return x == "1" })(New(42, "42")) + if res != false { + t.Error("CheckFst should return false. Received:", res) + } +} + +func TestCheckBoth_True_True(t *testing.T) { + res := CheckBoth( + func(x int) bool { return x > 10 }, + func(x string) bool { return x == "42" }, + )(New(42, "42")) + + if res != true { + t.Error("CheckBoth should return true. Received:", res) + } +} + +func TestCheckBoth_True_False(t *testing.T) { + res := CheckBoth( + func(x int) bool { return x > 10 }, + func(x string) bool { return x != "42" }, + )(New(42, "42")) + + if res != false { + t.Error("CheckBoth should return false. Received:", res) + } +} + +func TestCheckBoth_False_True(t *testing.T) { + res := CheckBoth( + func(x int) bool { return x < 10 }, + func(x string) bool { return x == "42" }, + )(New(42, "42")) + + if res != false { + t.Error("CheckBoth should return false. Received:", res) + } +} + +func TestCheckBoth_False_False(t *testing.T) { + res := CheckBoth( + func(x int) bool { return x < 10 }, + func(x string) bool { return x != "42" }, + )(New(42, "42")) + + if res != false { + t.Error("CheckBoth should return false. Received:", res) + } +} + +func TestMergeC(t *testing.T) { + mul := func(a int) func(int) int { return func(b int) int { return a * b } } + + res := MergeC(mul)(New(21, 2)) + if res != 42 { + t.Error("MergeC should return 42. Received:", res) + } +} + +func TestMerge(t *testing.T) { + mul := func(a, b int) int { return a * b } + + res := Merge(mul)(New(21, 2)) + if res != 42 { + t.Error("Merge should return 42. Received:", res) + } +} + +func TestZip_EqLen(t *testing.T) { + res := Zip[bool]([]int{1, 2, 3, 4, 5})([]bool{true, true, false, false, true}) + want := []Pair[int, bool]{New(1, true), New(2, true), New(3, false), New(4, false), New(5, true)} + if !eq(res, want) { + t.Error("Zip should have returned", want, ". Received:", res) + } +} + +func TestZip_Large_Fst(t *testing.T) { + res := Zip[bool]([]int{1, 2, 3, 4, 5})([]bool{true, true, false}) + want := []Pair[int, bool]{New(1, true), New(2, true), New(3, false)} + if !eq(res, want) { + t.Error("Zip should have returned", want, ". Received:", res) + } +} + +func TestZip_Large_Snd(t *testing.T) { + res := Zip[bool]([]int{1, 2, 3})([]bool{true, true, false, false, true}) + want := []Pair[int, bool]{New(1, true), New(2, true), New(3, false)} + if !eq(res, want) { + t.Error("Zip should have returned", want, ". Received:", res) + } +} + +func TestEq_True_True(t *testing.T) { + res := Eq(New(42, 21))(New(42, 21)) + if res != true { + t.Error("Eq should return true. Received:", res) + } +} + +func TestEq_True_False(t *testing.T) { + res := Eq(New(42, 21))(New(42, 100)) + if res != false { + t.Error("Eq should return false. Received:", res) + } +} + +func TestEq_False_True(t *testing.T) { + res := Eq(New(21, 42))(New(100, 42)) + if res != false { + t.Error("Eq should return false. Received:", res) + } +} + +func TestEq_False_False(t *testing.T) { + res := Eq(New(42, 21))(New(84, 42)) + if res != false { + t.Error("Eq should return false. Received:", res) + } +}