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)
4 comments:
just a minor note:
instead of "let _ = ..." you can write "... |> ignore" that normaly looks somewhat nicer.
Also, you should be able to just use "upcast" rather than specify the exact type you want to cast to.
Actually, I think the real problem is that any form of fleunt interface violates the spirit of functional languages,(that is "No side effects" when a fleunt interface is entirely side effects)
@James: yes, but not in this case, since I'm only consuming the API with F#. Mutability would be an issue if the API would have been built with F#. The problem here is that what's idiomatic for C# is likely not idiomatic for F#, therefore "fluency" is a highly language-dependent term and API designers should keep this in mind.
Post a Comment