Home > Back-end >  C# - Dynamic pattern matching cases in switch expression?
C# - Dynamic pattern matching cases in switch expression?

Time:03-10

I've recently been finding a ton of value in simplifying branching logic into switch expressions and utilizing pattern matching. Particulary in the case where I have multiple data elements that need to be considered.

return tuple switch
{
   ("Foo", "Bar") => "FooBar",
   ("Foo", _ ) => "Foo",
   (_, "Bar") => "Bar",
   (_, _) => "Default Value"
};

My expected behaviour for any given value of "tuple" variable would yield as follows:

var tuple = ("Hello", "World") -> "Default Value"
var tuple = ("Foo", "World") -> "Foo"
var tuple = ("Foo", "") -> "Foo"
var tuple = ("Foo", null) -> "Foo"
var tuple = ("Hello", "Bar") -> "Bar"

This all works well and good.

I've recently identified a situation, where what I want is a sequence of rules that have to be checked in a "most specific to least specific" order, with reasonable defaults if a value does not exist. So effectively the same as the pattern matching sequence above. However, I need my end users to have the capability of configuring the patterns themselves, and the pattern cases to be dynamic (i.e coming from a database table).

So, given these data records:

Input1, Input2, ReturnValue
"Foo",  "Bar",  "FooBar"
"Foo",  NULL,   "Foo"
NULL,   "Bar",  "Bar"
NULL,   NULL,   "Default Value"

I'd want to "generate" these cases, exactly like my hardcoded example above.

   ("Foo", "Bar") => "FooBar",
   ("Foo", _ ) => "Foo",
   (_, "Bar") => "Bar",
   (_, _) => "Default Value"

Then if a user wanted to add a new "rule", they'd add a new record

Input1, Input2,  ReturnValue
"Hello","World", "I'm the super special return value".

which would create the following pattern case:

("Hello", "World") => "I'm the super special return value",

and the corresponding results when evaluating would be:

var tuple = ("Hello", "World") -> "I'm the super special return value"
var tuple = ("Hello", "Other") -> "Default Value"

In my mind, I would want to do something to the effect of:

var switchOptions = dataRecords.Select(record =>
{
   var pattern = (record.Input1 ?? '_', record.Input2 ?? '_');
   var func = (pattern) => record.Result;
   return func;
});
//and then somehow build the switch expression out of these options.

It makes sense why this doesn't work for a few reasons, I'm sure not limited to:

  1. the switch expression syntax isn't an object that has AddPattern().
  2. The char '_' and the _ operator in the expression aren't the same thing...

The other option I thought of is to map the record set into a dictionary, where the Key would be the Tuple (columns: Input1, Input2), and the value is the expected return value (column: ReturnValue). The problem with this is, it doesn't provide me any capacity to treat NULL database value as a discard pattern, by a simple key lookup.

At the end of the day, my question is this: I assume the switch expression syntax is just some nice sugar overtop of a more complicated implementation under the hood. Is the idea of a "dynamically" switch expression something I can accomplish with an already existing implementation within C# 9? Or am I barking up the wrong tree, and need to fully implement this on my own?

CodePudding user response:

I'm not sure if you're looking for some kind of code-generation, but with a bit of Linq aggregation you can put your patterns/records in a sequence and use the result as a function that kinda acts like the switch-expression pattern matching. It's important that dataRecords contains the records in the order you want them to be evaluated:

public record Record(string Input1, string Input2, string ReturnValue);
public record Pattern(Func<string, string, bool> Predicate, string ReturnValue);

Pattern CreatePattern(Record rec)
{
    return new (
        (l, r) =>
            (l == rec.Input1 || rec.Input1 == null)
            && (r == rec.Input2 || rec.Input2 == null),
        rec.ReturnValue
    );
}

// Create your pattern matching "switch-expression"
var switchExp = dataRecords
    .Reverse()
    .Select(CreatePattern)
    .Aggregate<Pattern, Func<string, string, string>>(
        (_, _) => null,
        (next, pattern) =>
            (l, r) => pattern.Predicate(l, r) ? pattern.ReturnValue : next(l, r)
    );

switchExp("abc", "bar"); // "bar"

See it in action.

CodePudding user response:

You'll have to implement this on your own. The switch pattern matching is similar to the switch case in regular use, requiring compile time constants, and is likely implemented with a jump table. Therefore it cannot be modified in runtime.

What you are trying to achieve feels like shouldn't be too hard. Something like this

PatternDict = new Dictionary<string, Dictionary<string, string>>();
PatternDict["_"] = new Dictionary<string, string>();
PatternDict["_"]["_"] = null;

With the update code be:

Dictionary<string, string> dict;
if (!PatternDict.TryGetValue(input1, out dict)) {
  dict = new Dictionary<string, string>();
  dict["_"] = "default";
}
dict[input2] = returnValue;
PatternDict[input1] = dict;

And retrieval code be:

Dictionary<string, string> dict;
if (!PatternDict.TryGetValue(input1, out dict)) {
  dict = PatternDict["_"];
}
string returnVal;
if (!dict.TryGetValue(input2, out returnVal)) {
  returnVal = dict["_"];
}
return returnVal;

You might also be able to change string to nullable strings string? if you are using new version of C# that supports it to use null as your default value key.

  • Related