Adapting Method Signatures…Functional Style

26. March 2010 00:09

Over the last year or so, I’ve become increasingly interested in functional programming.  There is just something elegant and sexy about the ways in which it can be applied.  In an effort to find more opportunities to apply a functional approach, I’ve started using delegates a lot more in my code.  I am gradually starting to see patterns emerge where the delegates offer a cleaner solution.  Perhaps, it would be more accurate to say combining delegates.

I recently had what I thought would be a practical example to share.  Rather than getting into a long tangent about the context, I’m going to get to the point and keep the example as short as possible.  Hopefully, you will still see the benefit and the point.

I was working on some code to facilitate simple SELECT queries that automatically get mapped into an instance of a specified type.  Here is a simplified version of the relevant code. 

public class Query
{
    readonly string sql;
    readonly Func<IDataReader, object> load_results_from;

    public Query(string sql, Func<IDataReader, object> load_results_from)
    {
        this.sql = sql;
        this.load_results_from = load_results_from;
    }

    public object run_against(IDatabase database)
    {
        using (var reader = database.execute_reader(sql))
            return load_results_from(reader);
    }
}

 

As you can see, there is nothing really glamorous about this code.  It allows the consumer to run a query against some database and map the results into an object.  The portion of interest for this post is how the result set gets transformed into an instance of a type with all of its properties automatically set. 

For the signature of the delegate responsible for loading the results, I’ve chosen for it to simply take an IDataReader and return some object.  This allows the consumer instantiating the class to have a great deal of flexibility for changing the behavior of loading the result set.  It is really nothing more than a strategy.  I could have declared an IResultLoadingStrategy to do the same thing, but this would require the consumer to create a class to provide another concrete implementation when they want to use a different strategy.  With this approach, the consumer only needs to provide a method, which could even be a simple lambda expression.

In most cases, I want to use AutoMapper and let it automatically handle the transformation from an IDataReader into the specified result type.  However, I need to give AutoMapper an additional argument.  It needs to know the Type of the object to return in order to perform the mapping procedure.  Based on the current signature, the Query class uses a delegate that accepts IDataReader.  Now, we could change the delegate to also accept a Type argument, but it seems unnecessary to me.  If a consumer is going to provide a custom method for loading results, I’m going to assume that the consumer already knows the type to be returned and will handle it accordingly in the supplied delegate.  So, why should I make them pass the type if they aren’t going to use it?  [Yes, you could argue the other way as well, but this is my example!]

Instead of changing the signature of the delegate to meet the needs of AutoMapper, we really need a way to adapt the AutoMapper signature to the signature required by the Query class.  If we had used an interface to define the signature of the load strategy, we could use the traditional adapter pattern.  However, it would require creating even more classes when all we really want is a function. 

 

Let’s see how a functional programming approach could achieve the same results as the traditional adapter pattern.

We basically need the following line of code for AutoMapper to perform the transformation (assuming a mapping has already been created):

return Mapper.Map(reader, typeof(IDataReader), result_type); 

 

So, we need to pass IDataReader and the Type of the result in order to get back the mapped instance.  However, the Query class requires a function that only accepts IDataReader and returns an object.  Recall the delegate as shown below:

Func<IDataReader, object> 

 

Basically, the problem is we need a way to adapt the necessary signature for AutoMapper

Func<IDataReader, Type, object>

to the signature needed by Query

Func<IDataReader, object>

 

If we put on our functional programming hat, a functional solution starts to seem obvious.  All we really need is a function that returns another function.  Something with a signature like this:

Func<Type, Func<IDataReader, object>>

 

How could that possibly work?  Closures to the rescue! 

public class MyAdapters
{
    public static Func<Type, Func<IDataReader, object>> auto_map = (result_type) =>
    {
        Func<IDataReader, object> load_result = (reader) =>
        {
            return Mapper.Map(reader, typeof(IDataReader), result_type);
        };

        return load_result;
    };
}

 

This does exactly what we need.  When auto_map is invoked, it is given the Type of the result that AutoMapper requires for the transformation.  The value of result_type is captured within a closure that corresponds to the signature required by the Query class.

In other words, you could create an instance of the Query class that uses AutoMapper by doing the following:

var query = new Query("SELECT * FROM ViewModel", MyAdapters.auto_map(typeof(ViewModel)));

 

This is a big part of what functional programming is all about.  Functions can return other functions.  When you start to look at your applications in this way, there are lots of interesting possibilities that arise from combining functions in many different ways.  Hopefully, this example has made some [minor] sense and you can get an idea of what’s possible.

But, wait a minute!  There is one last thing that still bothers me a bit.  Our functional solution may be “elegant”, but it seemed a bit ugly and difficult to read…especially with the nested function declaration with generics.  Wouldn’t it be nice to clean that up a bit.  Thanks to the C# 3.0, we can reduce the code quite a bit.  Actually, just one dense line:

public class MyAdapters
{
    public static Func<Type, Func<IDataReader, object>> auto_map = 
        (result_type) => 
        (reader) => Mapper.Map(reader, typeof(IDataReader), result_type);
}

 

That is certainly better, but I’m still not crazy about those ugliness of a nested Func<>.  Fortunately, .NET only cares that the underlying signatures match for delegates.  So, we could declare our own delegate with the same signature that has a more meaningful name.

public delegate object LoadQueryResultBehavior(IDataReader reader);
public delegate LoadQueryResultBehavior AutoMapQueryResult(Type destination_type);

public class MyAdapters
{
    public static AutoMapQueryResult auto_map = 
        (result_type) => 
        (reader) => Mapper.Map(reader, typeof (IDataReader), result_type);
}

 

Looking much better!  We could also change the Query implementation to use our custom delegate as well, but it still maintains the same semantics.

public class Query
{
    readonly string sql;
    readonly LoadQueryResultBehavior load_results_from;

    public Query(string sql, LoadQueryResultBehavior load_results_from)
    {
        this.sql = sql;
        this.load_results_from = load_results_from;
    }

    public object run_against(IDatabase database)
    {
        using (var reader = database.execute_reader(sql))
            return load_results_from(reader);
    }
}

 

Mmm…tasty!

Comments are closed

About Me

I'm a passionate software developer and advocate of the Microsoft .NET platform.  In my opinion, software development is a craft that necessitates a conscious effort to continually improve your skills rather than falling into the trap of complacency.  I was also awarded as a Microsoft MVP in Connected Systems in 2008, 2009, and 2010.


Can’t code withoutThe best C# & VB.NET refactoring plugin for Visual Studio
Follow jeff_barnes on Twitter

View Jeff Barnes's profile on LinkedIn

 

Shared Items

Disclaimer

Anything you read or see on this site is solely based on my own thoughts.  The material on this site does not necessarily reflect the views of my employer or anyone else.  In other words, I don't speak for anyone other than myself.  So, don't assume I am the official spokesperson for anyone.