Today I was thinking about dependency injection and Haskell. If we think about how an IoC container in a language like C# works, there are several pieces:
- Services are described by types (usually interfaces).
- Components (classes) describe their dependencies with the types of their constructor arguments.
- The components in turn describe the services they provide by implementing service interfaces.
- Service types are registered against implementation types using some API provided by the IoC container.
- A clever piece of framework (the IoC container), that when asked for a service (described by an interface), creates the entire dependency chain and then passes it the caller.
The important point is that we don’t have to manually wire up the dependency chain. We simply build our software in terms of service contracts and implementations, register them with the IoC container which then magically does the wiring up for us.
If you dig into the source code for any of the .NET IoC containers (such as Windsor, StructureMap, Autofac, Ninject etc) you’ll see they do an awful lot of reflection to work out the dependency graph. I remember reading (or hearing someone say) that reflection is often used to make up for inadequacies in C#’s type system, so I started experimenting to see if Haskell could provide the decoupling of registration and dependency graph building without the need for an IoC container. And indeed it can.
First let’s define some services:
-- this could talk to a database
type GetReport = Int -> IO Report
-- this could talk to an email API
type SendReport = Report -> IO ()
-- this takes a report id and does something with it
type ProcessReport = Int -> IO ()
Now let’s define some implementations:
-- getReport simply creates a new report with the given id
getReport :: GetReport
getReport id =
return $ Report id "Hello"
-- sendReport simply prints the report
sendReport :: SendReport
sendReport report = putStr $ show report
-- processReport uses a GetReport and a SendReport to process a report
processReport :: GetReport -> SendReport -> ProcessReport
processReport get send id = do
r <- get id
send r
Partial function application is equivalent to dependency injection in OO. Here our processReport’s dependencies are given as the first two arguments of the processReport function.
Now let’s define a type class with a ‘resolve’ member. The resolve member isn’t a function as such, it’s just a label for whatever ‘a’ happens to be when we define instances of the type class:
class Resolvable a where
resolve :: a
Now let’s make each of our services an instance of ‘Resolvable’, and ‘register’ the implementation for each service:
instance Resolvable GetReport where
resolve = getReport
instance Resolvable SendReport where
resolve = sendReport
instance Resolvable ProcessReport where
resolve = processReport resolve resolve
Note that we partially apply processReport with two resolve calls that will provide implementations of the service types.
The whole caboodle compiles and we can use resolve to grab a ProcessReport implementation with its dependencies provided:
> let p = resolve :: ProcessReport
> p 23
Report 23 "Hello"
All the functionality of an IoC container without an IoC container. Wonderful.
So, to sum up, we’ve simply registered implementations against services and let the Haskell type system build the dependency graph for us. The added benefit we get here over reflection based IoC containers in C# and Java, is that this is all typed checked at compile time. No need to run it to find missing dependencies.
Please bear in mind that I’m a total Haskell novice and this is probably something that real Haskell programmers would never do. But it’s an interesting illustration of the power of the Haskell type system, and how a lot of what we do with reflection in C# is simply to augment the limitations of the language.
Now what is done is pretty neat, me too, i am not a Haskell expert, just more into OO..
ReplyDeleteSo what if there were 3 implementations of GetReport, registered, how could i specify which one to use for ProcessReport???
Hi Manish,
ReplyDeleteGood question, and the answer is, you can't. Haskell's type system will only allow you to create one instance of the Resolvable per type. You could create type synonyms, but then you'd resolve them differently as well.
As far as I'm aware Funq (and the derivative Munq) generally avoid using reflection for wiring up the dependency graph. Makes them extremely fast when compared with many of the other containers. Munq is available as a nuget package for MVC3 projects.
ReplyDelete