Sunday, November 24, 2019

Tuple patterns in C# 8


Hello everyone. Today I want to show you a new feature in C# 8 called Tuple patterns. After more than two-year break, I have decided that it’s high time to resurrect my blog. So, let’s talk what is the tuple pattern and what the problem this pattern solving. Tuple patterns some algorithms depend on multiple inputs. Tuple patterns allow you to switch based on multiple values expressed as a tuple. The following code shows a switch expression for the game rock, paper, scissors (I’ve grabbed this code from What's new in C# 8.0 documentation):
public static string RockPaperScissors(string first, string second)
=> (first, second) switch
{
    ("rock", "paper") => "rock is covered by paper. Paper wins.",
    ("rock", "scissors") => "rock breaks scissors. Rock wins.",
    ("paper", "rock") => "paper covers rock. Paper wins.",
    ("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
    ("scissors", "rock") => "scissors is broken by rock. Rock wins.",
    ("scissors", "paper") => "scissors cuts paper. Scissors wins.",
    (_, _) => "tie"
};
If you see in existing code, we used tuple pattern to compare a pair of strings and return string as a result. For example, for strings “rock” and “paper” we will return a new string “rock is covered by paper. Paper wins.” So, let’s take a look how it works under the hood. (Below you will find IL code with further explanation how this code works)
.method public hidebysig static
    string RockPaperScissors (
        string first,
        string second
    ) cil managed
{
    // Method begins at RVA 0x2078
    // Code size 206 (0xce)
    .maxstack 2
    .locals init (
        [0] string,
        [1] string
    )

    IL_0000: ldarg.0
    IL_0001: brfalse IL_00c2

    IL_0006: ldarg.0
    IL_0007: ldstr "rock"
    IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0011: brtrue.s IL_0032

    IL_0013: ldarg.0
    IL_0014: ldstr "paper"
    IL_0019: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_001e: brtrue.s IL_0054

    IL_0020: ldarg.0
    IL_0021: ldstr "scissors"
    IL_0026: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_002b: brtrue.s IL_0073

    IL_002d: br IL_00c2

    IL_0032: ldarg.1
    IL_0033: brfalse IL_00c2

    IL_0038: ldarg.1
    IL_0039: ldstr "paper"
    IL_003e: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0043: brtrue.s IL_0092

    IL_0045: ldarg.1
    IL_0046: ldstr "scissors"
    IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0050: brtrue.s IL_009a

    IL_0052: br.s IL_00c2

    IL_0054: ldarg.1
    IL_0055: brfalse.s IL_00c2

    IL_0057: ldarg.1
    IL_0058: ldstr "rock"
    IL_005d: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0062: brtrue.s IL_00a2

    IL_0064: ldarg.1
    IL_0065: ldstr "scissors"
    IL_006a: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_006f: brtrue.s IL_00aa

    IL_0071: br.s IL_00c2

    IL_0073: ldarg.1
    IL_0074: brfalse.s IL_00c2

    IL_0076: ldarg.1
    IL_0077: ldstr "rock"
    IL_007c: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0081: brtrue.s IL_00b2

    IL_0083: ldarg.1
    IL_0084: ldstr "paper"
    IL_0089: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_008e: brtrue.s IL_00ba

    IL_0090: br.s IL_00c2

    IL_0092: ldstr "rock is covered by paper. Paper wins."
    IL_0097: stloc.0
    IL_0098: br.s IL_00ca

    IL_009a: ldstr "rock breaks scissors. Rock wins."
    IL_009f: stloc.0
    IL_00a0: br.s IL_00ca

    IL_00a2: ldstr "paper covers rock. Paper wins."
    IL_00a7: stloc.0
    IL_00a8: br.s IL_00ca

    IL_00aa: ldstr "paper is cut by scissors. Scissors wins."
    IL_00af: stloc.0
    IL_00b0: br.s IL_00ca

    IL_00b2: ldstr "scissors is broken by rock. Rock wins."
    IL_00b7: stloc.0
    IL_00b8: br.s IL_00ca

    IL_00ba: ldstr "scissors cuts paper. Scissors wins."
    IL_00bf: stloc.0
    IL_00c0: br.s IL_00ca

    IL_00c2: ldstr "tie"
    IL_00c7: stloc.0
    IL_00c8: br.s IL_00ca

    IL_00ca: ldloc.0
    IL_00cb: stloc.1
    IL_00cc: ldloc.1
    IL_00cd: ret
} // end of method Program::RockPaperScissors

On the surface, there is a lot of IL code here which is hard to understand. But we can take a look more deeply at this code and split this code to the simple logical blocks.
IL_0000: ldarg.0
    IL_0001: brfalse IL_00c2

    IL_0006: ldarg.0
    IL_0007: ldstr "rock"
    IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0011: brtrue.s IL_0032

    IL_0013: ldarg.0
    IL_0014: ldstr "paper"
    IL_0019: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_001e: brtrue.s IL_0054

    IL_0020: ldarg.0
    IL_0021: ldstr "scissors"
    IL_0026: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_002b: brtrue.s IL_0073

    IL_002d: br IL_00c2

The first line of code loads the argument at index 0 into the evaluation stack. The next line allows to transfer control to a target instruction if value is false, a null reference or zero. If this check instruction returns false for us, then compiler navigates to IL_00c2. Below you will find how this label looks like.
    IL_00c2: ldstr "tie"
    IL_00c7: stloc.0
    IL_00c8: br.s IL_00ca
From development perspective this IL instruction works like goto statement.
public static string RockPaperScissors(string first, string second)
{
    if (first == null)
        goto NotFound;

NotFound:
    return "tie";
}
If the first instruction does not return null, the compiler loads the arguments at index 0. This instruction will check the first argument “rock” using equality operator
    IL_0006: ldarg.0
    IL_0007: ldstr "rock"
    IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0011: brtrue.s IL_0032
If the first argument (in our case the parameter first) called “rock” compiler creates an instruction to navigate to label IL_0032. I will post bellow the information about IL_0032 label.
    IL_0032: ldarg.1
    IL_0033: brfalse IL_00c2

    IL_0038: ldarg.1
    IL_0039: ldstr "paper"
    IL_003e: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0043: brtrue.s IL_0092

    IL_0045: ldarg.1
    IL_0046: ldstr "scissors"
    IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0050: brtrue.s IL_009a

    IL_0052: br.s IL_00c2
Here at the first line the compiler downloads the second argument from stack and checks this argument to null value. If this value is equal to null, we will jump to IL_00c2 label and return “tie” result. In example below I will try to show how this result will look in C# code.
public static string RockPaperScissors(string first, string second)
{
    if (first == null)
        goto NotFound;

    if(first.Equals("rock"))
        goto IL_0032;
   
IL_0032:

    if (second == null)
        goto NotFound;

NotFound:
    return "tie";
}
In the second operation, we are checking equality to value “paper”. If the second argument is equal to “paper”, the compiler creates an instruction to navigate to IL_0092 label and returns string “rock is covered by paper. Paper wins.”
IL_0092: ldstr "rock is covered by paper. Paper wins."
    IL_0097: stloc.0
    IL_0098: br.s IL_00ca
If the previous check returns false, the compiler checks the second instruction from stack and compares this argument with “scissors”
    IL_0045: ldarg.1
    IL_0046: ldstr "scissors"
    IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
    IL_0050: brtrue.s IL_009a
If this checking result returnes true, we will jump to label IL_009a and return value “rock breaks scissors. Rock wins.”
IL_009a: ldstr "rock breaks scissors. Rock wins."
    IL_009f: stloc.0
    IL_00a0: br.s IL_00ca
Otherwise, the next call IL_0052: br.s IL_00c2 will jump us to exit and return “tie” from method. Ok, I will propose to see how this code will be mapped to C#.
public static string RockPaperScissors(string first, string second)
{
    if (first == null)
        goto NotFound;

    if(first.Equals("rock"))
        goto IL_0032;
   
IL_0032:

    if (second == null)
        goto NotFound;

    if (second.Equals("paper"))
        goto IL_0092;

    if (second.Equals("scissors"))
        goto IL_009a;

IL_0092:
    return "rock is covered by paper. Paper wins.";
IL_009a:
    return "rock breaks scissors. Rock wins.";
NotFound:
    return "tie";
}
You see, that is something like a jump table or simple state machine. Other checking for “rock” and “scissors” just adds additional labels for jumping. I would suggest taking a look at the C# projection for our IL code with updating label names to a readable format.
public static string RockPaperScissors(string first, string second)
{
    if (first == null)
        goto NotFound;

    if(first.Equals("rock"))
        goto ROCK;

    if (first.Equals("paper"))
        goto PAPER;

    if (first.Equals("scissors"))
        goto SCISSORS;

ROCK:

    if (second == null)
        goto NotFound;

    if (second.Equals("paper"))
        goto rock_is_covered_by_paper;

    if (second.Equals("scissors"))
        goto rock_breaks_scissors;

PAPER:

    if (second == null)
        goto NotFound;

    if (second.Equals("rock"))
        goto paper_covers_rock;

    if (second.Equals("scissors"))
        goto paper_is_cut_by_scissors;

SCISSORS:

    if (second == null)
        goto NotFound;

    if (second.Equals("rock"))
        goto scissors_is_broken_by_rock;

    if (second.Equals("paper"))
        goto scissors_cuts_paper;

rock_is_covered_by_paper:
    return "rock is covered by paper. Paper wins.";
rock_breaks_scissors:
    return "rock breaks scissors. Rock wins.";
paper_covers_rock:
    return "paper covers rock. Paper wins.";
paper_is_cut_by_scissors:
    return "paper is cut by scissors. Scissors wins.";
scissors_is_broken_by_rock:
    return "scissors is broken by rock. Rock wins.";
scissors_cuts_paper:
    return "scissors cuts paper. Scissors wins.";
NotFound:
    return "tie";
}

When we look more precisely at this C #code, we can split this code into four states (state machine). 
In state 0 we have the basic logic for checking the first argument. The State number one shows how state machine works when we check “rock” parameter.
The state number two does the same for “paper” validation
And the last state 3, where we will check “scissors” validation.
So, as you see, as much parameters you will add, as more complex state machine will be created. I find this feature very useful and it will help to develop a new functionality in less time using C# language.

No comments:

Post a Comment