Premise

Go is great. Scala is great. Scala has Trys. Go doesn’t.

Motivation

I’m a horrible person. ¯\_(ツ)_/¯

Horribleness

In Go it’s typical to return errors 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:

def configLoad()                   : Try[String]         = ???
def getDbConnection(s: String)     : Try[sql.Connection] = ???
def queryForData(c: sql.Connection): Try[sql.Result]     = ???
def transformResults(r: sql.Result): Try[app.MyModel]    = ???

val model: Try[app.MyModel] =
  for {
    s <- configLoad()
    c <- getDbConnection(s)
    r <- queryForData(c)
    m <- transformResults(r)
  } yield (m)

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.

type Try struct {
    success bool
    value   interface{}
    err     error
}

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.

func ToTry(value interface{}, err error) *Try {
    return &Try{
        success: err == nil,
        value:   value,
        err:     err,
    }
}

Now we can wrap other functions that produce value/err results. Such as:

func doSomething() (string, error) {
    if someCheck() {
        return "biscuits", nil
    }
    return "", errors.New("You failed the all-important check")
}

func main() {
    var result Try = ToTry(doSomething())

    // use result as Try
}

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:

// NewSuccess creates a new succeeded Try
func NewSuccess(value interface{}) *Try {
    return &Try{
        success: true,
        value:   value,
        err:     nil,
    }
}

func NewFailure(err error) *Try {
    return &Try{
        success: false,
        value:   nil,
        err:     err,
    }
}

func (t *Try) IsSuccess() bool {
    return t.success
}

func (t *Try) IsFailure() bool {
    return !t.success
}

func (t *Try) GetValue() interface{} {
    if t.IsFailure() {
        panic("you do not know what you are doing") // totally right thing to do
    }
    return t.value
}

func (t *Try) GetFailure() error {
    if t.IsSuccess() {
        panic("you do not know what you are doing") // totally right thing to do
    }
    return t.err
}

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:

func (t *Try) Map(f func(interface{}) interface{}) *Try {
    if !t.success {
        return t
    }
    return &Try{
        success: true,
        value:   f(t.value),
        err:     nil,
    }
}

func (t *Try) FlatMap(f func(interface{}) *Try) *Try {
    if !t.success {
        return t
    }
    return f(t.value)
}

func (t *Try) FlatMapWrap(f func(interface{}) (interface{}, error)) *Try {
    if !t.success {
        return t
    }
    return ToTry(f(t.value))
}

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:

func test()          (string, error) { return "", nil }
func test2(s string) (int, error)    { return 128, nil }
func test3(i int)    *Try            { return NewSuccess(true) }
func test4(b bool) string {
    if b {
        return "true"
    }
    return "false"
}

func main() {
    ToTry(test()).FlatMapWrap(func(s interface{}) (interface{}, error) {
        return test2(s.(string))
    }).FlatMap(func(i interface{}) *Try {
        return test3(i.(int))
    }).Map(func(b interface{}) interface{} {
        return test4(b.(bool))
    })
}

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):

package try

import (
	"github.com/cheekybits/genny/generic"
)

type Type generic.Type

type TypeTry struct {
	success bool
	value   Type
	err     error
}

func ToTypeTry(value Type, err error) *TypeTry {
	return &TypeTry{
		success: err == nil,
		value:   value,
		err:     err,
	}
}

func NewTypeSuccess(value Type) *TypeTry {
	return &TypeTry{
		success: true,
		value:   value,
		err:     nil,
	}
}

func NewTypeFailure(err error) *TypeTry {
	return &TypeTry{
		success: false,
		err:     err,
	}
}

func (t *TypeTry) IsSuccess() bool {
	return t.success
}

func (t *TypeTry) IsFailure() bool {
	return !t.success
}

func (t *TypeTry) GetValue() Type {
	if t.IsFailure() {
		panic("you do not know what you are doing")
	}
	return t.value
}

func (t *TypeTry) GetFailure() error {
	if t.IsSuccess() {
		panic("you do not know what you are doing")
	}
	return t.err
}

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:

# inside the root of your project
mkdir -p try
genny -in try_base.go.tmp -out try/int_base.go gen "Type=int"

And you can now write code like:

var it IntTry = NewIntSuccess(5)
if it.IsSuccess() {
    fmt.Println(it.GetValue())
}

Of course, without being able to compose things with Trys of other types, this isn’t that great. So let’s make a template that allows us to map from one type to another:

package try

import (
	"github.com/cheekybits/genny/generic"
)

type FromType = generic.Type
type ToType = generic.Type

func (t *FromTypeTry) MapToToType(f func(FromType) ToType) *ToTypeTry {
	if !t.success {
		return &ToTypeTry{
			success: false,
			err:     t.err,
		}
	}
	return &ToTypeTry{
		success: true,
		value:   f(t.value),
		err:     nil,
	}
}

func (t *FromTypeTry) FlatMapToToType(f func(FromType) *ToTypeTry) *ToTypeTry {
	if !t.success {
		return &ToTypeTry{
			success: false,
			err:     t.err,
		}
	}
	return f(t.value)
}

func (t *FromTypeTry) FlatMapWrapToToType(f func(FromType) (ToType, error)) *ToTypeTry {
	if !t.success {
		return &ToTypeTry{
			success: false,
			err: t.err,
		}
	}
	return ToToTypeTry(f(t.value))
}

With this I can replace our original genny command with these two:

genny -in try_base.go.tmp    -out try/try_base.go    gen "Type=string,int"
genny -in try_compose.go.tmp -out try/try_compose.go gen "FromType=string,int ToType=string,int"

And now I have two Try types, IntTry and StringTry and can seamlessly convert back and forth like so:

func main() {
	composedValue := try.NewStringSuccess("37").
		FlatMapWrapToInt(strconv.Atoi).
		MapToString(func(i int) string {
			return fmt.Sprintf("Parsed to: %d", i)
		})

	if composedValue.IsSuccess() {
		fmt.Println(composedValue.GetValue())
	}
}

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… ¯\_(ツ)_/¯