Positive, negative, or error?
Posted at 08:32 on 19 January 2018
In my recent blog post about error handling in Go, I wrote about why I thought that they made the wrong decision by not implementing exception handling. One of the posts I linked to was this article by a Go developer called Dave Cheney.
In another article by the same author, he says that he will prove that Go's error handling is superior. However, the example he gives is based on flawed logic. Nevertheless, it's instructive to look at it, because there are some important things that we can learn from it.
His argument is based around this snippet of code:
package main import "fmt" // Positive returns true if the number is positive, false if it is negative. func Positive(n int) bool { return n > -1 } func Check(n int) { if Positive(n) { fmt.Println(n, "is positive") } else { fmt.Println(n, "is negative") } } func main() { Check(1) Check(0) Check(-1) }
which, as he points out, gives this answer:
1 is positive 0 is positive -1 is negative
which is wrong, because 0 is not positive. However, as he says, 0 is not negative either. To fix this, he proposes returning an error condition as well:
// Positive returns true if the number is positive, false if it is negative. // The second return value indicates if the result is valid, which in the case // of n == 0, is not valid. func Positive(n int) (bool, bool) { if n == 0 { return false, false } return n > -1, true } func Check(n int) { pos, ok := Positive(n) if !ok { fmt.Println(n, "is neither") return } if pos { fmt.Println(n, "is positive") } else { fmt.Println(n, "is negative") } }
Here's the flaw.
What a function does should be defined by its name and any relevant established conventions.
The flaw in Dave's logic is that he is trying to use the Positive()
function for purposes for which it should not be intended. There is nothing in the function's name that tells you that it will determine whether or not a number is negative. It only tells you that it will determine whether or not it is positive.
You can see this more clearly if we change the requirements a bit. What happens if he was asked to produce a program that, rather than telling whether a number is positive or negative, would tell whether it was prime or Fibonacci? The series of prime numbers goes 2, 3, 5, 7, 11, 13, 17, 19 ... whereas the series of Fibonacci numbers goes 1, 1, 2, 3, 5, 8, 13 and so on. But should we have functions IsFibonacci()
and IsPrime()
that throw errors for 4, 6, 9, 10, 12, 14, 15, 16, 18, 20? Of course not!
What he needs to do instead is declare a second function, Negative()
:
func Positive(n int) (bool) { return n > 0 } func Negative(n int) (bool) { return n < 0 } func Check(n int) { pos := Positive(n) neg := Negative(n) if pos { fmt.Println(n, "is positive") } else if neg { fmt.Println(n, "is negative") } else { fmt.Println(n, "is neither") } }
Neither a function called Positive()
nor one called Negative()
should have any preconditions whatsoever. A number — any number, zero included — is either positive or it isn't. Zero is not positive, so you would return false. If we were dealing with floating point numbers, NaN
(not a number) would not be positive, so you would return false. Strictly speaking, in dynamically typed languages, null, "Hello world"
, an HTTP client class, or an aardvark, are not positive either, so you would return false.
Remember that, whether you are using exceptions or error codes, an error indicates that your function could not do what its name says that it does. Making zero — or anything else — an error condition violates this rule, is outside of the scope implied by the function's name and any well known conventions I can think of, and as such, it is counterintuitive, confusing, and wrong.