Research of pattern matching implementation

Create issue
Issue #1 new
Vladimir Makarevich repo owner created an issue

There are two kinds of pattern matching in a context of conditional operators (if-else, switch):

• by type [type test pattern ?: from F#]

• by value [another syntax variations from F#]

The key difference between this two kinds that type check does not match null because impossible to get a certain type from null reference but value checks match null because it is the valid value for all reference types.

What it is mean?

Consider the model of F# pattern matching with basic features.

Classical matching by value

#!c#

        private static string MatchByValue(Point point) =>
            point.Match(
                point.X.To(out object x),
                point.Y.To(out object y)
            ).Is(out var s) &&

            s.Case(null, null) ? "X=null, Y=null" :
            s.Case(null, y) ? $"X=null, Y={y}" :
            s.Case(x, null) ? $"X={x}, Y=null" :
            s.Case(x, 777) ? $"X={x}, Y=Bingo" :
            s.Case(x, y) ? $"X={x}, Y={y}" :

            throw new ArgumentException();
Type test with a single value check for null
#!c#

        private static string MatchByTypeAndNull(IModel model) =>
            model.Match().Is(out var s) &&

            s.Case(out ModelA a) ? $"A {a}" :   // s.Case<ModelA>(out var a)
            s.Case(out ModelB _) ? "B" :        // s.Case<ModelB>()
            s.Case(out var c) ? $"C {c}" :      // s.Case<IModel>(out var c)
            s.Case(null) ? "null" :             // value check

            throw new ArgumentException();
Implementation of this model is simple

#!c#

    public static Switch<T> Match<T>(this T value, params object[] pattern) =>
        new Switch<T>(value, pattern);

    public class Switch<T>
    {
        private readonly object _value;
        private object[] _pattern;

        public Switch(T value) => _value = value;
        public Switch(T value, object[] pattern) : this(value) =>
                _pattern = pattern;

        public bool Case(params object[] pattern)
        {
            pattern = pattern ?? new[] {(object) null};
            _pattern = _pattern ?? new[] {_value};
            for (var i = 0; i < pattern.Length && i < _pattern.Length; i++)
            {
                if (Equals(pattern[i], _pattern[i])) continue;
                return false;
            }

            return true;
        }

        public bool Case<TValue>() where TValue : T => _value is TValue;
        public bool Case(out T value) => Case<T>(out value);
        //public bool Case(out T value) => Case(value = _value); // C# impl

        public bool Case<TValue>(out TValue value) where TValue : T =>
            (value = _value) is TValue;
    }

Put attention to the method

#!c#

bool Case(out T value)
At C# implementation s.Case(out var c) match null
#!c#
bool Case(out T value) => Case(value = _value); 
// call bool Case(params object[] pattern)
But IMHO s.Case(out var c) conceptually much more closely to F#
#!c#

bool Case(out T value) => Case<T>(out value);
// call bool Case<TValue>(out TValue value) where TValue : T
Why? Consider is-like extensions
#!c#

public static bool Is<T>(this T o) => typeof(T).IsValueType || o != null; // o is T
public static bool Is<T>(this T? o) where T: struct => o.HasValue; // o is T
public static bool Is<T>(this object o) => o is T; // o != null && typeof(T).IsAssignableFrom(o.GetType());

public static bool Is<T>(this T o, out T x) => o.To(out x).Is(); // is T    
public static bool Is<T>(this object o, out T x, T fallbackValue = default(T)) =>
    (x = o.Is<T>().To(out var b) ? (T) o : fallbackValue).Let(b); // is T

public static TR Let<T, TR>(this T o, TR y) => y;
We naturally can write

#!c#

GetModel().Is(out var m) // instead GetModel().Is(out IModel m)
by analogy

#!c#

s.Case(out var c) // instead s.Case(out IModel c)
Yes, possible two variation of implementation! Try to consider props and minuses of both

like F#

(+) saved symmetry

(+) saved meaning of case var and is var

(+) no ambiguously between single item deconstruction and type test

(-) no support of mixed matching

#!c#

/* value check variations */
case (var x, IModel y, 3)
case (IModel y)
case (var x)
case (3)
case 3 // reduced form of 'case (3)'

/* type check variations */
case IModel y
case var x

like C#

(-) lost of symmetry

(-) expanding meaning of case var and is var

(-) ambiguously between single item deconstruction and type test

(+) support of mixed matching

#!c#

/* mixed Matching */
case (var x, IModel y, 3) // match null for x and don't match for y

/* value check variations */
case (var x)
case var x
case (3)
case 3 // reduced form of 'case (3)'

/* type check variations */
case (IModel y)
case IModel y
As we see C# implementation have only one potential benefit with valid minuses. Is it mixed matching so important?

The previous F#-like simple case

#!c#

case (var x, IModel y, 3) where y != null
not so much difficult then C#-like
#!c#
case (var x, IModel y, 3)
Try to consider a more interesting F#-like scenario
#!c#

case (var x, null)
case (null, Control y)
case (Control x, Control y) where x is Grid && y is Button
Seems C#-like version looks better
#!c#

case (var x, null)
case (null, var y)
case (Grid x, Button y)
But why not to use something like F# type test operator :? if it so important to have mixed pattern matching and save another concepts?

#!c#

case (:? var x, null)
case (null, Control y)
case (:? Grid x, :? Button y)

case AModel m // reduced form of case (:? AModel m)
case var m // reduced form of case (:? var m)
It is an open question for me...

Another minus of C#-like implementation is missing of a fallback value

#!c#

if (GetModel().Is(out ModelA m, ModelA.Default))
    WriteLine($"Matched model {m}"});
else WriteLine($"Used default model {m}");

/* cause compile errors */
if (GetModel() is ModelA m = ModelA.Default)
    WriteLine($"Matched model {m}"});
else WriteLine($"Used default model {m}");

Comments (6)

  1. Log in to comment