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:
- the switch expression syntax isn't an object that has AddPattern().
- 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.