Skip to content

PatternMatchingEither

David Arno edited this page Feb 17, 2020 · 1 revision

Pattern Matching

Succinc<T> pattern matching guide: Either<TLeft, TRight>


Introduction

This pattern matching guide is split into the following sections:

A either type (eithers) can contain either a "left value" or a "right value". Pattern matching on eithers therefore consists of matching value of those various types, via Left() or Right().

Syntax

The generalised syntax for eithers patterns can be expressed using BNF-like syntax. As with all Succinc<T> pattern matching cases, there are two types of match. Firstly, matching and returning a value:

result = {either}.Match<{result type}>()
    [LeftExpression|RightExpression] ...
    [ElseExpression]
    .Result();

LeftExpression ==>
    .Left()[OfExpression|WhereExpression].Do({value} => {result type expression}) |
    .Left()[OfExpression|WhereExpression].Do({result type value})

RightExpression ==>
    .Right()[OfExpression|WhereExpression].Do({value} => {result type expression}) |
    .Right()[OfExpression|WhereExpression].Do({result type value})

OfExpression ==>
    .Of({value})[.Or({value})]...

WhereExpression ==>
    .Where({item} => {boolean expression})

ElseExpression ==>
    .Else({either} => {result type expression}) |
    .Else({result type value})

And the alternative is a match that invokes a statement (ie, an Action<{item type}>):

{either}.Match()
    [LeftExpression|RightExpression] ...
    [ElseExpression]
    .Exec();

LeftExpression ==>
    .Left()[OfExpression|WhereExpression].Do({value} => {action on value})

RightExpression ==>
    .Right()[OfExpression|WhereExpression].Do({value} => {action on value})

OfExpression ==>
    .Of({value})[.Or({value})]...

WhereExpression ==>
    .Where({item} => {boolean expression})

ElseExpression ==>
    .Else({either} => {action on either}) |
    .IgnoreElse()

To explain the above syntax:

  • {} denotes a non-literal, eg {void expression} could be the empty expression, {}, or something like Console.WriteLine("hello").
  • Items in [] are optional.
  • | is or, ie [x|y] reads as "an optional x or y".
  • ... after [x] means 0 or more occurrences of x.
  • ==> is a sub-rule, which defines the expression on the left of this symbol.

Left/Right

For Either<int, string>,

  • Left() matches an either containing an int value,
  • Right() matches an either containing a string value.

Basic Usage

The most basic form is matching on which type an either contains:

public static bool ContainsRectangle(Either<Rectangle, Circle> shape) 
    => shape.Match<bool>()
        .Left().Do(x => true)
        .Right().Do(x => false)
        .Result();

public static void PrintShape(Either<Rectangle, Circle> shape) 
    => shape.Match()
        .Left().Do(Console.WriteLine("Rectangle"))
        .Right().Do(Console.WriteLine("Circle"))
        .Exec();

In ContainsRectangle, we test against Left() (rectangle) and Right() (circle) to return true/false accordingly. In PrintShape, we test against Left() and Right() once more, and invoke an action to print the shape type that corresponds to the union's state.

In both cases, we have used both Left() and Right(), but we could optionally use Else():

public static bool ContainsRectangle(Either<Rectangle, Circle> shape) 
    => shape.Match<bool>()
        .Left().Do(x => true)
        .Else(x => false)
        .Result();

public static void PrintShape(Either<Rectangle, Circle> shape) 
    => shape.Match()
        .Left().Do(Console.WriteLine("Rectangle"))
        .Else(Console.WriteLine("Circle"))
        .Exec();

Else() or IgnoreElse() is invoked if there is no match from any specified Left() or Right() expressions respectively.

One further change can be made to the functional example. We are supplying a parameter, x, which isn't then used. In this case, we can dispense with the lambda and just specify the return value:

public static bool ContainsRectangle(Either<Rectangle, Circle> shape) 
    => shape.Match<bool>()
        .Left().Do(true)
        .Else(false)
        .Result();

Matching Individual Values

The previous examples just matched each case of the union with any value. We might want to match specific values though. We can use this feature as part of a simple calculator:

public static ExpressionNode CreateExpressionNode(Either<string, float> token)
    => token.Match<ExpressionNode>()
        .Left().Of("+").Do(new ExpressionNode(x, y => x + y))
        .Left().Of("-").Do(new ExpressionNode(x, y => x - y))
        .Left().Of("/").Do(new ExpressionNode(x, y => x / y))
        .Left().Of("*").Do(new ExpressionNode(x, y => x * y))
        .Else(x => new ExpressionNode(x))
        .Result();

CreateExpressionNode will create an instance of the type, ExpressionNode, that takes either a Func<float, float, float> or float parameter. For the former, it constructs a lambda function to perform the appropriate calculation. For the latter, it just stores the number supplied.

It's often the case that more than one value needs to match a particular pattern. We have two choices here: we can use Or() or Where().

Firstly, using Or we could write a more advanced CreateExpressionNode method:

public static ExpressionNode CreateExpressionNode(Union<string, float> token) 
  => token.Match<ExpressionNode>()
        .Left().Of("+").Or("-".Or("*").Or("/").Do(ArithmaticExpression)
        .Left().Of("(").Do(new ExpressionNode(SpecialAction.StartGroup))
        .Left().Of(")").Do(new ExpressionNode(SpecialAction.EndGroup))
        .Else(x => new ExpressionNode(x))
        .Result();
}

Here we now match +, -, / and * together and invoke a method ArithmaticExpression that returns one of the four previously described lambdas. ExpressionNode now accepts an enum SpecialAction too, which is used to denote the start and end of a grouping (via ()).

If we want to check a range of values, we can use Where:

public static void PositiveOrNegative(Union<string, int> token)
    => data.Match()
        .Right().Where(i => i < 0).Do(_ => Console.WriteLine("Negative"))
        .Right().Where(i => i > 0).Do(_ => Console.WriteLine("Positive"))
        .Right().Do(_ => Console.WriteLine("Zero"))
        .Else("Not a number")
        .Exec();

Match Order

So far, we have only considered distinct match patterns, ie where there is no overlap. In many cases, more than one Left() or Right() pattern will be required and the match patterns may overlap. The following function highlights this:

public static string OddOrNegative(Union<string, int> token)
    => data.Match<string>()
        .Right().Where(i => i % 2 == 1).Do(_ => Console.WriteLine("Odd"))
        .Right().Where(i => i < 0).Do(_ => Console.WriteLine("Negative"))
        .Else("Neither")
        .Result();

Clearly in this situation, all negative odd integers will match both Where clauses. The matching mechanism tries each match in the order specified and stops on the first match. So OddOrPositive(new Union<string, int>(-1)) will return Odd, rather than Negative.

Clone this wiki locally