Sunday, January 25, 2015

The Either type for Delphi

In a previous post I talked about the Maybe type that similar to the Nullable type allows you to handle values or the state of having no value in a functional approach.

Today I show you another type that is common in functional programming - the Either type. It allows you to return 2 different types from one function. This could be for example the content of a web request or an error message. In our example we are using similar code as in the previous post and return the result of a division or an error message.

Here is how the function would look like:

function Divide(x, y: Integer): Either<string,Integer>;
begin
  if y = 0 then
    Result := 'Division by zero!'
  else
    Result := x div y;
end;

As you already can imagine Either is a record type with implicit operator overloading making this code look nice and clean. It allows assigning either a string or an integer value. It then sets a flag that says if it's a left or a right. In cases where you use it for returning errors of the action it is common practice to use the right for the correct result and left for some exception/error message.

There are different ways to call this  - and I am afraid I will shock quite some people - one of them involves our beloved with. I know that people went on some crusade against that thing but in this code I find it very convenient.

with Divide(42, 0) do
  case Match of
    IsRight: Writeln(Right);
    IsLeft: ShowError(Left);
  end;

// or

with Divide(42, 0) do
  if Match then
    Writeln(Right)
  else
    ShowError(Left);

// or

Divide(42, 0).Fold(
  procedure(i: integer)
  begin
    Writeln(i);
  end,
  ShowError);

Either has the members Left, Right and Match which is a Boolean. IsRight and IsLeft are just aliases for True and False to make the code clearer. The Fold method takes two anonymous methods of which it calls one depending on if it's a left or right. ShowError is just a small routine I wrote taking a string and writing it to the console to make the code shorter for this example.

Wait a second - don't we have something like that in Delphi already? Yes, its called variant records and it allows something like that. Having a flag field and a dynamic part to store values depending on the flag field. Unfortunately that only works for non nullable value types which renders it pretty useless for this approach.
So finally here is the code for the Either type:

const
  IsLeft = False;
  IsRight = True;

type
  Either<TLeft,TRight> = record
  strict private
    fMatch: Boolean;
    fRight: TRight;
    fLeft: TLeft;
    function GetRight: TRight; inline;
    function GetLeft: TLeft; inline;
  public
    constructor FromLeft(const value: TLeft);
    constructor FromRight(const value: TRight);

    procedure Fold(const right: TProc<TRight>; const left: TProc<TLeft>); overload;
    function Fold<TResult>(const right: TFunc<TRight,TResult>;
      const left: TFunc<TLeft,TResult>): TResult; overload;

    property Match: Boolean read fMatch;
    property Right: TRight read GetRight;
    property Left: TLeft read GetLeft;

    class operator Implicit(const value: TRight): Either<TLeft,TRight>;
    class operator Implicit(const value: TLeft): Either<TLeft,TRight>;
  end;

constructor Either<TLeft, TRight>.FromRight(const value: TRight);
begin
  fRight := value;
  fLeft := Default(TLeft);
  fMatch := IsRight;
end;

constructor Either<TLeft, TRight>.FromLeft(const value: TLeft);
begin
  fLeft := value;
  fRight := Default(TRight);
  fMatch := IsLeft;
end;

procedure Either<TLeft, TRight>.Fold(const right: TProc<TRight>;
  const left: TProc<TLeft>);
begin
  case Match of
    IsRight: right(fRight);
    IsLeft: left(fLeft);
  end;
end;

function Either<TLeft, TRight>.Fold<TResult>(
  const right: TFunc<TRight, TResult>;
  const left: TFunc<TLeft, TResult>): TResult;
begin
  case Match of
    IsRight: Result := right(fRight);
    IsLeft: Result := left(fLeft);
  end;
end;

function Either<TLeft, TRight>.GetRight: TRight;
begin
  case fMatch of
    IsRight: Result := fRight;
    IsLeft: raise EInvalidOpException.Create('Either type has no right value.');
  end;
end;

function Either<TLeft, TRight>.GetLeft: TLeft;
begin
  case fMatch of
    IsRight: raise EInvalidOpException.Create('Either type has no left value.');
    IsLeft: Result := fLeft;
  end;
end;

class operator Either<TLeft, TRight>.Implicit(
  const value: TRight): Either<TLeft, TRight>;
begin
  Result.fRight := value;
  Result.fLeft := Default(TLeft);
  Result.fMatch := IsRight;
end;

class operator Either<TLeft, TRight>.Implicit(
  const value: TLeft): Either<TLeft, TRight>;
begin
  Result.fLeft := value;
  Result.fRight := Default(TRight);
  Result.fMatch := IsLeft;
end;

And finally here is a little teaser of something else I am working on:

Writeln(Divide(42, 3).Fold<string>(
  'Result: ' + i.ToString,
  'Error: ' + s));

No comments:

Post a Comment