About Jolt.NET Libraries

Inspired by the Boost C++ libraries, Jolt.NET aims to complement the .NET Base Class Library (BCL) with algorithms, data structures, and general productivity tools. It is the hope of the authors that the features of Jolt.NET will one day be part of, or represented in the BCL and the .NET Framework.

Jolt.NET Functor Composition

I recently completed the implementation of the generic Compose class, allowing a caller to build composite functors with up to seven parameters.  In this post, I will discuss the implementation and a minor coding nuisance that I encountered while writing the code.  But first, here is a quick recap the features that were committed since my last post.

  • Jolt.Functional functor creation and manipulation library (see this post, and the docs)
  • Ability to override return type in Jolt.Testing.ProxyTypeBuilder (see this post, and the docs)

Compose Implementation

The Compose class is implemented as a collection of generic static functions: First, Second, Third and Fourth.  Each of these functions binds the execution of a delegate to an argument of another delegate, the position of the argument being denoted by the function’s name. A composite delegate is represented as a new delegate, and its generic implementation takes care of binding arguments and calling the composed delegates at the right time.  Functor composition is very similar to argument binding, except that the bound arguments are now delegates instead of constant values.

The rules and supported scenarios for creating a composite are straightforward:

  • You can bind any System.Func functor F to an argument A of any System.Action or System.Func delegate so long as the return type of F matches the type of A.
  • Only one functor can be bound per Compose method call.
  • Bound functors are executed when the resulting composite is executed, in LIFO order.
    • E.g. Given a binary functor F, nullary functors G and H, and the composite c = Compose.First(Compose.First(F, G), H), executing c() results in the execution of H first, followed by G and F.
  • Creating a composite yielding more than four arguments results in a Jolt.Functional.Action or Jolt.Functional.Func delegate, each of which support five through seven arguments.

When writing the methods of Compose, I was primarily concerned with enforcing the rules from the first bulleted item above at compile-time.  Doing so would ensure that the creation of the composite is as fast as possible since the code would not need to perform checks for possible user errors.  Unfortunately, taking this approach meant that I would need to declare every possible permutation of binding a 1-4 argument delegate with a 0-4 argument functor.  There are a total of 100 of such combinations, and the only difference between any two given functions is generally the number of delegate arguments and the position at which the binding occurs.

To the best of my knowledge, there is no way to use generics to reduce the number of functions that are needed to implement all of the desired combinations.  C# does not allow a generic parameter to be constrained to a System.Delegate type, and if it were possible, there is still no facility to constrain a delegate to one with a given number of arguments.  For this situation, a C++-like variadic template argument or typelist feature is very desirable as it will eliminate the method overloads enabling composition of functors with varying arguments.  To illustrate, one could reduce the number of First method overloads from 40 to 8 using the following variadic generic argument pseudocode.

public static class Compose
{
public Func<params Args, TResult>
First<T, TResult, params Args>(Func<T, TResult> function, Func<params Args, T> innerFunction);
public Func<params Args, TResult>
First<T1, T2, TResult, params Args>(Func<T1, T2, TResult> function, Func<params Args, T1> innerFunction);
public Func<params Args, TResult>
First<T1, T2, T3, TResult, params Args>(Func<T1, T2, T3, TResult> function, Func<params Args, T1> innerFunction);
public Func<params Args, TResult>
First<T1, T2, T3, T4, TResult, params Args>(Func<T1, T2, T3, T4, TResult> function, Func<params Args, T1> innerFunction);

public Action<params Args>
First<T, params Args>(Action<T> function, Func<params Args, T> innerFunction);
public Action<params Args>
First<T1, T2, params Args>(Action<T1, T2> function, Func<params Args, T1> innerFunction);
public Action<params Args>
First<T1, T2, T3, params Args>(Action<T1, T2, T3> function, Func<params Args, T1> innerFunction);
public Action<params Args>
First<T1, T2, T3, T4, params Args>(Action<T1, T2, T3, T4> function, Func<params Args, T1> innerFunction);
}

An alternate implementation technique is to utilize reflection to compose abstract System.Delegate instances.  As mentioned earlier, this solution requires additional code to guard against user errors.  It would also rely heavily on reflection to determine the type of and create an instance of the resulting delegate.  The implementation will not perform as well as the many-overloads solution, but has the benefit of producing far less code to maintain.  It also removes the restriction of delegate types that are usable with the library (System.Action and System.Func), even though this isn’t a bad restriction.

In the end, I chose to avoid the reflection-based approach favoring runtime speed and a strongly-typed interface over smaller code quantity.  The implementations of the Compose methods and their tests are trivial, and if I had to write such a class again, I would likely implement a code generator to emit all of the desired permutations.  Implementing the code generator may not be such a trivial task, but it will likely take far less time than copying and tweaking code, and making sure that the XML doc comments for the new code is correctly specified.

Finally, I should note that the reflection-based approach has the merit of enabling composition/binding support for abstract System.Delegate instances.  We can utilize a method similar to the following to convert a System.Delegate to its equivalent System.Action or System.Func counterpart.  The resulting delegate may then be used with the Jolt.Functional classes.


public static class Convert
{
public static Action ToAction(Delegate function)
{
// TODO: validate input and cast.
return Delegate.CreateDelegate(typeof(Action), function.Method) as Action;
}
}

1 comments:

Steve Guidi said...

One of the great things about delegates is that they can be initialized from another delegate with a matching signature. Consequently, the Convert.ToAction() example is not necessary if you know the concrete type of the given delegate. You can simply invoke new Action(MyCustomDelegate).