Wednesday, January 21, 2015

Never return nil? Maybe!

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.

No comments:

Post a Comment