I've been doing some experiments with F# which involves using Windsor (my default choice for a IoC container) and found that Windsor's fluent interface is... not so fluent when used in F#.
UPDATE 10/21/2010: The F# team has loosened the syntax a lot, F# 2.0 can now consume the Windsor fluent API without any changes.
Fluent interface as-is in F#
Example: given this code in C#
container.Register(Component.For<IMyServiceContract>().ImplementedBy<MyServiceImpl>());
Let's try to translate this to F#. (I'll do it step by step so it's more didactic)
Code: container.Register(Component.For<IMyServiceContract>().ImplementedBy<MyServiceImpl>())
Compiler says: Error: Successive arguments should be separated by spaces or tupled, and arguments involving function or method applications should be parenthesized.
Explanation: Component.For<IMyServiceContract>()
is a method application, so it has to be parenthesized:
Code: container.Register((Component.For<IMyServiceContract>()).ImplementedBy<MyServiceImpl>())
Compiler says: Error: Type constraint mismatch. The type ComponentRegistration<IMyServiceContract> is not compatible with type IRegistration array.
The type 'ComponentRegistration<IMyServiceContract>' is not compatible with the type 'IRegistration array'.
Explanation: The reason for these errors is that F# expects an array as the parameter for Register, since it's signature is
IWindsorContainer Register(params IRegistration[] registrations)
and F# doesn't support params arrays, so we have to explicitly construct an array:
Code: container.Register([|(Component.For<IMyServiceContract>()).ImplementedBy<MyServiceImpl>()|])
Compiler says: Error: This expression has type ComponentRegistration<IMyServiceContract> but is here used with type IRegistration.
Explanation: What? But ComponentRegistration<IMyServiceContract>
implements the IRegistration
interface! Yeah, but Register()
takes an array of IRegistration
, not an array of ComponentRegistration<IMyServiceContract>
, and F# doesn't implement array covariance. You can write this in C#:
IMyServiceContract[] arr = new MyServiceImpl[] { new MyServiceImpl() };
but you can't write this in F#:
let arr: IMyServiceContract[] = [| MyServiceImpl() |]
This is actually a Good Thing since this kind of covariance has some nasty consequences. So we have no option but to cast:
Code: container.Register [| (((Component.For<IMyServiceContract>()).ImplementedBy<MyServiceImpl>()) :> IRegistration) |]
Compiler says: Warning: This expression should have type 'unit', but has type 'IWindsorContainer'.
Explanation: We have to do something about the return value. We're not using it in this example, so we'll just discard it:
Code: let _ = container.Register [| (((Component.For<IMyServiceContract>()).ImplementedBy<MyServiceImpl>()) :> IRegistration) |]
This finally compiles, but it's way too noisy. Not fluent at all!
We could have also written:
let _ = container.Register(Seq.to_array (Seq.cast [(Component.For<IMyServiceContract>()).ImplementedBy<MyServiceImpl>()]))
but it's just as ugly.
Extension method solution
We could hide the casting and array stuff in an extension method:
module WindsorContainerExtensions =
type IWindsorContainer with
member x.RegisterComponents (r: seq<#IRegistration>) =
let _ = x.Register (Seq.to_array (Seq.cast r))
()
which allows us to write:
container.RegisterComponents [(Component.For<IMyServiceContract>()).ImplementedBy<MyServiceImpl>()]
Much better, but it doesn't quite fit the F# spirit.
Function pipelines solution
A better-yet solution is to use function pipelining to build a DSL, like FsUnit and FsTest do, so we can write:
typeof<IMyServiceContract>
|> implementedBy (typeof<MyServiceImpl>)
|> registerIn container
Here are the functions that support this:
let implementedBy impl (service: Type) =
(Component.For service).ImplementedBy impl
let registerIn (container: IWindsorContainer) registration =
container.RegisterComponents [ registration ]
It's easy to follow this pattern and implement similar functions to cover more functionality. For example, this sets the lifestyle for a registration:
let withLifestyle lifestyle (registration: ComponentRegistration<_>) =
(registration.LifeStyle).Is lifestyle
Usage:
typeof<IMyServiceContract>
|> implementedBy (typeof<MyServiceImpl>)
|> withLifestyle LifestyleType.Transient
|> registerIn container
Conclusion
Fluent interfaces are cool but quite language-dependent, so if you're designing an API targeting the CLR and thinking of building a fluent interface, make sure you also provide an alternative, simpler, non-fluent API so other languages can build their own flavor of fluent interface (Windsor does provide this, of course)