Skip to content
Open

Pair #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.
105 changes: 105 additions & 0 deletions pair/pair.go
Original file line number Diff line number Diff line change
@@ -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
}
}
227 changes: 227 additions & 0 deletions pair/pair_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}