Today I was idly thinking about an idea I had a couple of years ago for a functional IoC container. I’d had a go at implementing such a beast, but soon got bogged down in a tangled mess of spaghetti reflection code and gave it up as too much bother. But today it suddenly occurred to me that there was no need for any reflection voodoo; the C# type system is powerful enough to do all the work for us.
In object oriented programming languages we build programs from classes. Classes declare the contract(s) they support with interfaces and declare their dependencies with constructor arguments. We use an IoC container to wire instances of our classes together to make a running program.
Pure functional languages, like Haskell, don’t have any concept of class, instead they use currying and partial application to compose hierarchies of functions.
Here’s an example of a purely functional program written in C#.
public static class Module
{
public static Data GetAndTransform(Func<Input,Data> dataAccsessor, Func<Data,Data> transformer, int id)
{
var input = new Input() {Id = id};
var data = dataAccsessor(input);
var transformed = transformer(data);
return transformed;
}
public static Data DataAccsessor(Input input)
{
return new Data
{
Id = input.Id,
Name = "Test"
};
}
public static Data Transformer(Data original)
{
original.Name = original.Name + " transformed";
return original;
}
}
GetAndTransform simply takes an int id argument, does some work, and then returns some data. It needs a dataAccsessor and a transformer in order to do its job.
C# doesn’t support currying or partial application, so in order to run it we have to compose the program and execute it all in one step. For example:
var id = 10;
var data = Module.GetAndTransform(Module.DataAccsessor, Module.Transformer, id);
Console.Out.WriteLine("data.Id = {0}", data.Id);
Console.Out.WriteLine("data.Name = {0}", data.Name);
But what if we had a ‘currying container’, one that could compose the program in one step and then return a function for us to execute in another? Here is such a container at work with our program:
var registration = new Container()
.Register<Func<Input, Data>, Func<Data, Data>, int, Data>(Module.GetAndTransform)
.Register<Input,Data>(Module.DataAccsessor)
.Register<Data,Data>(Module.Transformer);
var main = registration.Get<Func<int, Data>>();
var data = main(10);
Console.Out.WriteLine("data.Id = {0}", data.Id);
Console.Out.WriteLine("data.Name = {0}", data.Name);
In the first line, we create a new instance of our container. On the next three lines we register our functions. Unfortunately C#’s type inference isn’t powerful enough to let us do away with the tedious type annotations; we have to explicitly declare the argument and return types of each of our functions.
Once our functions are registered we can ask the container for a program (main) that takes an int and returns a Data instance. The container works out that it needs to curry GetAndTransform and then partially apply DataAccsessor and Transformer to it to produce the desired function.
We can then run our ‘main’ function which gives us our expected output:
data.Id = 10
data.Name = Test transformed
The container turns out to be very simple, just a dictionary that’s keyed by type and contains a collection of constructor functions that know how to build the target (key) type.
public interface IRegistration
{
void Add(Type target, Func<object> constructor);
T Get<T>();
}
public class Container : IRegistration
{
private readonly Dictionary<Type, Func<object>> registrations = new Dictionary<Type, Func<object>>();
public void Add(Type target, Func<object> constructor)
{
registrations.Add(target, constructor);
}
public T Get<T>()
{
return (T)registrations[typeof (T)]();
}
}
The magic sauce is in the Registration function overloads. If you take the standard functional idea that a function should only have one argument and one return type, you can take any input function, curry it, and then partially apply arguments until you are left with a Func<X,Y>. So you know what the ‘target’ type of each function should be, a function from the last argument to the return type. A Func<A,B,C,R> gets resolved to a Func<C,R>. There’s no need to explicitly register a target, it’s implicit from the type of the provided function:
public static class RegistrationExtensions
{
public static IRegistration Register<A,R>(this IRegistration registration, Func<A, R> source)
{
var targetType = typeof (Func<A, R>);
var curried = Functional.Curry(source);
registration.Add(targetType, () => curried);
return registration;
}
public static IRegistration Register<A,B,R>(this IRegistration registration, Func<A, B, R> source)
{
var targetType = typeof (Func<B, R>);
var curried = Functional.Curry(source);
registration.Add(targetType, () => curried(
registration.Get<A>()
));
return registration;
}
public static IRegistration Register<A, B, C, R>(this IRegistration registration, Func<A, B, C, R> source)
{
var targetType = typeof(Func<C, R>);
var curried = Functional.Curry(source);
registration.Add(targetType, () => curried(
registration.Get<A>()
)
(
registration.Get<B>()
));
return registration;
}
}
Each overload deals with an input function with a different number of arguments. My simple experiment only works with functions with up to three arguments (two dependencies and an input type), but it would be easy to extend for higher numbers. The Curry function is stolen from Oliver Sturm and looks like this:
public static class Functional
{
public static Func<A, R> Curry<A, R>(Func<A, R> input)
{
return input;
}
public static Func<A, Func<B, R>> Curry<A, B, R>(Func<A, B, R> input)
{
return a => b => input(a, b);
}
public static Func<A, Func<B, Func<C,R>>> Curry<A, B, C, R>(Func<A, B, C, R> input)
{
return a => b => c => input(a, b, c);
}
}
Rather nice, even if I say so myself.
Of course this little experiment has many limitations. For a start it only understands functions in terms of Func< … >, so you can’t have more than one function of each ‘type’. You couldn’t have two Func<int,int> for example, which might be somewhat limiting.
The code is on GitHub here if you want to have a play.
This comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDelete