Skip to content

Instantly share code, notes, and snippets.

@dbousamra
Last active August 29, 2015 14:14
Show Gist options
  • Select an option

  • Save dbousamra/6122d0e5328cbf7b1d9f to your computer and use it in GitHub Desktop.

Select an option

Save dbousamra/6122d0e5328cbf7b1d9f to your computer and use it in GitHub Desktop.

Was thinking a bit about what you said yesterday around using values as exceptions, and think I misunderstood your point.

That post was around Go's handling of errors. They encode an error or exception as a second return value from a function (effectively a tuple). I.e. the function:

func Open(name string) (file *File, err error)

returns an error as it's 2nd return value. You then capture that at the calling site:

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
} else {
    // do something with the open *File f
}

I think that's fucking awful for two reasons:

A function returning two values feels weird. Either it fails... or it doesn't, and you can use your value. The big problem with this approach is you can FORGET to deal with the error value, and bam! Your program is an invalid state and can't continue. There's no compile time checking around the handling of the error. In my opinion the error situation, should be encoded into the type system, and the compiler should enforce some sort of handling. There are tools like https://github.com/kisielk/errcheck/ that you can lint your code with that let you know if you've failed to deal with it... but cmon, that should not be required.

You're probably thinking (in response to point 2)... but isn't that what Java does? I.e. - checked exceptions? You can't throw an exception inside a method without annotating that method’s signature to indicate you may do so, and you can’t call a method that may throw an exception without wrapping it in code to handle the potential exception.

This solution is better than Go's (or any other language that doesn't enforce handling of an error condition at compile time). The problems with checked exceptions (at least Java's impl) are:

Some exceptions are checked, some aren't. Throw a new RuntimeError() and boom program crashed. Exceptions aren't values. I mean they are, but you can't assign an exception to a val/var. You can't fold/map/filter on them. You can't compose or transform them. You can only deal with them inside a try block. This gets real ugly, real quick which leads to: It takes real discipline to try catch everywhere. Instead, you take shortcuts and bubble it up the chain, till you end up with a public static void main throwing Exception e and crashing at every exceptional scenario. Exception handling is baked into the syntax of the lang.

The approach I like is that employed by Rust, Scala and Haskell. Errors/Exceptions are one half of a value type (the other half being the success path), encoded inside the result of a function.

Take the following pseudo-scala:

val user: Try[User] = Try(database.fetchUserById("42"))

Value at this point is a Try[User]. If you look at the impl of Try (https://gist.github.com/dbousamra/f5f04de9889b87b1acb8) you can see it's a sum type with two paths. The happy path (capturing the value) and the failure path (capturing the error path). Anyone who wants to get the User out of the user variable, needs to explicitly handle both:

Try(database.fetchUserById("42")) match {
  case Success(user) => println("We got a user: " + user)
  case Failure(f) => println("We got a failure: " + f)
}

All Try does is execute a try catch in it's apply method, and return either a Success or Failure.

Given they are just values that we can assign and play with, there are tonnes of ways to write higher order fn's that make it a lot easier to work with. More interestingly, they are just monads:

def getBrothersOverAge20FromUserId(id: Int): Try[List[Brother]] = {
  database.fetchUserById("42").map(user => user.brothers).filter(brother => brother.age > 60)
}

Notice the map acts on the happy path only, if present. If fetchUserById failed, the code still returns a Try[List[Brother]] and we are forced to deal with the exception if it occurred. The fact it's a monad also means they compose with other monads, but I don't have a good example demonstrating that :(

It's not a huge amount different to checked exceptions, but I like this approach because:

  • Explicitly handle errors. You can't ignore them.
  • Errors are values. Functions deal with values, so either return a default value if you an exceptional state, or crash your program.
  • Functional - map, fold, filter. 'Nough said.
  • Just library code. There's no syntax specific stuff (ignoring the try catch in the Try impl - which is just sugar).

Well fuck, that turned into an essay. Maybe I'll turn it into a blog post. Interested to hear your thoughts on my rambling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment