Boilerplate
Last week I described my experiences with the tree operations from section 3.5 of Functional Programming in Scala. And this week I’ll discuss Chapter 4 on error handling without using exceptions.
I’ve uploaded my tests and exercises to github. If you want to work through the exercises for yourself, then I recommend you grab the tests from the github repo now and work through them before reading the rest of this post.
Error handling
I have to admit, I was tempted to skip this chapter and come back to it later. I don’t find error handling all that exciting and the thought of Chapter 5 on “Strictness and Laziness” nearly lured me away. But, given my background in C I’ve never really been liked exception based error handling so I was interested to see the non-exception approach Functional Programming in Scala would take. I’m glad I ended up working through this chapter as I learned a lot and the exercises were quite rewarding.
Optional values
The text makes use of the Option
type or Optional values which are part of the
Scala standard library. I had never heard of this concept until very recently
when I was briefly studying the Swift language. Elixir also makes use of
Option
values but with a little less formality. There’s no special class or
library, there are just tuples structured like {:ok, foo}
and {:error, "A
reason"}
. I choose to follow this convention as my goal is to solve the text’s
exercises using the Elixir way. However, I did retain the functions
some(value)
, corresponding to {:ok, value}
, and none
, corresponding to
:error
, from the text as I think their vocabulary is useful when discussing
error handling.
Functions: map, flat_map, get_or_else, and filter on Option
Exercise 1 asked the reader to implement map, flat_map, get_or_else, or_else,
and filter on the Option
type. The text poses these functions as a way of doing
computation on values that may or may not exist while deferring any error handling until
later. For example, the map
function can be used to execute a function on a
value that may not exist. That is,
Option.map({ok: value}, f)
should evaluate to f(value)
, but
Option.map(:error, f})
should return :error
. This allows deferring the handling of errors
until the end of a long computation or leaving it to the caller to decide what
to do in the case of an error.
Initially, I had some trouble understanding the question in Exercise 1. It
wasn’t clear to me what kind of value the functions should return (I hesitate to
use the word ‘type’ as Elixir is weakly typed.). I wrote up a version of map
that handled the :ok case and then looked at the solution. The solution told me
that I need to return a new :ok tuple containing the mapped value when the input
was :ok and :error on :error. Once I understood this I was able to write up this
recursive, pattern matching version of map
:
def map({:ok, value}, f), do: some(f.(value))
def map(:error, _f), do: none()
The rest of the functions could be built on top of map
and get_or_else
.
Reflections
Reflecting on my difficulty understing exactly what Exercise 1 was asking for I
realized that I wasn’t paying attention to the Scala code in the text. The text
shows me the signature for each function, for map
it gives:
def map[B](f: A => B): Option[B]
and based on the the return type I should have known that I have to return an
Option
value and it should be an Option
on type B, the result type of the
function, f
, passed to map. The exercises in Chapter 2 actually made a point
of this kind of type analysis, and while I enjoyed those exercises immensely I
have ignored their lessons: I’ve engrossed my self in the typelessness of
Elixir. I need to endeavor to pay more attention to the types in the Scala
examples in the text.
I will point out that the test driven approach that I’ve been taking has produced a set of tests that serve to document similar behavior to that described by the text using Scala’s type system. My tests express this information as a set of expectations on the behavior of the functions rather than as a type description. It reminds me the idea I’ve seen tossed around that one function that unit tests serve is as a replacement for static, compile-time checks.
On a completely, different note, the C programmer in me initially cringed at the
overhead in this style of error handling, tuples wrapping all these values,
calling map
when you just want to simply invoke the function f
. But, from a
design point of view this strikes me as very clean. C code I’ve worked with the
in past is littered with error checks (or worse, neglects them). At best a
handful errors can be consolidated into a single check or error checks can be
made smaller by using goto
to jump to clean up code. The text’s approach
seems cleaner, and more functional. Using map
, filter
, etc. on these
optional values allows computations to be chained or composed without having in
inject checks in between stages of a computation. I think it will take me some
time to get used to this form of error checking but I do see the advantages over
the C style when absolute performance isn’t the highest priority.
Using flat_map
The next 2 exercises are examples of how to use map
and flat_map
. When working
through these I had some trouble understanding flat_map
so I thought it best
to describe it here and solidify my understanding.
Like map
, flat_map
applies a function f
to an Option
value. The
difference is that f
may fail and therefore f
returns an option value of its
own. Given such a function if we called
map(option_value, f)
the we would get back one of the following as a result:
{:ok, {:ok, value}} # If f succeeds
{:ok, :error} # If f fails
:error # If option_value was :error to begin with
This result isn’t what we want because
- Two of the results are nested
Option
values - The result isn’t consistently nested in the case of initial
:error
value.
Both of these make the result very cumbersome to work with. The text describes
map
and flat_map
as treating Option
values like lists of single elements
and with this thinking I see that flat_map
collapses these nested Option
values just as Enum.flat_map/2
collapses nested collections. That is
flat_map(option_value, f)
would give the following, more usable, results
{:ok, value} # If f succeeds
:error # If f fails
:error # If option_value was none to begin with
These results can be handled much more consistently.
map2
One nice byproduct of experimenting with flat_map
was the map2
function
which can be used to combine two Option
values with binary function while
handling the case that either value is :error
. As I mentioned I was having
trouble getting the hang of flat_map
so I wrote up a pattern-matting solution:
def map2(:error, _b, _f), do: none
def map2(_a, :error, _f), do: none
def map2({:ok, a}, {:ok, b}, f), do: some(f.(a, b))
Then, after consulting the text’s solution I wrote up this version using map
and flat_map
def map2(a, b, f), do: flat_map(a,
fn (aa) ->
map(b, fn (bb) -> f.(aa, bb) end)
end
)
flat_map
is used to collapse any nested Option
values that come out map
.
Note that map2
is used in some of the later exercises.
Sequence and traverse
Exercise 4 described a new function called sequence
which can be used to
transform a list of Option
values into an Option
on list of the original
values. That is, sequence
would transform [{:ok, 1}, {:ok, 2}]
into {:ok,
[1, 2]}
. In my first attempt I implemented sequence
using a recursive, pattern
matching approach. Then I reimplemented sequence
using Enum.foldr/3
.
Exercise 5 required me to write a function called traverse
that can be used to
Enum.map/2
a function, f
over a list where f
could fail and return
:error. This Enum.map/2
would yield a list of Option
values. To simplify,
one might want to transform, à la sequence
, the list of Option
values into
a single Option
of a list, i.e {:ok, [list]}
or :error
. The composition
of these operations is called traverse. However, a simple composition would
require two passes over the list but this exercises challenges the reader to
implement it more efficiently.
For me, the most difficult part of this exercise was to implement a function
that could fail to use in the traverse
. I adapted the example from the text
that parses strings into integers. This required handling exceptions from
String.to_integer/1
. First, to figure out what the name of the exception was
I played around with the function in iex:
iex(5)> defmodule Foo do
...(5)> def parse_int(s) do
...(5)> try do
...(5)> {:ok, String.to_integer(s)}
...(5)> catch
...(5)> what, value -> "Caught #{inspect what} #{inspect value}"
...(5)> end
...(5)> end
...(5)> end
iex:5: warning: redefining module Foo
{:module, Foo,
<<70, 79, 82, 49, 0, 0, 6, 72, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 116, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 2, 104, 2, 100, 0, 4, ...>>,
{:parse_int, 1}}
iex(6)> Foo.parse_int("1")
{:ok, 1}
iex(7)> Foo.parse_int("a")
"Caught :error :badarg"
Next, I wrote up the function I needed and wrote up my test. I was able to confirm the test was good by running it against this simple-but-inefficient version of traverse:
def traverse(l, f), do: Enum.map(l, f) |> sequence
Finally, with a known-good test I was able to develop my final, efficient version
def traverse(l, f), do: List.foldr(l, some([]), fn (x, acc) ->
map2(f.(x), acc, fn (h, t) -> [h | t] end)
end
)
Either values
In the next section the text introduces the Either
type which is an alternative to
Option
. Either
introduces a reason to go along with None
. In Elixir there
isn’t much required to extend the processing we’ve already developed to handle
this case. We just end up with values of the form {:error, "Reason"}
instead
of just :error
. The functions we’ve discussed just need to match error
cases a little differently but there is otherwise no change in their behavior.
Conclusion
This has turned into quite a long and detailed post for a Chapter that I initially considered skipping. I’m glad I went through these exercises and had a chance to learn so much. I’m considering cleaning up my solutions and putting them together into an Elixir package so I can continue working with this style of error handling. I certainly expect to use these functions in future exercises from Functional Programming in Scala and hopefully in other projects as well. If I do put a package together I’ll announce it here on this blog.
Next week I’ll be digging into Chapter 5: “Strictness and Laziness” and will write up another post describing my experiences.