Advanced F# Interop

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#.

Meme: Stop right there criminal scum

There's a better way. And it doesn't actually require that much work.

Example Library

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 come up with. 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 pretty 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.

Meme: Trump saying wrong

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#.

Functional Push/Pop/Peek

I think 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 actually 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)

Functional Object Initializer

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 has 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: left and right. Not so bad. The generic part says that we're considering two statically resolved types: ^t and ^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 ^t or ^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 kinda 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 of these 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 ^t and 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.

Functional Stack Arithmetic

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.

Feels good man