I know, we have fancy nil to “handle” maybe, but that’s not always good. Maybe we don’t want to return with a reference, or we don’t want to take an argument with reference. For example, we want to make sure our function has no side-effects, but the argument is optional. Sure we can pass in an empty instance, but then we have to check somehow if it’s empty like nothing or it’s empty on purpose.

That’s why we can have Maybe, and we could do it before 1.18 too, but it was too much effort because then we have to create a Maybe for each of our structs, and that’s not acceptable.

With 1.18 we have generics and with generics we can create a generic Maybe struct.

What is a Maybe?

A Maybe is data type that can have a Just a value or Nothing. The terminology is stolen from my Haskell experiences. Example values:

  • Just 12 which is an int with the value of 12
  • Just "text" which is a string with the value of text
  • Nothing which, well nothing. It has no value, it’s not there.

Let’s implement a Maybe

// Maybe data structure.
type Maybe[T any] struct {
	value     T
	isNothing bool
}

// Value returns with the value.
func (m Maybe[T]) Value() T {
	return m.value
}

// IsNothing tells us if this Maybe is a Nothing or it has a specific value.
func (m Maybe[T]) IsNothing() bool {
	return m.isNothing
}

// Nothing creates a Nothing.
func Nothing[T any]() Maybe[T] {
	return Maybe[T]{isNothing: true}
}

// Just creates a Maybe struct with a specific value.
func Just[T any](value T) Maybe[T] {
	return Maybe[T]{value: value}
}

The two extra helper function is there to make it easier to create Maybe instances, that way we can say Just(12) or Just("text"). Nothing is a bit different, because we still have to specify what “type of nothing” we have so we can call Nothing[int]() which will create a Maybe int type as Nothing.

How can we use it?

We already have a Foldl function, so we can use that to fold a list from the user. The user can enter numbers and we calculate the sum of those numbers. We could ignore all the values where we failed to parse the input, but we will not ignore them, we just mark them as Nothing.

import "github.com/yitsushi/go1-18-experiments/pkg/data"

func readFromUser() chan data.Maybe[int] {
	ch := make(chan data.Maybe[int])

	myList := []data.Maybe[int]{
		data.Just(12),
		data.Just(2),
		data.Nothing[int](),
		data.Just(8),
	}

	go func() {
		for _, v := range myList {
			ch <- v
		}

		close(ch)
	}()

	return ch
}

Now we have an “infinite” channel where with values from the user. The value we are reading is maybe an int.

Previously we creates a Foldl function with a slice argument, now we have a channel, so we need a FoldlIter first:

func FoldlIter[T any, R any](init R, list <-chan T, fold func(carry R, next T) R) R {
	for value := range list {
		init = fold(init, value)
	}

	return init
}

Let’s fold out “user input”.

import (
	"constraints"
	"fmt"

	"github.com/yitsushi/go1-18-experiments/pkg/data"
)

func add[T constraints.Ordered](c data.Maybe[T], value data.Maybe[T]) data.Maybe[T] {
	if c.IsNothing() {
		return value
	}

	if value.IsNothing() {
		return c
	}

	return data.Just(c.Value() + value.Value())
}

func main() {
	result := data.FoldlIter(
		data.Just(0),
		readFromUser(),
		add[int],
	)

	fmt.Println(result)
}

Thanks to generics, we can even abstract out or add function. It can work with numbers, and even with string values.

With the same logic, we can create an Either type. Is it useful? Next I’ll come back with an answer.