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.