One of the big sells of shared runtime functional languages such as F#, Scala and Clojure is that you can carrying on using the surrounding library ecosystem and your existing code. The different paradigm occasionally causes a little pain, but there are plenty of blog posts about how to wrap OO interfaces in a functionally friendly way.
This is not one of those blog posts. This is about making sure that your colleagues who are consuming your shiny new code in an imperative language (generally C# in my case) don't threaten to defenestrate you.
At 15below we've recently had need in some of our services of taking a distributed lock between servers. There are many services available designed for doing this, but after some deliberation we decided that we didn't want to add a new piece of infrastructure purely for this one purpose. So Sproc.Lock was born: SQL Server based distributed locking.
In this post, I'm not going to talk about the design of the service. What I'm going to write about is how I engineered the API to be pleasent to use from both C# and F#, giving a idiomatic interface from both languages.
The original interface (F#)
The F# interface was written first, and follows a pattern that will feel
immediately familiar to an F# programmer. Our lock can be of 3 types (global,
organisation or environment) and so we have a discriminated union (Lock
)
representing these three options.
(I've removed the implementations of the various bits to leave the shape of the code clear)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
The lock is IDisposable
to take advantage of .net's most common resource
management idiom. You can release a lock by disposing it.
Then, of course, when we try and acquire a lock we may or may not be able to - the whole point of locks is that you cannot obtain them if someone else has locked it already, after all.
So we have a second discriminated union (LockResult
) wrapping the first,
with (again) three potential cases:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Again, this is IDisposable
so that you can just dispose of your overall
LockResult
object which makes a lot of the code cleaner.
So: how do we get a LockResult
? Well, we have a set of functions for getting
locks. Let's have a look at the skeleton of one of them:
1 2 3 |
|
What's this doing? Well, it's going to (try and) create a lock scoped to a
particular database and organisation with a particular ID, returning a
LockResult
.
From an API design point of view, what's interesting here is the order of the arguments. Currying enables easy partial application, and here it is very likely that the application will want to take all locks from the same database (making the first parameter) and reasonably likely that it will always want them scoped to the same organisation (second parameter). This is a common pattern in languages that allow for easy currying, and invariably a consumer of this library in F# will end up with a partially applied helper function looking something like this:
1 2 3 |
|
We also have a set of helper functions for common operations we might want to
carry out on locks, all of which take a higher order function as part of their
arguments. Let's have a look at AwaitLock
which will wait for a lock to
become available for a specified length of time, rather then returning
immediately with an Unavailable
result:
1 2 3 4 5 6 7 |
|
If we then want (say) to wait up to 2 seconds for one of a list of possible
locks to become available, we can then compose this function with the
OneOfLocks
function:
1 2 3 4 5 6 7 |
|
I'm sure the comments will disagree, but I'm actually pretty happy with this as an F# interface to this library. It's not strictly pure, but that's an option in F#, and the combination of composable functions and careful choice of parameter order make for concise and readable code.
So, we're done - right?
Unfortunately not. This code would be truely horrible to use from C#, and we still use a lot of C# here - some of our (stranger?) developers even prefer it. Why would it be so nasty?
- Consuming discriminated unions from C# is verbose to the point of unusable
- Partial application is a pain in C#, and no one wants to repeat the connection string everytime they want a lock
- Function composition is possible in C# but is not idiomatic and may make the capabilities of the library unclear
API Take 2: the "OO" namespace
In thinking about the kind of API I would expect for a locking library in C#, a few things immediately sprang to mind:
- I would expect some kind of configurable provider object or factory
- Out of flow returns are normally signalled by exceptions
- Function composition only for more unusual calling options
Wrapping the functional API turned out to be reasonably simple. A couple of
custom exception types and the OOise
method later (I love that function
name, even if I say so myself) we can easily wrap our functional API in
something that makes sense in C# land - they either return an acquired,
IDisposable
lock or throw.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Then, a simple LockProvider
class allows for all the normal patterns we've
come to know (and in some cases love) such as dependency injection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
As you can see, by the time we get to the OneOf
members, we're pretty much
forced into taking higher order functions to avoid a combinatorial explosion of
members (not that that always seems to deter OO API designers…). Other than
that, I think we're left with an API which will immediately make sense to a C#
developer: you can new up a LockProvider
, you have a specified list of
exception types to expect, and you can easily intellisense your way around all
of the available options.
Our C# consuming code ends up looking a bit like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
Note the very different parameter order, placing the parameters that change most frequently at the beginning of the list as you would normally expect in C#. This makes a surprisingly large difference to how easy the code is to consume.
Again: quite nice, if I do say so myself.
So there you have it - want to play nicely the whole .net ecosystem? Be kind to your consumers, and build them an ecumenical API!