cover

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 Eithers.

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.