In the Functional Programming (FP) realm monads are very useful. In fact you may have already used one without knowing it. If you’ve ever used jQuery, even under the hood, you’ve used a monad, but in this article I’m going to show you how to use one particular monad to handle errors in an efficient and elegant manner.
Now you might be thinking, I already know how to handle errors in an efficient and elegant manner… it’s called the Try statement.
And you would be right. Sort of. Try can be used for good, but it can also be used badly. Have you ever seen a traceback in production code? That’s a bad use of handling error conditions.
Raising a condition and handling it somewhere outside the method or procedure where it was raised invites all sorts of problems. The main issue is that it exits the method or proc by another path outside of the _return statement. If you want to decrease errors, you should avoid side-effects, mutating state and allowing your code to exit in multiple ways.
Solid code has one way into a method and one way out. In addition, the fact a condition can bubble up multiple levels, unwinding the stack each time, before it is handled means programs that do this are difficult to follow and reason about. They’re also inherently difficult to test.
However conditions, when used correctly, are great. They save us from having to check for errors after each step in our code. If you’ve ever tried to follow code with error handling statements interspersed you’ll know what I mean.
Take a look at this example.
_global c_style_programming <<
_proc @c_style_programming(p_msg)
_if p_msg _is _unset _orif p_msg.empty?
_then
_return -1
_endif
# Create socket
_if (socket_desc << socket(AF_INET, SOCK_STREAM, 0)) < 0
_then
write("Could not create socket")
_return -1
_endif
# Connect to remote server
_if connect(socket_desc) < 0
_then
write("Connect error")
_return -1
_endif
write("Connected")
# Send data
_if send(socket_desc, p_msg) < 0
_then
write("Send failed")
_return -1
_endif
write("Data Sent")
# Receive a reply from the server
_if recv(socket_desc, server_reply, 2000, 0) < 0
_then
write("Receive failed")
_return -1
_endif
write("Reply received: ", server_reply)
_return 0
_endproc
See how after every step that could cause an error we have to check if it successfully completed? That’s not the best pattern to follow.
In JavaScript, where callbacks are extensively used, the convention is to have the callback accept an error object argument. If the callback fails, the error parameter is set, otherwise the result is returned in another parameter. While slightly better than C-style error handling (because it separates the error from the return value), it still requires checking for the error.
Now look at this code (we’ll assume if a function fails, it will raise an error condition).
_global try_style_programming <<
_proc @try_style_programming(p_msg)
_try _with cond
# Create socket
socket_desc << socket(AF_INET, SOCK_STREAM, 0)
# Connect to remote server
connect(socket_desc)
write("Connected")
# Send data to remote server
send(socket_desc, p_msg)
write("Data Sent")
# Receive a reply from the server
recv(socket_desc, server_reply, 2000, 0)
write("Reply received: ", server_reply)
_when error
write(cond.report_string)
_return -1
_endtry
_return 0
_endproc
See how much easier it is to read when we can simply write our logic in one place and handle errors in another?
So conditions are definitely an improvement over the old C style of programming where failures were indicated by returning, for example, -1 from a function. The onus was on the caller to check for and handle errors. This resulted in convoluted functions, highly unreadable code and difficult to maintain and enhance programs.
But… what if there was an even better way to handle errors? Would you be interested?
What if we could write our code without having to worry about adding specific error handling? Just write the business logic and let ‘er rip. Sounds too good to be true, doesn’t it? But that’s exactly what monads, and in particular the Either monad, can help us do.
The Either Monad
Keep in mind monads are based in mathematics and came to the programming world during the 1950s. So there is a great deal of theory surrounding them and it can seem intimidating. However for our purposes, we will ignore just about all that theory and simply use the parts that make our programs robust (for an interesting overview of monads, YouTube has a video of Douglas Crockford giving a presentation in 2012).
So let’s start with a simple definition: A monad wraps a value and provides methods and rules to access it. See? In our non-math world a monad is very simple.
Look at the example below.
def_slotted_exemplar(:either, {
{:value, _unset, :writable}
} )
_method either.new(p_value)
>> _clone.init(p_value)
_endmethod
_private _method either.init(p_value)
.value << p_value
>> _self
_endmethod
We simply take the parameter and store it in the object’s slot. That’s the wrapping part.
To create a new object, call the new() method and pass in the value. Since Magik is dynamically typed, we’re free to pass in just about anything.
However we can save ourselves some typing by creating a helper method that by convention is called of(). Here’s what of() does.
_method either.of(p_value)
_return beeble_right.new(p_value)
_endmethod
It’s usually invoked directly on the exemplar (called a static method in other languages) and creates a new Either subclass object (we’ll get to that in a moment).
Next, we want to protect our programs from the dreaded _unset. So let’s write a method to do that. Again, by convention, we’ll call it from_nullable().
method either.from_nullable(p_value)
_if p_value _is _unset
_then
_return beeble_left.new(p_value)
_else
_return beeble_right.new(p_value)
_endif
_endmethod
This method lets us create an object whether or not the wrapped value is _unset. In standard OO practice, _unset is the bane of Magik programmers. But from_nullable() nullifies _unset (did you see what I just did there?).
So now we can wrap a value and we have some methods… consequently we just about have a monad!
Of course the point is to provide a benefit, so let’s add another method called map() to the mysterious, but soon to be explained, subclasses – I’m sure you’re seeing a pattern but I’ll say it one more time, although we can name these methods whatever we like, there are historical precedents that we should follow so others who are familiar with these concepts can understand what we’re doing. So map() it is.
_method beeble_right.map(p_fn)
_return either.of(p_fn.invoke(.value))
_endmethod
_method beeble_left.map(p_fn)
>> _self
_endmethod
Let’s also define a few test procs (and pay attention to the fact that none of them do any error handling whatsoever)…
_global zwrite <<
_proc @zwrite(p_value)
write("write: ", p_value)
_return p_value
_endproc
_global zshow <<
_proc @zshow(p_value)
show("show: ", p_value)
_return p_value
_endproc
_global zfirst <<
_proc @zfirst(p_value)
write("first: ", p_value.first)
write( 10 * p_value)
_return p_value
_endproc
_global zprint <<
_proc @zprint(p_value)
write("print:")
print(p_value)
_return p_value
_endproc
And just like that we have a barely useful monad. Here’s an example of how we can use it.
Magik> e << either.from_nullable("hello world").map(zwrite).map(zshow).map(zprint)
write: hello world
"show: " "hello world"
print:
char16_vector(1,11):
1 %h
2 %e
3 %l
4 %l
5 %o
6 %space
7 %w
8 %o
9 %r
10 %l
11 %d
a beeble_right
Notice how we’ve chained the calls to map() together. If a problem occurs in any of the methods, it’s skipped by the monad (or it will be just as soon as we write a bit more code) so we don’t have to explicitly handle error conditions in the chain.
You’ll note the pattern that’s being built: we create a monad using Either.from_nullable() to wrap a value and then we have a chain of map() methods that each do something to the value.
Our Either monad lets us write code without worrying if an error occurs. What’s happening is our wrapped value is being passed through the chain of map() methods, but because the values are wrapped in a container and each type of container understands the same messages (but take different actions – that’s polymorphism in action if you recall), we don’t have to worry about a does_not_understand error rearing its ugly head. That error is defeated by the container.
Onward.
Now we’ll define the two mysterious subclasses of Either.
Right and Left Subclasses
We’ll call one beeble_right and the other beeble_left. At this point I’ve veered away from convention a little, but it’s close enough. By convention the subclasses are named right and left, however those words are quite generic and I prefer not to pollute the global namespace with generic names.
beeble_right is used to map the sunny day path. If everything goes well and no errors occur, we end up with a beeble_right object wrapping our data. On the other hand, if an error occurs somewhere along the line, we get a beeble_left object wrapping a condition.
But what’s the point, you might be asking. Why go through all this trouble?
Stay with me a little longer and it shall all become clear. But first, let’s define some methods on our subclasses.
Of course we need map(), but we’ll also define get_or_else() and or_else() on both the beeble_right and beeble_left subclasses.
Each of these methods is quite basic, but there is one important thing to notice. They do different things depending on whether they’re defined on beeble_right or beeble_left.
_method beeble_right.get_or_else(p_other)
_return .value
_endmethod
_method beeble_right.or_else(_optional p_fn)
_return _self
_endmethod
_method beeble_left.get_or_else(p_other)
_return p_other
_endmethod
_method beeble_left.or_else(p_fn)
_return p_fn.invoke(.value)
_endmethod
And that’s because we want to handle the sunny day path differently from the rainy day one. For example, when we have an error, we don’t want to apply a function to the error, so map() on beeble_left simply returns _self.
However when there is no error, we do want to apply the function that will do some useful work on the wrapped data, so map() on beeble_right invokes the supplied proc on the wrapped value and wraps the result in a beeble_right object again.
The upshot is that regardless of whether we have an error or not, we can apply the map() method and it won’t crash our program. On the sunny day path (beeble_right) it will do some work but on the rainy day path (beeble_left) it will not do anything except pass along the error.
And that is the brilliance of the Either monad. We can simply write procedures (or methods, but procedures are far more flexible) without worrying about them failing on _unset or raising error conditions because the values don’t understand messages.
If an error does occur, it will raise a condition that can be handled nicely in a pipe so errors are propagated through the pipe of procedures and each procedure will simply pass it on. At the end of the pipe we can retrieve the wrapped value by using get_or_else().
Here’s an example of using get_or_else() on a chain of map methods.
Magik> e << either.from_nullable("hello world").map(zwrite).map(zshow).map(zfirst).map(zprint).get_or_else("An Error Occurred.")
write: hello world
"show: " "hello world"
first: h
hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world
print:
char16_vector(1,11):
1 %h
2 %e
3 %l
4 %l
5 %o
6 %space
7 %w
8 %o
9 %r
10 %l
11 %d
"hello world"
Magik>
Because there was no error, “hello world” was returned and the else part of get_or_else() (i.e. the supplied argument) was not returned. So that takes care of the sunny day scenario.
But what if an error occurs?
In that case we can create a safe_pipe() proc in order to handle errors as they occur and propagate them down the pipeline.
We can then define an error handling procedure at the end of the chain that will only be invoked if an error occurred (i.e. a beeble_left object shows up). Otherwise the error handler will be skipped. Here’s a sample error handling proc.
_global zerror <<
_proc @zerror(p_cond)
# log the error to a file.
write("Error: ", p_cond.report_string)
# send the error in a response to the client.
write("Returning HTTP 500 Error")
# do other stuff.
_return either.left(p_cond)
_endproc
With that out of the way, let’s move on to the pipeline procedure.
Using Pipe for Composition
If you’ve read my article on functional programming you’ll know I explained how the reduce() method works. Then if you read my piece on currying, you’ll see how I used reduce() to implement a pipe() function. In case you don’t want to click over there, here’s the procedure.
_global pipe <<
_proc @pipe(_gather p_fns)
_return _proc @reduce(p_initial_value)
_import p_fns
_local l_apply_fn << _proc @apply(x, f)
_return f(x)
_endproc
_return p_fns.reduce(l_apply_fn, p_initial_value).first
_endproc
_endproc
Notice lines 7 to 9 define a local procedure that is used by reduce(). It’s very simple in this case, but also very important — as we shall see.
pipe() allows us to execute procedures one after the other, passing the output of the previous one to the input of the next one in a loosely coupled manner. It’s the key to functional composition.
However error handling was a glaring omission and errors had to be checked for and handled in each procedure in the pipeline. If a procedure errored out and raised an unhandled condition, it would break the pipe. But with our shiny new Either monad, we can now build in automatic error handling by simply making some changes to the function reduce() applies.
Here are the changes.
_global safe_pipe <<
_proc @safe_pipe(_gather p_fns)
_if p_fns.empty?
_then
_return _unset
_endif
_local l_apply_fn << _proc @apply(x, f)
_try _with cond
write("running ", f)
_local l_val << x
_if (cn << l_val.class_name) _isnt :beeble_right _andif cn _isnt :beeble_left
_then
l_val << either.from_nullable(l_val)
_endif
_local l_result << l_val.map(f)
_if (cn << l_result.class_name) _isnt :beeble_right _andif cn _isnt :beeble_left
_then
l_result << either.from_nullable(l_result)
_endif
_return l_result
_when error
_return either.left(cond)
_endtry
_endproc
_return _proc @reduce(_optional x)
_import p_fns
_import l_apply_fn
_return p_fns.reduce(l_apply_fn, x)
_endproc
_endproc
Note there is a Try statement in this proc but the interesting thing is, it’s the only Try statement in all of the code. If we wrote a hundred more procedures for the pipeline, this would still be the only Try statement.
How is that possible?
Well, let’s take a look at our safe_pipe() in action.
Magik> e << safe_pipe(zwrite, zshow, zfirst, zprint)(2).or_else(zerror).get_or_else("An Error Occurred.")
running proc zwrite(p_value)
write: 2
running proc zshow(p_value)
"show: " 2
running proc zfirst(p_value)
running proc zprint(p_value)
Error: **** Error (does_not_understand): Object 2 does not understand message first
Returning HTTP 500 Error
"An Error Occurred."
Magik>
As you can see, when an error occurs (in line 6, because the integer 2 doesn’t understand the message first), it is propagated through the rest of the pipeline, wrapped in a beeble_left object. Because of the error, zprint is not executed (because at this point the error in zfirst caused the creation of a beeble_left object that now wraps the error condition).
The beeble_left object is then retrieved by or_else() and the error handling procedure we defined previously (zerror) is invoked to handle it. We then use get_or_else() to provide a default return value for the error (and since we have an error wrapped in a beeble_left object, the else part, i.e. “An Error Occurred,” is returned).
Using our pipeline and the Either monad frees us from having to check for errors after each step and also frees us from having to write error handling blocks in all our procedures. It also ensures errors don’t unwind the stack and bubble up unhandled.
It keeps the path of execution flowing as it should, without allowing exits outside of the standard _return, making it easier to reason about your program as well as far easier to test it — which leads to improved code quality, decreased errors and happier users (oh, and it also reduces costs – some people like that benefit the most).
Monads are another reason why I’ve moved to functional programming after more than 25 years of OO. I still use OO, but now it’s within the mindset of FP – so my programs are better. Give it a shot and see if you don’t agree.
SIDE NOTE: Okay, you caught me. I said there should be only one way into a function and one way out… but the sharp-eyed among you may have noticed the apply function, used by reduce(), invokes each function in the pipeline and if that function fails, it may raise a condition that is not handled within that function, but by the apply function. So technically there are two ways out of the invoked function. But, practically, the condition is handled immediately, by the apply function, so it doesn’t cause the issues I described. And the fact we don’t need to add error handling code to the invoked function makes it all worthwhile.
SIDE NOTE 2: Another point to consider is how we write pipelined functions. Ideally we should write them so if they call functions, those are written using pipelines and monads. However that’s usually not possible if we’re calling existing code, so you might have errors bubbling up from nested code — but at least they will be handled by the apply function rather than causing a traceback.
Creating Additional Monads
I’ll leave you with some code translated to Magik from the YouTube presentation by Douglas Crockford I mentioned earlier. See if you can understand what it’s doing and how the Maybe monad is created and behaves. You might also want to read my article on Closures, because the code uses a closure.
# monad axioms:
#
# Left Identity law: a Monad constructor is a neutral operation.
# Running it before Bind doesn't change the function call results.
#
# unit(value).bind(f) _is f(value)
#
#
# Right Identity law: given a monadic value, wrapping its contained data into another
# monad of the same type and then Binding it, doesn’t change the original value.
#
# monad.bind(unit) _is monad
#
#
# Associativity law: The order in which Bind operations are composed don't matter.
#
# monad.bind(f).bind(g) _is monad.bind(_proc (value)
# _return f(value).bind(g)
# _endproc)
# the prototype will contain the methods we want the monad to inherit.
_global monad_prototype << beeble_object.new()
# identity MONAD proc.
_global new_monad <<
_proc @new_monad(p_modifier_fn)
_return _proc @unit(p_value)
_import p_modifier_fn
_constant MONAD << beeble_object.new()
MONAD.bind << _proc @bind(p_fn, _gather args)
_import p_value
# p_value is referenced via BIND's closure.
_return p_fn.invoke(p_value, _scatter args)
_endproc
_if p_modifier_fn.class_name _is :procedure
_then
p_modifier_fn.invoke(MONAD, p_value)
_endif
_return MONAD
_endproc
_endproc
# BEEBLE_MAYBE:
# An example of how to create a MAYBE monad using the identity MONAD proc.
_global beeble_maybe <<
new_monad (_proc (p_monad, p_value)
_if p_value _is _unset
_then
p_monad.is_null << _true
p_monad.bind << _proc()
_import p_monad
_return p_monad
_endproc
_endif
_endproc
)
# Usage:
# Magik> m << beeble_maybe(_unset)
# a beeble_object
# Magik> m.prototype << monad_prototype
# --> nothing is written because beeble_maybe wraps _unset and simply returns the monad.
# Magik> m.bind(write)
# a beeble_object
# Magik> m << beeble_maybe("hey")
# a beeble_object
# Magik> m.prototype << monad_prototype
# --> "hey" is written because beeble_maybe wraps a value that isn't _unset, so the function is invoked on that value.
# Magik> m.bind(write)
# hey