Bastard Go - Try
Premise
Go is great. Scala is great. Scala has Try
s. Go doesn’t.
Motivation
I’m a horrible person. ¯\_(ツ)_/¯
Horribleness
In Go it’s typical to return error
s as soon as they happen and handle them locally, or
wrap them with some additional context and pass them back up the stack. In Scala however it’s
idiomatic to wrap an error into a Try
which represents either a successfully computed value
or an error. The advantage of a Try
over a 2-tuple (or multiple return-values) is that the
Try
can be composed so that you can chain functions together and worry if they failed later,
allowing the error to cascade. An example in Scala might look like:
Look how readable that is! Wow….
Above are several functions, meant to be called in order. Each function returns a possible
error and the next function takes the success value of the previous function. The code starting
with for {
is some syntactic sugar (mmmmmm sugar) that allows us to call map
and flatMap
which are essentially ways of calling the next function assuming the previous one succeeded.
If there was an error, then the value of model
will be a failed Try
(Failure
in Scala)
with the error.
This allows the programmer to write code that describes the “happy path”, dealing with errors at the last possible moment (just like real life). Some would argue this style is more readable (if at least not more relatable). ¯\_(ツ)_/¯
To be fair, this programming style has it’s own pattern named after trains and if that isn’t enough to convince you, then I think there are larger issues at play. Go read this two-part post, it’s super good so you should read it without me prompting you anyways: part-1, part-2.
I don’t want this to turn into an in-depth explanation of Try
in Scala, so go read the
docs here if you still want to know more.
Continued Horribleness
Let’s make a Try
in Go.
Since we don’t have generics let’s (for now) just use interface {}
as the value contained
within our Try.
Pretty self-explanatory. Also note that success
is used instead of checking for nil
on
success
or err
because it’s more explicit’ish.
Next a utility function.
Now we can wrap other functions that produce value/err results. Such as:
So we got a basic container, but that’s more or less useless. Especially given that there
are no publicly exported fields in our struct and no functions except our utility function
to create/use the Try
. Let’s define some basic functions for creating and interacting with
Try
:
This is good, but not entirely usable. As it stands this is just as wonderful as the
existing Go model of checking errors right at the source. We need some way for the errors
to cascade and for us to say what we want to do without having to worry about so much
error handling. For this we have to break out the big guns (pew pew), map
and flatMap
.
If you haven’t heard of these functions yet, maybe go try googling “higher order functions”
to get a better idea. I’ll avoid explaining them here since that could be a whole other post
on it’s own.
Let’s take a stab at this:
So now with this, we can attempt to chain some stuff together, with all the wonderfulness
that is working with interface {}
which is obviously the superior way of writing code in
Go:
I mean, can you not see the benefit of this yet? Who would not choose this highly readable, soooper maintainable code over immediate error checking? It’s always best to avoid your problems as long as possible (LPT).
Less Horrible?
Even though so far we have a working Try
complete with interface {}
everywhere (a favorite
style of “real programmers”), the benevolent overlords of Go built us go generate
and it would be a shame to not find a way to shove that into this blog post.
Thanks to genny, onwards and upwards my friends!
First a template without any of the transformation functions (Map
, FlatMap
, etc):
With this + genny
, we can generate some basic Try
code for a specific type without any composition
functions (we’ll get there). To get the basic support we’d simply run:
And you can now write code like:
Of course, without being able to compose things with Try
s of other types, this isn’t that great. So let’s
make a template that allows us to map from one type to another:
With this I can replace our original genny
command with these two:
And now I have two Try
types, IntTry
and StringTry
and can seamlessly convert back and forth
like so:
Conclusion
\o/ Yay! This is all very horrible, but it actually works in a usable way. Feel free to spread the love ;-)
github.com/johnmurray/bastard-go/
PS
If you made it all the way to the end and didn’t realize this was written with intentional sarcasm… ¯\_(ツ)_/¯