Reputation: 13
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 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?
Upvotes: 1
Views: 2013
Reputation: 676
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.
Upvotes: 1
Reputation: 5261
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.
Upvotes: 2