Joseph Kain bio photo

Joseph Kain

Professional Software Engineer learning Elixir.

Twitter LinkedIn Github

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

  1. Two of the results are nested Option values
  2. 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.