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