We looked at the Failure monad in an earlier article, It is worth reading that first if you haven't already, because this article builds on it.
The Failure monad allows us to deal with functions that might fail when they are called. We wrap values in a monad that contains a flag that indicates if the value is invalid because a function threw an exception while trying to do a calculation. The monad catches exceptions, marks the value as failed, and then doesn't call any other functions that use the value, it just marks the result as failed. The failure propagates back to the original caller, which means exceptions can be handled within the normal code path.
In this article we will look at a similar case - optional values - but take a slightly different approach, using two monads.
It is not uncommon for programs to encounter values that may or may not be present at runtime. For example, your program might look up a value in a set of user preferences, or try to read a record from a database, and find out that in fact the value simply isn't there.
A traditional way to handle this situation would be to set the value of the variable to
None if it can't be found. That kind of works but it has problems:
- Your code base will be littered with
if not xtype statements to take special action if the values isn't present.
Nonedoesn't specifically tell you that the value isn't present. Sometimes it is used because an error occurred in a calculation, or for other reasons.
Noneis a valid option for a field, and doesn't indicate anything special such as a not present state.
This is quite similar to the Failure monad case. The main difference is that a function can potentially fail (as in throw an exception) at anytime, whereas if an optional value is unavailable we will know this right from the start. So we will handle it slightly differently. We will use a Maybe monad that has two separate sub-classes - Just and Nothing.
The Maybe monad
The Maybe monad often exists as an abstract class, with two concrete sub-classes, Just and Nothing. So when we talk about a Maybe object, what we actually mean is an object that could be either Just or Nothing.
In Python, because we can use duck typing, we don't necessarily have to define a Maybe class at all. So long as Just and Nothing implement all the required methods, they don't really need to be related by an actual base class. We will take that approach here.
The Just monad
The Just monad is quite similar to the Failure monad, in that it wraps a value, binds functions to that value. But that is all it needs to do. If an object has a value we will assume that any function will also return a value.
class Just(): def __init__(self, value): self.value = value def get(self): return self.value def bind(self, f): result = f(self.value) return Just(result) def __str__(self): return 'Just(' + str(self.value) + ')' def __or__(self, f): return self.bind(f)
The Nothing monad
The Nothing monad represents no value. When we apply a function to Nothing (using
bind), we don't actually call the function, because there is no value to pass to it. The result is always Nothing
class Nothing(): def __init__(self): pass def get(self): return None def bind(self, f): return Nothing() def __str__(self): return 'Nothing()' def __or__(self, f): return self.bind(f)
Here is the result of binding the
neg function to a value
Just(3) and ot no value
value = Just(3) result = value.bind(neg) print(result) # Just(-3) no_value = Nothing() result = no_value.bind(neg) print(result) # Nothing()