On being exceptional
What a better way to finish a tour than by talking about failing, right? Different languages solve the problem of failure differently. Many use the concept of exceptions: when an exception occurs, the program breaks its control flow and starts back-tracking the calls until it finds an appropriate exception handler. Other languages, such as Go do it differently, they employ error values that the programmer must manually check.
In this part, we'll talk about how error handling can be done in Funky. Of course, just like pretty much anything else, error handling isn't built-in to the language but is a part of the standard library instead.
The standard library currently only implements the simplest form of error handling: the Maybe
type. More sophisticated error types will be added when the need and specific use-cases arise.
The Maybe
type
Most functions always succeed. But some can also fail. To represent failure, we need a new type. The simplest (and so far only) type for this purpose is Maybe
:
union Maybe a = none | some a
It has two alternatives: none and some. The former is a failure. The latter is a success.
Do you remember the list manipulation functions first!
and rest!
? Well, they crash when the list is empty. Crashing the whole program isn't usually desirable and so there are non-crashing alternatives: first
and rest
. What do they return? A Maybe
of course:
func first : List a -> Maybe a
func rest : List a -> Maybe (List a)
For example:
func main : IO =
let [1, 2, 3, 4] \nums
println (
switch first nums
case none
"nothing there"
case some \x
"the first is " ++ string x
);
quit
It works:
$ funkycmd maybe.fn
the first is 1
If we don't wanna use the switch
, we can use none?
and extract!
instead:
func main : IO =
let [1, 2, 3, 4] \nums
println (
let (first nums) \maybe-x
if (none? maybe-x) "nothing there";
"the first is " ++ string (extract! maybe-x)
);
quit
Note. The function
extract!
can crash, so use at your own risk.
Yet another alternative would be to use ?
and map
. They're quite simple, but I'll explain them. Here are their types:
func ? : a -> Maybe a -> a
func map : (a -> b) -> Maybe a -> Maybe b
The ?
function takes a default value and a maybe and evaluates to the default value if the maybe is none
. Otherwise, it evaluates to the value inside the maybe. For example: 0 ? none
evaluates to 0
, while 0 ? some 3
evaluates to 3
.
The map
function transforms the value inside a maybe using the supplied function. If the maybe is none
, the result will be none
as well. For example: map (+ 1) none
results in none
, while map (+ 1) (some 3)
results in some 4
. It works very much like map
for lists.
We can use the map
function to turn the Maybe Int
obtained from first nums
into a Maybe String
like this:
map (\x "the first is " ++ string x) (first nums)
Then we can use ?
to fill the case when the list is empty:
"nothing there" ? map (\x "the first is " ++ string x) (first nums)
And here's the final program:
func main : IO =
let [1, 2, 3, 4] \nums
println ("nothing there" ? map (\x "the first is " ++ string x) (first nums));
quit
That line is quite long. Too long!
But, there's a solution! The functions ?
and map
have synonyms that are suitable for vertical code. Namely: if-none
and let-some
.
func if-none : a -> Maybe a -> a
func let-some : Maybe a -> (a -> b) -> Maybe b
The function if-none
is a precise synonym of ?
. And the only difference between let-some
and map
is the order of arguments.
We use let-some
to safely extract a value from a maybe:
let-some (first nums) \x
"the first is " ++ string x
The whole thing becomes a new maybe. To turn it into a naked value, we handle the none
case:
if-none "nothing there";
let-some (first nums) \x
"the first is " ++ string x
And here's the whole thing:
func main : IO =
let [1, 2, 3, 4] \nums
println (
if-none "nothing there";
let-some (first nums) \x
"the first is " ++ string x
);
quit
That's more readable!
Note. Of course, there are many cases when
?
andmap
are the right choices. Perhaps this one is too. Depends on your personal preference.
In this short refactoring exercise, we've seen most of the useful Maybe
functions: none?
, extract!
, ?
, map
, if-none
, let-some
.
There are some more, like let-::
, some?
, for-some
, filter-some
, and when-some
. You can try them out on your own.
Just for the sake of giving another example, here's a simple function to get the third element of a list:
func third : List a -> Maybe a =
\list
let-some (rest list) \tail
let-some (rest tail) \tail
first tail
Or, here's a more complicated example that uses let-::
(used for destructuring lists into the first element and the rest, otherwise very similar to let-some
):
func merge : List Int -> List Int -> List Int =
\left \right
if-none (left ++ right);
let-:: left \l \ls
let-:: right \r \rs
if (l < r) (l :: merge ls right);
r :: merge left rs
func merge-sort : List Int -> List Int =
\list
let (length list) \len
if (len <= 1) list;
let (take (len / 2) list) \left
let (drop (len / 2) list) \right
merge (merge-sort left) (merge-sort right)