Functional programming and Go

The Go Time podcast on functional programming (fp) was an interesting listen and as someone who has done many years of both Scala and Go it's a topic I'm interested in.

The show talked about the stuff you'd expect, pure functions, immutability, higher-order functions etc. Here's some thoughts I'd like to add on top.

Errors

Let's have a look at a function written in a few programming languages

Go

func Divide(x, y int) (int, error)

Scala

def Divide(x: Int, y: Int): Either[Exception, Int]

Java

public Int Divide(int x, int y)

What does the Go and Scala one have in common that Java does not?

You can trust the function from the signature. You do not need to read the function body to understand that it can fail.

In Java it would not be surprising for the function to throw an exception. Unless the author decided to use checked exceptions it means the user has to inspect the function body to see if anything potentially bad can happen.

At first throwing exceptions can seem convenient but in the long run become a pain.

FP values explicitness and and so does Go. It is recommended that you do not write packages that panic because that makes your code more difficult to work with and trust.

Errors as values

Go (and Scala) treat errors as values just like any other data in our system.

Crucially there is no special special syntax to work with errors. Error is just an interface that everyone happens to use as it's built-in.

Go 2 has proposals to make working with errors more convenient because error handling in Go right now can be fairly verbose.

a, err := Divide(2, 0)

if err != nil {
    return 0, fmt.Errorf("oh no %v", err)
}

b, err := Divide(2, 2)

if err != nil {
    return 0, fmt.Errorf("oh no %v", err)
}

return a + b, nil

For Go 2, we would like to make error checks more lightweight, reducing the amount of Go program text dedicated to error checking. We also want to make it more convenient to write error handling, raising the likelihood that programmers will take the time to do it.

The main problem with errors in Go is they do not safely compose and this is an important property (and advantage) of FP

Let's see how Scala let's us safely compose functions that can sometimes fail

val result: Either[Exception, Int] = for {
  a <- Divide(2, 0)
  b <- Divide(2, 2)
} yield(a + b)

Explanation

  • In this case, the 2nd Divide would not run and result would equal Left[ArithmeticException("Cannot divide by 0")]
  • If we changed the first divide so it would work it would run the 2nd Divide and then in the yield it is able to used the "unwrapped" a and b to simply add them as if they are two normal Ints

I need to stress this isn't special syntax, the for syntax works with any type that has map and flatMap. (cough monads cough)

Go 2, errors and generics

I worry about the proposals around errors in Go 2 because they are making errors "special". It is adding ad-hoc syntax and that doesn't feel very "Go like" to me.

Go 2 also has generics under consideration but the two topics seem to be discussed in separation a lot of the time. FP has identified patterns and datatypes for safely and easily working with errors; and they almost all rely on generics.

Instead of

type Reader interface {
        Read(p []byte) (n int, err error)
}

Maybe we could have

type Reader interface {
        Read(p []byte) Either<Int, error>
}

With a good generics solution we wouldn't have to have special syntax that only work with errors and errors can retain their non-special status they have right now.

We'd still need some kind of new syntax to help with composition (like the Scala one earlier) but it wouldn't be tied down to a specific datatype; and could be based on proven and existing patterns from other FP languages.

(Incidentally, I find Kotlin suffers from trying too hard to not be like Scala and instead invents syntax and idioms which are not as portable as learning about say.... monads.)

Wrapping up

Go already values a lot of principles from FP. Rather than creating new syntax that makes errors somehow different and special, wouldn't it be better if Go continued to lean on FP principles to help Go programmers work with errors a little more elegantly?

I could be totally off the mark (this is very likely, I am not a language designer). It can definitely be argued that bringing in more elements from FP is also not "Go like".

Generics are simple but not always easy. Adding ad-hoc syntax to work with errors is probably easy but not simple.

Go errors right now are simple and easy. If we dont want to tack on more complexity just for the sake of making code less verbose... maybe dont bother and leave errors as they are?

What's the difference between simple and easy?

Simple made easy is one of my most favourite tech talks.

Rich Hickey emphasizes simplicity’s virtues over easiness’, showing that while many choose easiness they may end up with complexity, and the better way is to choose easiness along the simplicity path.