What C # 7 Prepares for Us (Part 2. Pattern matching)

    Continuing a series of articles on innovations in C # 7, I focus on perhaps the main innovations - Pattern matching and Record type (An approximate translation of “registered types”). These functionalities complement each other, therefore it is better to talk about them together.

    Let's start with the Record type. He comes to us from F #. In essence, this is a quick definition of a class, and with the impossibility of changing its properties, i.e. all its fields have a readonly parameter, and parameters are set in the constructor. Describing this is quite long and tedious, so let's start right away with an example code and we’ll take a look at an example. Here is an example of a record type definition:

    public class Cartesian(double x: X, double y: Y);


    This is the definition of a class that stores the Cartesian coordinates of a point. It should be broadcast in the following class:

    
    public class Cartesian
    {
        private readonly double $X;
        private readonly double $Y;
        public Cartesian(double x, double y)
        {
            this.$X = x;
            this.$Y = y;
        }
        public double X { get { return this.$X; } }
        public double Y { get { return this.$Y; } }
        public static bool operator is(Cartesian c, out double x, out double y)
        {
            x = c.X;
            y = c.Y;
            return true;
        }
        override public bool Equals(object obj)
        {
            if (obj.GetType() != typeof(Cartesian)) return false;
            var $o = obj as Cartesian;
            return object.Equals(X, $o.X) && object.Equals(Y, $o.Y);
        }
        override public int GetHashCode()
        {
            int $v = 1203787;
            $v = ($v * 28341) + X?.GetHashCode().GetValueOrDefault();
            $v = ($v * 28341) + Y?.GetHashCode().GetValueOrDefault();
        }
        override public string ToString()
        {
            return new System.Text.StringBuilder()
                .Append(“Cartesian(X: “)
                .Append(X)
                .Append(“, Y: ”)
                .Append(Y)
                .Append(“)”)
                .ToString();
        }
    }
    

    Let's analyze the properties of the class. In his definition, we specified two double parameters. These parameters are translated into two read-only properties, and two read-only fields inside the class. Then a constructor is created with the parameters specified in the class definition. Also, Equals , GetHashCode , ToString methods are created .

    Of greatest interest is the overloaded operator is. Here he is already more related to Pattern matching. The is operator now supports additional comparisons, in addition to the usual check for castability. An additional call to this overloaded operator is also possible on the class. Let's start with how the operator is overloaded and what actions can be performed. The first parameter in the operator is the class object passed to it; it does not have to be the class of this operator. Then come the returned parameters with which we need to compare or which we need to get when executing the is statement. When creating a class via record type, the is statement is created with the class passed to the record type and the return values ​​of this class specified in the definition. Here is an example of how to convert the Cartesian coordinates to polar using the operatoris :

    
    public static class Polar
    {
        public static bool operator is(Cartesian c, out double R, out double Theta)
        {
            R = Math.Sqrt(c.X*c.X + c.Y*c.Y);
            Theta = Math.Atan2(c.Y, c.X);
            return c.X != 0 || c.Y != 0;
        }
    }
    

    What we get if we pass an object of the Cartesian class to the operator : it will try to convert the data of this class to the data of the Polar class and return the converted data.

    Pattern matching (or Pattern matching; although I don't like this name very much, the English definition seems more accurate), what is it? He came to us from languages ​​such as Python and F #. In essence, this is an extended switch that can not only compare values ​​of the same type with constants, but also use type casting and their conversion to the necessary structure. And in all this, the new overloaded operator is will help us. Let's start with the new features of the old type checking operator. Now instead of this:

    
    var v = expr as Type;   
    if (v != null) {
        // Используем v
    }
    

    You can write like this:

    
    if (expr is Type v) {
        // используем v
    }
    

    This, of course, will reduce the type cast code. But back to Pattern matching and find out what possibilities he is preparing for us. We will write a check to bring specific Cartesian coordinates to polar and get the radius:

    
    var c = Cartesian(3, 4);
    if (c is Polar(var R, *)) Console.WriteLine(R);
    

    So what's going on here, let's see. The variable c is taken , the type is obtained by the variable , and the is operator is searched for , where the first parameter is this type. Then this operator is called and, if it returns true, the condition is considered fulfilled. Next, we get the local variable R in the condition block . Here, the angle is not important for us, and therefore we passed * to the second parameter - this means ignoring the second parameter. Another use of this operator is possible:

    
    if (c is Polar(5, *)) Console.WriteLine("Радиус равен 5");
    

    Here we impose an additional condition on the return value of the radius, and the condition is satisfied only when the radius is 5.

    The main application to the new operator is is, of course, in the switch statement . Here is an example of solving algebraic expressions using pattern matching. We define the classes we need using the record type.

    
    abstract class Expr;
    class X() : Expr;
    class Const(double Value) : Expr;
    class Add(Expr Left, Expr Right) : Expr;
    class Mult(Expr Left, Expr Right) : Expr;
    class Neg(Expr Value) : Expr;
    

    To begin with, we write the derivative method:

    
    Expr Deriv(Expr e)
    {
      switch (e) {
        case X(): return Const(1);
        case Const(*): return Const(0);
        case Add(var Left, var Right):
          return Add(Deriv(Left), Deriv(Right));
        case Mult(var Left, var Right):
          return Add(Mult(Deriv(Left), Right), Mult(Left, Deriv(Right)));
        case Neg(var Value):
          return Neg(Deriv(Value));
      }
    }
    

    Or simplification of the expression:

    
    Expr Simplify(Expr e)
    {
      switch (e) {
        case Mult(Const(0), *): return Const(0);
        case Mult(*, Const(0)): return Const(0);
        case Mult(Const(1), var x): return Simplify(x);
        case Mult(var x, Const(1)): return Simplify(x);
        case Mult(Const(var l), Const(var r)): return Const(l*r);
        case Add(Const(0), var x): return Simplify(x);
        case Add(var x, Const(0)): return Simplify(x);
        case Add(Const(var l), Const(var r)): return Const(l+r);
        case Neg(Const(var k)): return Const(-k);
        default: return e;
      }
    }
    

    In the descriptions of this functional, I met mainly examples related to mathematical calculations. I will be very glad to see your examples in the comments, where this functionality will really be useful not in mathematical calculations.

    Here you can read the source

    Also popular now: