Wednesday, May 13, 2009

Monads pt. 2

In my previous post I derived the Maybe monad through a sequence of refactorings. In this post I will delve into another practical example that builds on the previous code we wrote. Refresh yourself here.


At the conclusion of my previous post I mentioned a couple impurities in our implementation:

1) The Bind function named _IfSuccessful_ resides in the Maybe class when in fact it should be separate from the monad wrapper.

2) We are missing the Unit function which elevates the base type to the monadic type i.e. from the string "john" to the Maybe object _maybejohn_.


I will define the Bind and Unit functions as extension methods:

    8 public static class MaybeExtensions

    9 {

   10     public static Maybe<Out> IfSuccessful<In, Out>(this Maybe<In> instance, Func<Maybe<In>, Maybe<Out>> f)

   11     {

   12         if (instance != null)

   13         {

   14             return f(instance);

   15         }

   16         return null;

   17     }

   18 

   19 

   20 

   21     public static Maybe<T> Unit<T>(this T s)

   22     {

   23         if (s != null)

   24         {

   25             var maybe = new Maybe<T>();

   26             maybe.Value = s;

   27             return maybe;

   28         }

   29         return null;

   30     }

   31 }



A couple things of note:

the _IfSuccessful_ method from my previous post took in a Func<Maybe<T>, Maybe<T>> as an arg. The new version now takes in two args, the first is by virtue of an extension method is the caller of the extension method. The second arg has been modified from original Func<Maybe<T>, Maybe<T>> to be Func<Maybe<In>, Maybe<Out>>. This implies the function has the ability to output a Maybe object wrapping a different underlying type from the input Maybe object. Basically, the original implementation was contrived and overly simplified; time for the training wheels to come off. Another change to the _IfSuccessful_ method is foregoing the _IsNothing_ boolean and simply testing for _null_ instead. This doesn't change the overall behavior; _null_ is just a simpler, more generic thing to check for than a boolean flag.

The Unit() extension method takes the caller and promotes it to its Maybe type (if the caller is not _null_).

And here's the new Maybe class after cleaning it out:

    8     public class Maybe<T>

    9     {

   10         public T Value; 

   11     }




We are ready to tackle our imaginary business problem:

A large company has a policy that provides education grants for employee's dependents. The catch is that the employee is qualified to receive this money if the dependent's report card shows an 'A' average. This business rule can be explicitly written as:

  118             Func<Employee, ReportCard, Employee> TestAndApplyEducationGrant =

  119                 (employee, dependentReportCard) =>

  120                 {

  121                     //if the dependent's grade is an A

  122                     if (dependentReportCard.Grade == "a")

  123                     {

  124                         //give employee the grant

  125                         employee.Grant = new Grant();

  126                         return employee;

  127                     }

  128                     else

  129                     {

  130                         //not qualified for the grant

  131                         return null;

  132                     }                   

  133                 };



Here's the supporting story:

  137         var Joe = new Company().FindEmployee("joe");

  138         var IsSuccessful = false;

  139 

  140         if (Joe != null)

  141         {

  142             var joeDependent = Joe.GetDependent();

  143 

  144             if (joeDependent != null)

  145             {

  146                 var dependentSchool = joeDependent.GetSchool();

  147 

  148                 if (dependentSchool != null)

  149                 {

  150                     var reportCard = dependentSchool.FindReportCard(joeDependent.Name);

  151 

  152                     //here is the business rule

  153                     var result = TestAndApplyEducationGrant(Joe, reportCard);

  154 

  155                     if (result != null)

  156                     {

  157                         //save the result somewhere and inform us of failure

  158                         IsSuccessful = new DB().Persist(result);

  159                     }

  160                 }

  161             }

  162         }



The nested _if_ statements are necessary to traverse through the object graph, checking for a blocking null.

Here is the code using the monad:

  169 var result =

  170     maybeJoe.IfSuccessful(employee => employee.Value.GetDependent().Unit())

  171                      .IfSuccessful(dependent => dependent.Value.GetSchool().Unit())

  172                      .IfSuccessful(school => school.Value.FindReportCard(maybeJoe.Value.GetDependent().Name).Unit())

  173                      .IfSuccessful(reportCard => TestAndApplyEducationGrant(maybeJoe.Value, reportCard.Value).Unit())

  174                      .IfSuccessful(employee => new DB().Persist(employee).Unit());



Notice the call to Unit() at the tail of each method call. Remember Unit() wraps the raw object back into the Maybe type so that it can be passed on to the next Bind function. Taking a step back to take a look at the larger picture, we can see that the .Value method accesses the base type - or "unboxes" it, work is then performed and the result is "boxed" up again by the Unit() call and passed on to the next step.

We have to be mindful of designing the object methods such that it returns _null_ if it fails to retrieve/perform its work. That way, _result_ will be non-null if the whole workflow succeeds, null otherwise.

The monad shortens the code considerably, improving its fluency as well.

No comments:

Post a Comment