Last time we implemented Maybe
.
Our example was a simple folding through user input lines. With Maybe
it makes
less sense, because Maybe
is able to capture only “value or nothing”. The
real value would be to conserve wrong user inputs, but still ignore them in a
fold. That’s the place where we can use an Either
data type.
What is an Either?
An Either
can Left value
or Right value
(again stolen from my Haskell
experiences). It can be either A
value or B
value, but their type can be
different, and usually they are different, otherwise we don’t have to Either
.
An easy example would be an Either String Int
where Left
has string
type,
while Right
has int
type.
Let’s implement an Either
// Either data type
type Either[L any, R any] struct {
left L
right R
isRight bool
}
// Left value.
func (e Either[L, R]) Left() L {
return e.left
}
// Right value.
func (e Either[L, R]) Right() R {
return e.right
}
// IsRight check if it was a Right value.
func (e Either[L, R]) IsRight() bool {
return e.isRight
}
// Left creates a new Either with Left value.
func Left[L any, R any](value L) Either[L, R] {
return Either[L, R]{left: value}
}
// Right creates a new Either with Right value.
func Right[L any, R any](value R) Either[L, R] {
return Either[L, R]{right: value, isRight: true}
}
The two extra helper function is there to make it easier to create an Either
instance.
How can we use it?
Let’s the same example we have
from Maybe
, but now when we use Foldl
we log entries we skipped.
First we need a function that reads the “user input” and returns with channel of
Either
s.
import "github.com/yitsushi/go1-18-experiments/pkg/data"
func readFromUser() chan data.Either[string, int] {
ch := make(chan data.Either[string, int])
myList := []data.Either[string, int]{
data.Right[string](12),
data.Right[string](2),
data.Left[string,int]("asd"),
data.Right[string](8),
}
go func() {
for _, v := range myList {
ch <- v
}
close(ch)
}()
return ch
}
Not much changed, we replaced all Just()
calls with Right()
, and all
Nothing()
calls with Left()
.
Our folding function will be a bit different. Previously we used the same kind
for folding. We used a chan Maybe
and a Maybe
as the final result. Now we
have a chan Either
, but we have no reason to return with an Either
. We could
return with a single int
value, but what if all the input values are wrong
(Left
)? We can return with a Maybe
:
import (
"constraints"
"fmt"
"log"
"github.com/yitsushi/go1-18-experiments/pkg/data"
)
func add[L any, R constraints.Ordered](c data.Maybe[R], value data.Either[L, R]) data.Maybe[R] {
if !value.IsRight() {
log.Printf("Skip: %s\n", value.Left())
return c
}
if c.IsNothing() {
return data.Just(value.Right())
}
return data.Just(c.Value() + value.Right())
}
func main() {
result := data.FoldlIter(
data.Nothing[int](),
readFromUser(),
add[string, int],
)
fmt.Println(result)
}
Basically, if we see a Left
value, we skip the value, log the original
value and return with our carry value without changes. If it was a Nothing
it
will remain Nothing
. If we had Nothing
in our carry value and we have a
Right
value, just return with the new value. Otherwise, add the two values
together and return with a Just
value.