logo

gertjanassies.dev


ramblings of a chaotic mind

An Optional type in Go - an experiment

on 2022-05-28 by Gertjan Assies reading time 0 min, viewed 4 times, read 0 times, liked 0 times

An Optional type in Go - an experiment

A common pattern in Go is to have your function return an error next to the result to signify a failure inside the function like so:

func Divide(a float64, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("Divide by zero, really?")
    }
    return a/b
}

What if we could capture this in a Type with a couple of methods like so:

package optional

Type Maybe[T] struct {
    Value T
    Error error
}

type Optional interface {
    New()       // Constructor
    Failed()    // true if Error
}

Then our Divide function can become:

func Divide(a float64, b float64) Maybe[float64] {
    if b == 0 {
        return optional.New(Value: 0, Error: errors.New("Divide by zero, really?"))
    }
}

maybeDivived := Divide(2,4)
if maybeDivided.Failed() {
    fmt.Printf("Divide failed with %v", maybeDivided.Error)
}

Now this is even more verbose then the original example, that’s not improving on the situation. So lets try to implement a Map, where we can apply a function on the Value inside the Maybe and return a Maybe with the result of the function.

func Map[S any, T any](m Maybe[S], f func(S) (T, error)) Maybe[T] {
    if m.Error != nil {
        var unit T
        return New(unit, m.Error)
    } else {
        r, err := f(m.Value)
        return New(R, err)
    }
}

So here we take a:

  • Maybe[S] and unwrap the S
  • apply a function from S -> T on the inside Value
  • wrap again and return the Maybe[T]

When there’s an Error in the Maybe[S], I return a Maybe[T] with an uninitialized var of type T (which is kinda the Nothing type in Go, if you don’t look too closely) and the error, if not I’m executing the function and returning the result with any error that might have happened in that function

So now running:

maybe := New("2022-05-27T12:24:00Z", nil)
f := func(date string) (time.Time, error) {
    return time.Parse(time.RFC3339, date)
} // returns a func(string) (time.Time, error)
result := Map(maybe,f) // returns a Maybe[time.Time]

Will parse the string as a time.Time and handle the error when the string cannot be parsed, OK this is becoming a little more useful.

But what if I want to chain a bunch of functions together that all could potentially give an error. Let’s implement a let’s call it `AndThen` function

func (m Maybe[T]) AndThen(f func (T) (T, error)) Maybe[T] {
    return Map(m, f)
}

As you can see AndThen is just a Map function. but now it applies to the struct

addOne := func (x int) (int, error) { return x+1, nil }
multThree := func (x int) (int, error) { return x*3, nil }

maybe := New(42, nil)

res := maybe.
        andThen(addOne).
        andThen(multThree).
        Value
// 129

Unfortunately I couldn’t get the AndThen to work with different input and result types. maybe someone more familiar with Go Generics can explain to me how that is done, if at all possible.

Here I come to the conclusion that the chainable AndThen functionality is actually useful. especially when it’s possible to use different types for in- and output.

But I’m also coming to the conclusion, that implementing a pure functional library in Go is maybe not a good idea, as the language is not really suited for that. and that’s fine. go learn yourself a bit of Haskell if you want that.
The number of abandoned ‘experiments’ I found on public repo’s also attested to this

All the code used is in this repo: https://gitlab.com/gertjana/optional-go/-/blob/main/cmd/optional/optional.go

Hope you learnt something, I did

UPDATE:

It is indeed not possible to have the AndThen method on the Maybe with a function with different types, as all the types you use need to be defined on the Maybe.

Maybe there is a way to mimic union types with interfaces and get it working like that, but that’s something for a new experiment.

Opinions expressed here are my own and not the views of my employer or anyone else, (re)use is free, but quoting the source is appreciated.
This blog is licensed under a Creative Commons Attribution 4.0 International License. © 2023 by Gertjan Assies