home

Abusing CSharp Query Expressions

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