• Skip to primary navigation
  • Skip to main content

MarkHing.com

Carefully Crafted Content

  • Home
  • Smallworld GIS
  • Software
  • Investing
  • Random Musings
  • Blog
  • About
    • Privacy Policy
    • Terms of Service
    • GNU General Public License
  • Contact Me
  • Show Search
Hide Search

Using Monads for Better Error Handling in Smallworld GIS Magik

Mark Hing · May 20, 2021 ·

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


Smallworld

About Mark

Mark Hing, a Value Investing and Smallworld GIS specialist, created the Automatic Investor software and is the author of "The Pragmatic Investor" book.

(Buy the book now on Amazon.ca)
(Buy the book now on Amazon.com)

As President of Aptus Communications Inc., he builds cutting-edge FinTech applications for individual investors. He has also used his software expertise to architect, develop and implement solutions for IBM, GE, B.C. Hydro, Fortis BC, ComEd and many others.

When not taking photographs or dabbling in music and woodworking, he can be found on the ice playing hockey -- the ultimate sport of sports.

linkedin   Connect with Mark

All views expressed on this site are my own and not those of my employer.

Copyright © 2023 Aptus Communications Inc. All Rights Reserved.