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 anint
with the value of12
Just "text"
which is astring
with the value oftext
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.