Skip to content

Instantly share code, notes, and snippets.

@preslavrachev
Last active November 16, 2021 10:19
Show Gist options
  • Save preslavrachev/2c1913e3007384c05e0c54755e7648c7 to your computer and use it in GitHub Desktop.
Save preslavrachev/2c1913e3007384c05e0c54755e7648c7 to your computer and use it in GitHub Desktop.
Generic Go Optionals

Goal

The goal of this experiment is to try and implement the minimum requirements for the existence of a generic-style Option type in Go, similar to what we know from other languages, such as Java or Rust.

Motivation

Provide a fluent approach to error handling in Go which focuses on highlighting the "happy path", while being 100% idiomatic with Go's error handling convention.

NOTE: The core type is an interface called Option, having a single requirement:

type Option[T any] interface {
	Res() (T, error)
}

This makes it easy for consumers to implement an Option in whichever ways they see fit.

There are two other important cases here, provided as simple package-level functions:

func Try[T any](val T, err error) Option[T]

Analogous to an Option constructor, this function can directly take the result/error pair coming from a standard Go function, and produce an Option instance

The second one is Unwrap:

func Unwrap[T, K any](o Option[T], fn func(T) (K, error)) Option[K]

Unwrap is where the fun with optionals happens. It would take an Option instance, as well as a continuation function, resulting in a new Option instance. Depending on whether the continuation function returns a value or an error, the new Option instance may serve as a fast-track error propagator in long call chains (see the example).

NOTE: Why is Unwrap a package function and not an Option method? Indeed, it would have been much more convenient to have Unwrap be a method on an already created Option instance. Howeve, at the moment, there is a limitation of the current implementation of generic type parameters. For the same reason, func(T) (K, error) cannot be extracted into its own convenience type that implements Option.

Improvement suggestions are more than welcome.

func main() {
opt := Try(successFunc())
opt = Unwrap(opt, func(in int) (int, error) { /* Simple funcs can also be inlined */ return in + 2, nil })
opt = Unwrap(opt, erroringFunc) // The fun stops here. The error will be propagated back to the end of the chain
opt2 := Unwrap(opt, successConvertFunc)
res, err := opt2.Res()
if err != nil {
panic(err)
}
fmt.Println(res)
}
func successFunc() (int, error) {
return 10, nil
}
func erroringFunc(in int) (int, error) {
return 0, errors.New("boom")
}
func successConvertFunc(in int) (string, error) {
return "42", nil
}
package main
import (
"errors"
"fmt"
)
// Option represents a value that may be present or not.
type Option[T any] interface {
Res() (T, error)
}
// optionImpl is a concrete implementation of Option.
// A less Java-like name suggestion is more than welcome.
type optionImpl[T any] struct {
val T
err error
}
// NewOption creates a new Option with the given value.
func NewOption[T any](val T, err error) Option[T] {
return &optionImpl[T]{val: val, err: err}
}
// NewOptionErr is a convenience function for directly creating an Option with an error.
func NewOptionErr[T any](err error) Option[T] {
return &optionImpl[T]{err: err}
}
func (o *optionImpl[T]) Res() (T, error) {
return o.val, o.err
}
// Try is a convenience function for creating an Option from a function that may return an error.
func Try[T any](val T, err error) Option[T] {
return NewOption(val, err)
}
// Unwrap is a convenience function for unwrapping an Option.
// If the Option is an error, the error is propagated back to the end of the chain.
func Unwrap[T, K any](o Option[T], fn func(T) (K, error)) Option[K] {
val, err := o.Res()
if err != nil {
return NewOptionErr[K](err)
}
return NewOption(fn(val))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment