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 12which is anintwith the value of12Just "text"which is astringwith the value oftextNothingwhich, 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.