This is an adaptation of the article that used to be on my personal blog that I neglected. It was originally part of the 2019 F# Advent.
You like F#. I like F#. I also like C# and even when I have control over both ecosystems, getting the two to play nice with each other isn't the easiest of things. Even worse is when you need to use code written by someone that... oh my god, where did they learn to program? Look, we've all been there. Some API's just feel awful, especially when consuming in F#.
So how do you deal with it? For many people they write their own replacement in F#.
There's a better way. And it doesn't require that much work.
I've written this mostly in hopes that library authors use this information to provide better F# support when possible, and to help the community to provide better F# support when not.
For the sake of this article I've written every F# programmers' worst nightmare: The most extremely imperative and procedural and impure library I could produce. And you don't have a choice in using something else because your manager is an idiot or something. The library is simple: a single specialized stack type for Double. You can check it out on GitHub.
Normally people assume that if they mark the assembly CLSCompliant then everything is great and every language can consume the API. This is half true. Everything can consume the API. I'm fairly sure everything can also eat dirt, but that doesn't mean you or I want to. I marked the example library CLSCompliant, but it's bad. I died inside writing it.
Let's have a look at the API.
void Add(); void Add(out Double result); void Subtract(); void Subtract(out Double result); void Multiply(); void Multiply(out Double result); void Divide(); void Divide(out Double result); //... As well as everything Stack<Double> would already have
Let's have a look at just how bad this is, using the stack API for the arithmetic
3 * 5 - 8.
let stack = DoubleStack() stack.Push(3.0) stack.Push(5.0) stack.Multiply() stack.Push(8.0) stack.Subtract()
So, that's not great. It works, and it's identical across languages. But man that's not the kind of API we want to be working with in F#. But what about those other methods, the ones with the out parameters?
let stack = DoubleStack() let mutable result = ref 0.0 stack.Push(3.0) stack.Push(5.0) stack.Multiply(result)
You see why I say I died inside. Surely there's no hope here. Surely this is so far-gone that you either suck it up and use it as is, or write your own with a F# friendly API.
Over the course of this article I'll show you how to turn this into a very function feeling API you'd be certain was written natively in F#.
A good starting point is to make Push(), Pop(), and Peek() feel functional, by implementing our own stack pipeline.
let ( |=> )(stack:DoubleStack)(value) = stack.Push(value) let stack = DoubleStack() |=> 5.0
And just like that we have a working pipeline! Right? No. We do have a working single operation, but this leaves us with the same situation, in a different coat of paint. Try chaining the pipeline and you'll see the issue. We need to return the stack in the function call.
let ( |=> )(stack:DoubleStack)(value) = stack.Push(value) stack let stack = DoubleStack() |=> 5.0 |=> 3.0 stack.Multiply() Assert.Equal(15.0, stack.Peek())
That's already looking a good amount better. Still a ways to go though. Let's take care of the other two methods we mentioned.
let inline pop (stack:DoubleStack) = stack.Pop() let inline peek (stack:DoubleStack) = stack.Peek()
This are very straightforward translations. So straightforward that they are inlined. These kind of methods are the easiest to bind, and are something you should already be familiar with. With all these combined, we're now left with something that's starting to look functional, but is still obviously not there yet.
let stack = DoubleStack() |=> 5.0 |=> 3.0 stack.Multiply() Assert.Equal(15.0, peek stack)
This is a deep one, because while it's not that big of an issue, it's not easy to solve. You can still deal with things though. See, that
DoubleStack() at the beginning of the pipeline when declaring is a bit annoying. Not the end of the world, but we can do better.
type Pipeline = static member Pipe(left:DoubleStack, right:float) = left.Push(right) left static member Pipe(left:float, right:float) = let result = DoubleStack() result.Push(left) result.Push(right) result let inline private pipe< ^t, ^a when (^t or ^a) : (static member Pipe : ^a * float -> DoubleStack)> left right = ((^t or ^a) : (static member Pipe : ^a * float -> DoubleStack)(left, right)) let inline ( |=> )(left:^a)(right:float) = pipe<Pipeline, ^a> left right
What in the heck is this? I promise this isn't some fancy black magic. Let's go over it one thing at a time.
First is the
Pipeline type we defined. This must have the same visibility as the function or operator which will be using it. In it we're defining overloads of
Pipe() which is static. You can define whatever you want here; these are the actual methods that are being called. The first one does what we already defined: it takes a stack and a float, pushes the float onto the stack, and returns the stack. The second one adds the behavior we wanted: it creates a stack, pushes the left value onto it, then pushes the right value onto it, then returns the stack.
Second is some inline and generic trickery which is probably the most intimidating thing to those not familiar with F#'s type system. It's not that bad, I promise. Ignoring the generic part, we have a function called
pipe with two curried parameters:
right. Not so bad. The generic part says that we're considering two statically resolved types:
^a. That static resolution is important, and because of that, this function absolutely must be inlined. It doesn't need to be visible however, so I've taken to making it private, always. The rest of the generic says that on
^a, there will be a static member
Pipe with the signature
^a * float -> DoubleStack. Look at those methods we were just talking about. As long as
^a matches one of their first parameters, we have a matching method. The seemingly repeating code in the definition part of this function just says to call whatever method this resolves to, with the tupled arguments
(left, right). There, that wasn't too bad. I'll admit even I still kind of think it's black magic though.
The third part is just a slight modification to our original stack pipeline operator. This also needs to be inlined now. Inlining both functions is extremely important. Similarly, we can change the lefthand parameter to be of
^a, so that it statically resolves. Then we call the black magic function instead of what we were doing. This is where
Pipeline comes into play. Assuming the library we're working with is third party, we can't add an instance member called
Pipe to it. And we definitely can't add an instance member to
Double! This additional parameter is for a type we've defined that will also have these methods, which is why it was declared as static member in the generic. Now it knows to look inside of our own type as well.
How much progress did this make?
let stack = 5.0 |=> 3.0 stack.Multiply() Assert.Equal(15.0, peek stack)
Not a whole lot has changed, but it certainly looks cleaner. Try out longer pipelines, it still works.
There is another, simpler way, to accomplish this. Use it when you can, but from my experience it's not possible in many situations, while this approach is. This simpler approach would not work in this instance.
I've found this is most useful when mapping overridden methods to functions so that currying and pipelining is possible. So this often means rearranging parameters. You may find other uses. I don't recommend doing this just because you can.
The last remaining thing is those pesky arithmetic methods. Surely by now we've finally run into something we can't fully bind to this functional, pipeline heavy, environment. Right?
Actually this one is really easy, with what we've already set up
let add (stack:DoubleStack) = stack.Add() stack
And so on. That's it. No seriously, that's it. Because of the stack pipeline operators exact symbol (|=>) not only does it render like a pipe arrow thing when using Fira Code or related fonts, but it also has the exact same precedence and associativity as the function pipeline operator, so there's nothing new to add.
Putting everything together we have:
let stack = 3.0 |=> 5.0 |> mul |=> 8.0 |> sub Assert.Equal(7.0, peek stack)
I told you it was possible. 😉
There's a lot more, but this article has covered a lot and I don't want to provide an overwhelming amount of information. So expect more in the future.