Abstraction is the bread and butter of our trade, right? The problem is that when you abstract too much, you don't always know where you came from and why.
You see? The sentence above is already very abstract ;-). Let's try to be more concrete. I want to talk about Monads and the kind of problems they solve:
- monads --> powerful abstract beast
- problems they solve --> very concrete
A classical code sample
I was trying to fix an issue with our software 2 days ago when I realized that part of my data wasn't valid: "What!? My exposure is not valid, how come!?". A quick look at the code revealed that the validity of an exposure is determined by the following (actually simplified,...):
public boolean isValid() {
return traded && !matured &&
!excluded && !internal && leId > 1;
}
Of course I started blaming the programmer: thank you smart boy, I know that my exposure is not valid but would you have the decency to explain me WHY? Is it untraded and/or matured and/or excluded,...?
I quickly realized why more precise error reporting hadn't been written: time to market. Writing big if/then/else and accumulating error messages is tedious, error-prone and boring to the extreme.
Monads to the rescue!
Somehow, that's the kind of problem that Monads can solve. One way to see Monads is to consider them as a convenient way to chain computations:
- you put something in the Monad
- you apply one or more functions to it, the results stay in the Monad ("What goes in the Matrix, stays in the Matrix" as I've read somewhere)
- then you can extract the final result
Here is the use of a Monad (coded in Scala but you could achieve the same kind of effect in Java) to solve the problem:
def isValid(e: Exposure) = {
e.traded ?~! ("is not traded") ?~! (!e.matured, "is matured") ?~!
(!e.excluded, "is excluded") ?~! (!e.internal, "is internal") ?~!
(e.leId > 1, "doesn't have a defined legal entity")
}
val exposure = new Exposure
exposure.traded = false
exposure.matured = true
isValid(exposure) match {
case f: Failure => {
println(f.messages.mkString("exposure is not valid because: it ", ", it ", ""))
}
}
It outputs: exposure is not valid because: it is not traded, it is matured, it doesn't have a defined legal entity
So, at the (small) expense of changing the operator from && to ?~! and adding a specific message at each step we get a nice failure message indicating precisely what went wrong.
How does this work?
I implemented this cryptic ?~! operator as a small variation of the existing ?~! method on the Can class from the lift webframework.
A Can is kind of box which either be Empty, Full(of something) or a Failure(reason). So when you perform computations on a Can, either:
- there is nothing to do (Empty) --> the result will stay Empty or become a Failure
- there is a value (Full(value)) --> the result will either stay Full or become a Failure
- it is a Failure --> it will stay a Failure, possibly with an additional error message
What are the 2 operations I need on the Can objects? First of all I need an operation to "put a boolean in the monad". Here I have used an implicit definition:
implicit def returnCan(b: Boolean): Can[Boolean] = if (b) Full(true) else Empty
then I need one method which will either return the same Can if some boolean is true or a new Failure accumulating one more error message:
def ?~!(b: Boolean, msg: String): Can[A] = {
if (b) this else Failure(msg :: this.messages)
}
What happens if the first boolean expression is false?
- an Empty Can is created (representing a failure with no cause yet)
- the ?~!(msg: String) method is called and returns a Failure(msg)
- when the ?~!(b: Boolean, msg: String) method is called, it either returns the same Failure, or create a new one with one more error message
And what happens if the first boolean expression is true?
- a Full Can is created (representing a success)
- the ?~!(msg: String) method is called and returns the Full can
- when the ?~!(b: Boolean, msg: String) method is called, it either returns the same Full can if b is true, or create a new Failure Can with one error message
What I will not do in this post
Although that would be interesting, I will not try to show why the behavior described above actually correspond to a Monad, with the mandatory Monad laws. The first reason is that it is too late to do so ;-). The second reason is that I'm not sure it matters much. What is certainly more important is that Monads, Arrows, Applicative Functors and their kind provide powerful models of computations which can also be applied to Everyday-Enterprisey jobs.
And, by the way, Scala provides syntactic sugar for operators definition, but there is really nothing which avoid us to do the same thing in Java:
// warning!! pseudo-java code!!
class Result {
private List messages = new ArrayList();
private Result(List msgs) {
this.messages = msgs;
}
public Result andCheck(Boolean b, String msg) {
if (b) return this;
else return new Result(messages.append(msg));
}
static public check(Boolean b, String msg) {
return new Result().andCheck(b, msg);
}
}
public Result isValid(e: Exposure) = {
check(e.traded, ("is not traded")).
andCheck(!e.matured, "is matured").
andCheck(!e.excluded, "is excluded").
andCheck(!e.internal, "is internal").
andCheck(e.leId > 1, "doesn't have a defined legal entity")
}
Eureka
I hope I haven't brought more confusion on the topic of Monads with that not-very-formal post. This is a world I'm just starting to explore and I just love when I can have some "Eureka" moments and write better code!
2 comments:
Far from causing confusion, I think this is one of the better explanations of monads I've ever read. James Iry does a nice job illustrating all of the laws in their awful splendor, but you have demonstrated the practical appeal of these beasts. Nice job!
Great post. Very pragmatic. Something like this might be useful in test frameworks??
Post a Comment