C# query expressions are usually used for slicing and dicing enumerables or running database queries with entity framework. However, query expressions can be used with any object that has a Select
method. Even though C# is a statically typed language, some of its built in forms like this resemble duck typing. Taking advantage of this feature, I figured out a way to make a function builder entirely using query expressions.
Warning: This is a toy example. Anyone who uses this in production is a bad person.
A very simplistic definition of function is something that takes an argument of particular type and applies transformations until you get an output value.
Our function therefore starts with an argument represented as an identity function:
static Func<TArg, TArg> Arg<TArg>() => arg => arg;
Our transformations are passed to a Select
function, implemented using an extension method on the ‘Func’ class.
static Func<TArg, TResult> Select<TArg, TVal, TResult>( this Func<TArg, TVal> func1, Func<TVal, TResult> func2) => arg => func2(func1(arg));
These simple constructs allow us to go surprisingly far. The classic use case would look like this:
var command = Arg<int>() .Select(i => i + 1) .Select(i => i + 2); command(10); // => 13
But now, Func's
can be used in query expressions:
var command = from i in Arg<int>() select i + 3; command(10); // => 13
This expression uses basic from
and select
clauses, but we can also use into
like this:
var command = from i1 in Arg<int>() select i1 + 1 into i2 select i2 + 2; command(10); // => 13
And we can use let
clauses like this:
var command = from i1 in Arg<int>() let i2 = i1 + 1 let i3 = i1 + i2 select i3 + i2 + i1; command(10); // => 42
We can unlock additional clauses by adding more extension methods to the Func
class. For example, we could add rudimentary where
support that throws an exception if the test fails.
static Func<TArg, TResult> Where<TArg, TResult>( this Func<TArg, TResult> func, Func<TResult, bool> where) => arg => { var ret = func(arg); if (!where(ret)) throw new ApplicationException("where clause failed"); return ret; };
And use it like this:
var command = from i1 in Arg<int>() let i2 = i1 + 3 where i2 == 12 select i2; command(10); // => throws an exception
My favorite addon, however, is to use SelectMany
to change the arity of the function and add additional arguments. The implementation of SelectMany
could look like this:
static Func<TArg1, TArg2, TResult> SelectMany<TArg1, TVal1, TArg2, TResult>( this Func<TArg1, TVal1> func1, Func<TVal1, Func<TArg2, TResult>> func2) => (arg1, arg2) => func2(func1(arg1))(arg2);
And and could be used like this:
var command = from i in Arg<int>() from j in Arg<string>() select i + 3 + j; command(10, " suffix"); // => 13 suffix
Again, I would not recommend anyone build functions this way, but it is a fun example of the possibilies with C# query expressions.
2020-12-16