In mixed paradigm languages such as F# and Scala you frequently end up using mutable APIs in your "nice" pure functional code. It might be because you're using a 3rd party library, or it might be for performance reasons - but either way it's very easy to make mistakes with mutable constructs when you're in a functional mind space, especially if you want to compose operations on instances of a mutable type.
Let's have a look at one way of handling this issue: custom operations on computational expressions. We'll take the Provided Types API for building types within a type provider as an example of an API to use, and see what we can do to wrap it.
Firstly, let's give an example of the issue. Creating even a simple type within a type provider looks something 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 |
|
The main problem is right at the end on line 26: having
created your property you need to then add it the the mutable ProvidedTypeDefinition
. This is easy to forget on the one hand, and makes it hard too
compose partial type definitions on the other.
One way to handle this would be to create a function that takes a provided type definition and knows how to amend it with a provided property.
1 2 3 4 5 6 7 8 9 |
|
Now if we have a lot of types that need, say, a "hello world" and "goodbye world" property added we can do something like this:
1 2 3 4 |
|
So now you can pass in a ProvidedTypeDefinition
and get out one with
your two common properties added. But now the secret is that you want to
pass around these builder functions as much as possible, and only actually
pass in a instance of ProvidedTypeDefinition
right at the end; up until
you do, you have something composable and reusable. Once you've created your
instance, you're done.
This sounds similar, but not quite like, continuation passing style programming
as used in things like async
under the hood. Which raises the interesting
possibility that we might be able to abuse computational
expressions to make our code a bit nicer. Let's give it a go!
Computational expressions are built via a class with some strictly named member methods which the F# compiler then uses to translate the computational expression code into "standard" F#.
The type the CE is going to operate on is going to be
ProvidedTypeDefinition -> ProvidedTypeDefinition
(similar to the state
monad for those of you who've played with it). But it's going to be a little
odd, as we have no monad and won't be following the monad laws, so there's
really no meaningful bind operation. What would that look like?
Something like this:
1 2 3 4 5 6 7 8 9 10 11 |
|
So we have a bind… but it can only bind unit
and no other type. All
it knows how to deal with is composing two ProvidedTypeBuilder -> ProvidedTypeBuilder
functions. Zero
and Return
make some sense as well: both
can be meaningfully defined using the id
function; just take the provided
type definition and pass it on unchanged.
Now we can write code like this!
1 2 3 4 5 |
|
Okay, so I admit we're not quite there yet. Time to dive into the fun bit; adding a custom operation to our builder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Now we're starting to get somewhere, with code that begins to look like this:
1 2 3 4 5 6 7 8 |
|
withHelloWorld
has a type of ProvidedTypeDefinition -> ProvidedTypeDefinition
as you'd expect. But there's still no easy way to compose these; let's
add that next.
1 2 3 4 5 6 |
|
The including
operation is just a wrapper around combine, but it allows us
to do things like this:
1 2 3 4 5 6 7 8 9 |
|
And now the power of this technique begins to be shown, as we build blocks of composable code which can be included within each other.
Obviously a lot more could be done at this point: we've barely scratched the provided types API, but we'll leave the blog post at this point.
This blog post comes with many thanks to Andrew Cherry who took some pretty mad lunch time discussions and turned them into the very real and usable Freya (along with a bunch of collaborators). Freya makes use of this kind of technique heavily.