TL;DR look at it here: https://pkg.go.dev/github.com/ivypuckett/to@v1.0.0
Go is a wonderful language, but I keep writing the same three lines of code over and over again:
if err != nil {
return nil, err
}
Once is fine but if I get unlucky, I may add 15 extra lines of code over the scope of a single method... Just to return errors. Having already implemented this pattern in C#, I knew that it should be possible to eliminate this boilerplate in Go.
The goal
Implement a forward pipe operator / bind monad which enables chaining multiple methods where the input of the next is the output of the last. Upon the first error returned, return that error and the zero value of the expected result. If no errors are returned, return the result of the last operation.
The pain
In C#, the forward piping is fairly easy. I created an extension method on the base object which allows any object to call .Pipe(methodName)
. That means someone can call 1.Pipe(Increment).Pipe(Increment)
and receive 3.
Go is not an object oriented language (thank goodness), but it still has methods. My first thought was to create a struct called Chain with a method .Bind
which takes in the next method and returns a new Chain object. It'd be a lot like a linked list. I found out that Go does not support generics on methods. There's no way (I've found) to create a struct with an unbound type.
The next thought was to create a Compose[T, U, V any](func(T)(U, error), fn2(U) (V, error)) func(T) (V, error)
function. This kind of function would allow someone to recursively bind functions by calling Bind(fn1, Bind(fn2, fn3))
. The syntax was too cumbersome though and there's a small bit of overhead with the number of function calls.
Using variable arguments and type assertions is also promising, but I didn't want to utilize reflection as there's overhead in that as well.
The semi-elegant solution
Ultimately, the interface for this is to.Bind3(input, fn1, fn2, fn3)
. There is only one extra character and the only overhead is the function call itself. The package provides 1-32 parameter variants.
I considered using bind.Three
as it fills a sentence better, but I thought better of it as it may break a programmer's flow easier if we have to think of how to spell a word instead of just typing the new number on a keyboard.
Example
// Before
func SaveWithoutBind(input string) (path, error) {
sanitized, err := sanitize(input)
if err != nil {
return "", err
}
cased, err := correctCasing(sanitized)
if err != nil {
return "", err
}
queried, err := getExtraInfo(cased)
if err != nil {
return "", err
}
path, err := submit(queried)
if err != nil {
return "", err
}
return path, nil
}
// After
func SaveWithBind(input string) (path, error) {
return to.Bind4(input,
sanitize,
correctCasing,
getExtraInfo,
submit)
}
Install
go get github.com/ivypuckett/to@v1
Links
- Repo: https://github.com/ivypuckett/to
- Go Package: https://pkg.go.dev/github.com/ivypuckett/to@v1.0.0
Image credit: https://unsplash.com/photos/people-partying-with-confetti-ZODcBkEohk8
Top comments (0)