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.