I admit - such headlines are getting old - at least for those that know a bit about functional programming. But for those of you not familiar with the term
monad it might be new. But don't be scared though by all that functional programming gibberish in that Wikipedia article.
Today after
Nick's heretical post about avoiding nil we had quite some discussion in the Spring4D team. Because given you must not use nil - how do you deal with the state of
none in your code? The business logic might define that a valid result is
zero or
one item. This often is represented as
nil or an
assigned instance. But then all your code will have to do nil checks whenever you want to perform operations on that item.
So after some research and reading several articles I found
this article and I smacked my head because I did not see that obvious solution. Of course having 0 or 1 element is a special case of a collection. So what would be better suited for that than an enumerable?
I looked around a bit more about and found some more articles with
example code making use of that idea.
In fact implementing it in a very similar way in Delphi is not that hard.
program MaybeMonad;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
Maybe<T> = record
strict private
fValue: T;
fHasValue: string;
type
TEnumerator = record
private
fValue: T;
fHasValue: string;
public
function MoveNext: Boolean;
property Current: T read fValue;
end;
public
constructor Create(const value: T);
function GetEnumerator: TEnumerator;
function Any: Boolean; inline;
function GetValueOrDefault(const default: T): T;
class operator Implicit(const value: T): Maybe<T>;
end;
constructor Maybe<T>.Create(const value: T);
begin
case GetTypeKind(T) of
tkClass, tkInterface, tkClassRef, tkPointer, tkProcedure:
if (PPointer(@value)^ = nil) then
Exit;
end;
fValue := value;
fHasValue := '@';
end;
function Maybe<T>.Any: Boolean;
begin
Result := fHasValue <> '';
end;
function Maybe<T>.GetValueOrDefault(const default: T): T;
begin
if Any then
Exit(fValue);
Result := default;
end;
function Maybe<T>.GetEnumerator: TEnumerator;
begin
Result.fHasValue := fHasValue;
Result.fValue := fValue;
end;
class operator Maybe<T>.Implicit(const value: T): Maybe<T>;
begin
Result := Maybe<T>.Create(value);
end;
function Maybe<T>.TEnumerator.MoveNext: Boolean;
begin
Result := fHasValue <> '';
if Result then
fHasValue := '';
end;
function Divide(const x, y: Integer): Maybe<Integer>;
begin
if y <> 0 then
Result := x div y;
end;
function DoSomeDivision(denominator: Integer): Maybe<Integer>;
var
a, b: Integer;
begin
for a in Divide(42, denominator) do
for b in Divide(a, 2) do
Result := b;
end;
var
a: string;
b: Integer;
c: TDateTime;
result: Maybe<string>;
begin
try
for a in TArray<string>.Create('Hello World!') do
for b in DoSomeDivision(0) do
for c in TArray<TDateTime>.Create(EncodeDate(2010, 1, 14)) do
result := a + ' ' + IntToStr(b) + ' ' + DateTimeToStr(c);
Writeln(result.GetValueOrDefault('Nothing'));
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
Now there are a few things in that code I should explain. The record is pretty straight forward. It holds a value and a flag if its empty or not depending on what gets passed to the constructor. For XE7 we can use the
new intrinsic function GetTypeKind that makes it possible for the compiler to remove the case code path in this particular code because we have a type kind of tkInteger in our example. But if you had an object or interface this code would run and check for nil.
The class operator makes assigning to a Maybe<T> possible. That's why we could write Result := x div y in the Divide function.
To enumerate our value we just need to implement the GetEnumerator method that returns an instance with the MoveNext method and a Current property.
Now for the fun part. "You are missing an assignment in the else part in Divide!" you might say. Well, that is OK because the field in Maybe<T> marking if we have a value is a string which is a managed type and thus gets initialized by the compiler generated code - you might know that trick already from the Spring.Nullable<T> (which is in fact very similar to the Maybe<T>). In case of y being 0 our result will contain an empty string in the fHasValue field - exactly what we want (please don't argue that a division by zero should raise an exception and not return nothing - I did not invent that example - I was just too lazy to come up with my own). ;)
DoSomeDivision and the 3 nested loops in the main now might look weird at first but if we keep in mind that a Maybe<T> is an enumerable that contains zero or one item it should be clear that these loops won't continue if we have an empty Maybe<T>. And that's the entire trick here. Not checking if there is an item or not. Just perform the operation on a data structure that fits our needs. In this case one that can deal with the state of having or not having an item.
Of course we could avoid all that Mumbo jumbo and use a dynamic array directly that contains no or one item. But even then our code would still contain any kind of checks (and we could not make sure there are not more than one element in that array). With using our Maybe<T> type we can easily use GetValueOrDefault or Any to perform the check if we have an item or not at the very end of our processing when we evaluate the result but not in the middle of the processing.
Of course if you are into functional programming you might argue that this is not what makes a monad and that is true but for this particular use case of dealing with zero or one item it does the job very well. Probably more about functional programming approaches in Delphi or other interesting things in the next post.
Edit: Here is another example which deals with objects:
type
Maybe = record
class function Just<T>(const value: T): Maybe<T>; static;
end;
class function Maybe.Just<T>(const value: T): Maybe<T>;
begin
Result := Maybe<T>.Create(value);
end;
var
window: TForm;
control: TControl;
activeControlName: Maybe<string>;
begin
for window in Maybe.Just(screen.ActiveForm) do
for control in Maybe.Just(window.ActiveControl) do
activeControlName := control.Name;
activeControlName.ForAny(ShowMessage);
end;
Same effect here: the loop will not execute if the Maybe returned by the Just call contains nil.