Some programming languages treat errors as values that are returned, as opposed to stopping the program and showing the error message right away. The problem with stopping the execution and possibly catching the errors to handle them leads to similar kinds of problems as with goto
statements mentioned by Dijkstra in his famous paper.
One of the examples of languages that handle errors as values is Go, instantly recognizable for the notorious
if err != nil {
return err
}
code blocks that can be found everywhere in Go code. A similar approach is taken by Rust and Haskell (see also here). The difference is that the two latter languages have a richer type system and use dedicated types for such cases. Rust uses the Result
type. The type can take two possible values, Ok
and Err
, for the result of the happy path and the exception.
enum Result<T, E> {
Ok(T),
Err(E),
}
If every function returns the Ok
or the Err
exception, this means that all the downstream computations need to handle both values. This could easily turn into a mess, hence Rust has many ways of handling such returns. One of the great utilities is the ?
syntax. Imagine that you have the thing
thing of the Result
type, then thing?
will extract the value
from Ok(value)
and in case of Err(msg)
will instantly return the Err(msg)
. Let me illustrate this with an example.
Not a FizzBuzz
I’ll write a program that takes a number as input and does the following:
- If the number is divisible by 3, throw the “fizz” error, otherwise, add 3 to the number and pass the result to the next step.
- If the number is divisible by 5, throw the “buzz” error, otherwise, add 5 to the number and return it.
It’s not exactly the FizzBuzz, but will suffice as an example. In Rust, we could write the two functions
fn fizz(num: i32) -> Result<i32, String> {
if num % 3 == 0 {
return Err(String::from("fizz"));
}
Ok(num + 3)
}
fn buzz(num: i32) -> Result<i32, String> {
if num % 5 == 0 {
return Err(String::from("buzz"));
}
Ok(num + 5)
}
and then call both of them with
fn notfizzbuzz(num: i32) -> Result<i32, String> {
match fizz(num) {
Ok(val) => buzz(val),
Err(msg) => Err(msg),
}
}
As you can see, it’s not the prettiest and would get even worse if we needed to use such blocks repeatedly in different places. Instead, we could use fizz(num)?
, to simplify the code.
fn notfizzbuzz(num: i32) -> Result<i32, String> {
?)
buzz(fizz(num)}
This will return the Err
if fizz(num)
raised such and otherwise pass the value from Ok(val)
to buzz
. It works because both fizz
and buzz
have the same return types.
Haskell does not have ?
Haskell would not allow for such an early return. But let’s not jump ahead. First of all, Haskell leaves it to the user how to handle the errors, without suggesting solutions. As Rust, it has the Maybe
type for nullable values, but it does not have Result
. Instead, it has the Either
type with Left
and Right
fields. But instead of using it, I’ll create my custom type.
data Result t
= Ok t
| Err String
Now we can write the same two functions in Haskell
fizz :: Integral t => t -> Result t
| num `mod` 3 == 0 = Err "fizz"
fizz num = Ok $ num + 3
fizz num
buzz :: Integral t => t -> Result t
| num `mod` 5 == 0 = Err "buzz"
buzz num = Ok $ num + 5 buzz num
and write the same clumsy
notfizzbuzz :: Integral t => t -> Result t
=
notfizzbuzz num case fizz num of
Ok val -> buzz num
Err msg -> Err msg
Haskell does not have ?
, or maybe I should say that my Haskell-fu is weak, and maybe there is something, but I didn’t find it. Hopefully, we can implement it ourselves! Instead of having ?
as a special syntax, we could define it as an operator ?>
that takes a value of Result t
type as the left-hand side argument and function f
that maps the value of type t
to the Result t
, i.e. (t -> Result t)
, as the right-hand size argument, and returns Result t
. Again, the types match, so we can easily pass the values.
(?>) :: Result t -> (t -> Result t) -> Result t
?>) (Ok x) f = f x
(?>) (Err msg) _ = Err msg (
Now, like in Rust, notfizzbuzz
becomes a one-liner:
notfuzzbuzz :: Integral t => t -> Result t
= fizz num ?> buzz notfuzzbuzz num
It can be used for chaining functions, but that’s great since this is how we would write them in Haskell. Early return at any point of the function is not possible, but it will work the same for the Haskell-like code as above. This is how we can have ?
in Haskell or any other functional language.
This is just Either
That said, my custom Result
type with ?>
behaves like the Either
monad used together with >>=
. In fact, >>=
for Either
is defined as
instance Monad (Either e) where
Left l >>= _ = Left l
Right r >>= k = k r
You may notice that Left
here has the same definition as Err
and Right
as Ok
. This shows that in both cases we are dealing with a monad, Rust and Haskell just use different syntaxes for dealing with them. We don’t need ?
in Haskell, because we have things like >>=
.