Tuesday, February 21, 2012

Reusable Ad-Hoc Extensions for .NET

I posted awhile ago about a pattern for ad-hoc extensions in .NET using generics. Unfortunately, like every "design pattern", you had to manually ensure that your abstraction properly implements the pattern. There was no way to have the compiler enforce it, like conforming to an interface.

It's common wisdom that "design patterns" are simply a crutch for languages with insufficient abstractive power. Fortunately, .NET's multicast delegates provides the abstractive power we need to eliminate the design pattern for ad-hoc extensions:

/// <summary>
/// Dispatch cases to handlers.
/// </summary>
/// <typeparam name="T">The type of the handler.</typeparam>
public static class Pattern<T>
{
    static Dispatcher<T> dispatch;
    static Action<T, object> any;

    delegate void Dispatcher<T>(T func, object value,
                                  Type type, ref bool found);

    /// <summary>
    /// Register a case handler.
    /// </summary>
    /// <typeparam name="T0">The argument type.</typeparam>
    /// <param name="match">Expression dispatching to handler.</param>
    public static void Case<T0>(Expression<Action<T, T0>> match)
    {
        var call = match.Body as MethodCallExpression;
        var handler = Delegate.CreateDelegate(typeof(Action<T, T0>),
                                              null, call.Method)
                   as Action<T, T0>;
        dispatch += (T x, object o, Type type, ref bool found) =>
        {
            // if type matches exactly, then dispatch to handler
            if (typeof(T0) == type)
            {
                found = true;   
                handler(x, (T0)o);
            }
        };
    }
    /// <summary>
    /// Catch-all case.
    /// </summary>
    /// <param name="match">Expression dispatching to handler.</param>
    public static void Any(Expression<Action<T, object>> match)
    {
        var call = match.Body as MethodCallExpression;
        var handler = Delegate.CreateDelegate(typeof(Action<T,object>),
                                              null, call.Method)
                   as Action<T, object>;
        any += handler;
    }

    /// <summary>
    /// Dispatch to a handler for <typeparamref name="T0"/>.
    /// </summary>
    /// <typeparam name="T0">The value type.</typeparam>
    /// <param name="value">The value to dispatch.</param>
    /// <param name="func">The dispatcher.</param>
    public static void Match<T0>(T0 value, T func)
    {
        bool found = false;
        dispatch(func, value, value.GetType(), ref found);
        if (!found)
        {
            if (any == null) throw new KeyNotFoundException(
                                       "Unknown type.");
            else any(func, value);
        }
    }
}

The abstraction would be used like this:

interface IFoo
{
    void Bar(int i);
    void Foo(char c);
    void Any(object o);
}
class xFoo : IFoo
{
    public void Bar(int i)
    {
        Console.WriteLine("Int: {0}", i);
    }
    public void Foo(char c)
    {
        Console.WriteLine("Char: {0}", c);
    }
    public void Any(object o)
    {
        Console.WriteLine("Any: {0}", o);
    }
}
static void Main(string[] args)
{
    Pattern<IFoo>.Case<int>((x, i) => x.Bar(i));
    Pattern<IFoo>.Case<char>((x, i) => x.Foo(i));

    Pattern<IFoo>.Match(9, new xFoo());
    Pattern<IFoo>.Match('v', new xFoo());
    try
    {
        Pattern<IFoo>.Match(3.4, new xFoo());
    }
    catch (KeyNotFoundException)
    {
        Console.WriteLine("Not found.");
    }
    Pattern<IFoo>.Any((x, o) => x.Any(o));
    Pattern<IFoo>.Match(3.4, new xFoo());
    // prints:
    // Int: 9
    // Char: v
    // Not found.
    // Any: 3.4
}

Unlike the previous pattern for ad-hoc extensions, dispatching is always precise in that it dispatches to the handler for the value's dynamic type. The previous solution dispatched only on the static type. This can also be a downside, but you could easily extend the Match method to test on subtypes as well.

The other downside of this solution is that it's not quite as fast since all the type tests are run on each dispatch, where the previous solution cached the specific delegate in a static generic field. This caching can be added to the above class as well. Then, you can have the best of both worlds if you happen to know that the static type is the same as the dynamic type.

No comments: