C#'s Generic Math from F#
Tags: dotnet, csharp, fsharp
Published: 2022-12-12 11:40:00

At this rate I should just legally rename myself to the language bindings guy or something. Fourth year in a row and I've got more to talk about when it comes to .NET bindings. But hey, I'm [not the only one](https://www.planetgeek.ch/2022/12/02/our-journey-to-f-c-f-interop/) this time! This year? The team introduced some goodies with .NET 7.0 and C# 11. [Generic Math](https://learn.microsoft.com/en-us/dotnet/standard/generics/math) is one of particular interest for use from F#, because of how well it fits with how F# already works. Like everything Microsoft has developed however, it clearly was rushed and has some rough spots. We'll be exploring those, and how I've found for working around them.

## Getting Started

I must really be milking this subject, right? You've read my last articles, you know how this is going to work. The generic math feature introduced with C# 11 and .NET 7.0 is just methods off generic interfaces. We can just call them directly like this:

```fsharp
let inline abs (value:^a) = ^a.Abs(value)
```

And that's it! I don't need to write another article on this again. Try it though. It fails. Miserably. The syntax analyzer will recognize the problem: this just isn't valid F#.

So what about this?

```fsharp
let inline abs (value:^a) = INumberBase< ^a>.Abs(value)
```

Not invalid syntax anymore, but this still won't compile. So how do we actually call this then? The trick is in a simplified form of what we've already done before:

```csharp
public static T Abs<T>(this T value)
    where T : INumberBase<T> => T.Abs(value);
```

and

```fsharp
let inline abs (value) = Extensions.Abs(value)
```

This actually solves another problem as well, or, at least what I consider a problem: numerical functions from the Generic Math API have poor discoverability the way they were implemented. If you don't want extension methods on the C# side, just remove the `this` part; no other changes are necessary.

## Cranking to 11

Okay, that was simple, just unexpected. Surely that's it, and I'm still just milking an article. Four years straight of this? Well... We're about to need to dive into the internals of the F# standard library and features that only exist inside of it. Yeah, this just went hard. The problem lies with a few of the Generic Math interfaces that are kinda already represented in F#, but not in the same way: operators. What do I mean? Let's look at how F# implements the operators:

```fsharp
let inline (+) (x: ^T) (y: ^U) : ^V = 
    AdditionDynamic<(^T),(^U),(^V)>  x y 
    when ^T : int32       and ^U : int32      = (# "add" x y : int32 #)
    when ^T : float       and ^U : float      = (# "add" x y : float #)
    when ^T : float32     and ^U : float32    = (# "add" x y : float32 #)
    when ^T : int64       and ^U : int64      = (# "add" x y : int64 #)
    when ^T : uint64      and ^U : uint64     = (# "add" x y : uint64 #)
    when ^T : uint32      and ^U : uint32     = (# "add" x y : uint32 #)
    when ^T : nativeint   and ^U : nativeint  = (# "add" x y : nativeint #)
    when ^T : unativeint  and ^U : unativeint = (# "add" x y : unativeint #)
    when ^T : int16       and ^U : int16      = (# "conv.i2" (# "add" x y : int32 #) : int16 #)
    when ^T : uint16      and ^U : uint16     = (# "conv.u2" (# "add" x y : uint32 #) : uint16 #)
    when ^T : char        and ^U : char       = (# "conv.u2" (# "add" x y : uint32 #) : char #)
    when ^T : sbyte       and ^U : sbyte      = (# "conv.i1" (# "add" x y : int32 #) : sbyte #)
    when ^T : byte        and ^U : byte       = (# "conv.u1" (# "add" x y : uint32 #) : byte #)
    when ^T : string      and ^U : string     = (# "" (String.Concat((# "" x : string #),(# "" y : string #))) : ^T #)
    when ^T : decimal     and ^U : decimal    = (# "" (Decimal.op_Addition((# "" x : decimal #),(# "" y : decimal #))) : ^V #)
    // According to the somewhat subtle rules of static optimizations,
    // this condition is used whenever ^T is resolved to a nominal type or witnesses are available
    when ^T : ^T = ((^T or ^U): (static member (+) : ^T * ^U -> ^V) (x,y))
```

If you've never seen `(# #)` blocks in F# before, congratulations, we're about to learn about something you can never use in your own code! This is a special feature only usable within the F# standard library, allowing for inline "IL" to be written. Kinda. It's mostly IL but there's some conveniences. We can't use this, but it is useful to understand what is going on in this method, to see what we can or can't do about it. If we want to include the Generic Math operators, this behavior has to be reimplemented in entirety. So let's break this down.

```fsharp
AdditionDynamic<(^T),(^U),(^V)>  x y
```

This is clearly some internal from the standard library, so, what does that look like?

```fsharp
let AdditionDynamic<'T1, 'T2, 'U> (x: 'T1) (y: 'T2) : 'U =
    if type3eq<'T1, 'T2, 'U, int32> then convPrim<_,'U> (# "add" (convPrim<_,int32> x) (convPrim<_,int32> y) : int32 #) 
    elif type3eq<'T1, 'T2, 'U, float> then convPrim<_,'U> (# "add" (convPrim<_,float> x) (convPrim<_,float> y) : float #) 
    elif type3eq<'T1, 'T2, 'U, float32> then convPrim<_,'U> (# "add" (convPrim<_,float32> x) (convPrim<_,float32> y) : float32 #) 
    elif type3eq<'T1, 'T2, 'U, int64> then convPrim<_,'U> (# "add" (convPrim<_,int64> x) (convPrim<_,int64> y) : int64 #) 
    elif type3eq<'T1, 'T2, 'U, uint64> then convPrim<_,'U> (# "add" (convPrim<_,uint64> x) (convPrim<_,uint64> y) : uint64 #) 
    elif type3eq<'T1, 'T2, 'U, uint32> then convPrim<_,'U> (# "add" (convPrim<_,uint32> x) (convPrim<_,uint32> y) : uint32 #) 
    elif type3eq<'T1, 'T2, 'U, nativeint> then convPrim<_,'U> (# "add" (convPrim<_,nativeint> x) (convPrim<_,nativeint> y) : nativeint #) 
    elif type3eq<'T1, 'T2, 'U, unativeint> then convPrim<_,'U> (# "add" (convPrim<_,unativeint> x) (convPrim<_,unativeint> y) : unativeint #) 
    elif type3eq<'T1, 'T2, 'U, int16> then convPrim<_,'U> (# "conv.i2" (# "add" (convPrim<_,int16> x) (convPrim<_,int16> y) : int32 #) : int16 #)
    elif type3eq<'T1, 'T2, 'U, uint16> then convPrim<_,'U> (# "conv.u2" (# "add" (convPrim<_,uint16> x) (convPrim<_,uint16> y) : uint32 #) : uint16 #)
    elif type3eq<'T1, 'T2, 'U, char> then convPrim<_,'U> (# "conv.u2" (# "add" (convPrim<_,char> x) (convPrim<_,char> y) : uint32 #) : char #)
    elif type3eq<'T1, 'T2, 'U, sbyte> then convPrim<_,'U> (# "conv.i1" (# "add" (convPrim<_,sbyte> x) (convPrim<_,sbyte> y) : int32 #) : sbyte #)
    elif type3eq<'T1, 'T2, 'U, byte> then convPrim<_,'U> (# "conv.u1" (# "add" (convPrim<_,byte> x) (convPrim<_,byte> y) : uint32 #) : byte #)
    elif type3eq<'T1, 'T2, 'U, string> then convPrim<_,'U> (String.Concat(convPrim<_,string> x, convPrim<_,string> y))
    elif type3eq<'T1, 'T2, 'U, decimal> then convPrim<_,'U> (Decimal.op_Addition(convPrim<_,decimal> x, convPrim<_,decimal> y))
    else BinaryOpDynamicImplTable<OpAdditionInfo, 'T1, 'T2, 'U>.Invoke "op_Addition" x y
```

Oh dear god, this is a mess, and it's still not even the entire thing, because there's that `BinaryOpDynamicImplTable<OpAdditionInfo, 'T1, 'T2, 'U>` type as well. However, we have everything we need to look at here, to see what behavior we need to provide. Going back to the `(+)` definition, let's look at one of the lines.

```fsharp
when ^T : int32       and ^U : int32      = (# "add" x y : int32 #)
```

This isn't too bad. When `^T` and `^U`, which are both input types, are both `int32`, do `(# "add" x y : int32 #)`; just a simple add instruction. There's many more lines like this that you can interpret the same way. So let's look at one with a bit more going on.

```fsharp
when ^T : int16       and ^U : int16      =
    (# "conv.i2" (# "add" x y : int32 #) : int16 #)
```

You recognize the `(# "add" x y : int32 #)` part from before. Critically here though, you'll recognize the `: int32` portion clashes with the `int16` type of the parameters. However, if you're used to C#, you might recognize this is correct behavior. .NET adds smaller numbers as 32-bit numbers. You have to convert back if you want smaller numbers again. `(# "conv.i2"` is the instruction for converting the top of the stack to an `i2`, or a two-byte integer; in F# you know this as a 16-bit integer, or `int16`. And lastly `: int16 #)` is just a typemark, which we now know matches the `i2` part of `conv`.

To summarize at this point, we've seen two behaviors. For numerics larger than 32-bits, they are added directly. For numerics smaller than 32-bits, they are upsized, added, and then converted back to their input size.

```fsharp
when ^T : string      and ^U : string     = (# ""
    (String.Concat((# "" x : string #),(# "" y : string #)))
    : ^T #)
```

There's parts of this that I could explain but won't. The critical part is something you've probably already figured out: when the input is two strings, concatenate them. We see this behavior in C# as well, and it's exactly the same.

This is now three different behaviors. Lovely.

```fsharp
when ^T : decimal     and ^U : decimal    = (# ""
    (Decimal.op_Addition((# "" x : decimal #),(# "" y : decimal #)))
    : ^V #)
```

Read this the same as the `string` one, and you can see that given two `decimal`, we're calling `Decimal.op_Addition` on them. This is the internal name for the addition operator, however your language presents it.

That leaves one last line in the main section:

```fsharp
when ^T : ^T = ((^T or ^U):
    (static member (+) : ^T * ^U -> ^V) (x,y))
```

And uhh... this is one I understand but don't understand, if you understand me. `(static member (+) : ^T * ^U -> ^V) (x,y))` is something you've seen before if you've read my previous articles. We're calling `op_Addition` on the parameters. `when ^T : ^T` is the part I'm a little confused by. Not constraining `^T` at all theoretically makes sense as a catch all, but considering I could never actually get this line of code to execute, I think they've made a mistake here. Regardless, the intention is for the standard `+` operator to fallback to any defined `op_Addition` that exists.

So what about the next block, the `AdditionDynamic` part? I'll let you read through that. It's another case where I understand but don't understand. It's basically a repeat of what we already covered, but is supposed to include behavior to automatically convert compatible types. If this sounds like something F# doesn't do, congratulations, you see why I'm confused.

So why do I say F#'s fallback addition doesn't work? Well, the reference project I have for writing this has the following definitions:

```fsharp
/// <inheritdoc/>
public static Percent<T> operator +(Percent<T> left, Percent<T> right) =>
    new(left.Value + right.Value);

/// <inheritdoc/>
public static T operator +(T left, Percent<T> right) =>
    left + right.Of(left);
```

The thing is, the `^T percent -> ^T percent -> ^T percent` one works fine, it's the `^T -> ^T percent -> ^T` one that doesn't. Looking back at the `+` definition, it should have caught this, so, what's going on here?

Let's review what necessary behaviors we're after:

1. Numerics greater than the working size of .NET (32-bit) can be added together directly and their results returned.
2. Numerics lesser than the working size of .NET (32-bit) must be converted to 32-bit numerics of compatible typeclass then added together, with their results converted back to the origin type.
3. Strings are concatenated together.
4. Any types with a defined `op_Addition` should have that used.

Is there another way we can get all this behavior? Well, yeah, actually. Let's start with this:

```fsharp
/// <summary>Adds two values together to compute their sum.</summary>
let inline ( + ) (left:^left) (right:^right):^result =
    ((^left or ^right) :
    (static member (+) : ^left -> ^right -> ^result)(left, right))
```

As you can probably tell, this is very similar to that catch-all line from the standard definition, although there's some differences. Funnily enough, this works in almost all of the cases we need it to. For 1, 3, and 4 of the requirements we laid out, this already works great. All of these tests will compile and pass fine:

```fsharp
Assert.Equal(2, 1 + 1)
Assert.Equal(2.0, 1.0 + 1.0)
Assert.Equal("hello!", "hello" + "!")
Assert.Equal(12.5, 10.0 + percent(25.0))
```

And that's pretty awesome, because there's wayyyyy less code on our end.

But what about requirement 2?

I'll leave this one to you to fix, as practice makes perfect and there's a lot to digest in this article. My tip is that the solution is something we covered right here in this article, but there's another way to solve it that comes from a prior article. As it stands now, adding numerics smaller than 32-bits adds fine, but returns a 32-bit numeric, which is standard behavior for .NET itself, C#, and VB. All that needs to be done is that in these cases, the return value be converted back to the origin type. In the rest of the cases, we're already got correct behavior.

If you've made it this far, the good news is that I've already done all the work in implementing both the [base library](https://www.nuget.org/packages/Numbersome) and [F# bindings](https://www.nuget.org/packages/Numbersome.FSharp) for this.