Assign a name to a set of type constraints in F#?

Viewed 121

Suppose I have a generic type with some complex type constraints in F#:

[<Struct>]
type Vec2<'t when 't : equality
              and 't : comparison
              and 't : (static member get_Zero : Unit -> 't)
              and 't : (static member (+) : 't * 't -> 't)
              and 't : (static member (-) : 't * 't -> 't)
              and 't : (static member (*) : 't * 't -> 't)
              and 't : (static member (/) : 't * 't -> 't)> =
  {
    X : 't
    Y : 't
  }

Now I want to create another generic type that builds on this:

// Does not work

[<Struct>]
type AABB<'t> =
  {
    Min : Vec2<'t>
    Max : Vec2<'t>
  }

This does not work unless I replicate the type constraints:

[<Struct>]
type AABB<'t when 't : equality
              and 't : comparison
              and 't : (static member get_Zero : Unit -> 't)
              and 't : (static member (+) : 't * 't -> 't)
              and 't : (static member (-) : 't * 't -> 't)
              and 't : (static member (*) : 't * 't -> 't)
              and 't : (static member (/) : 't * 't -> 't)> =
  {
    Min : Vec2<'t>
    Max : Vec2<'t>
  }

This gets old fast!

Is there a way to bind the type constraints to a name so that I can reuse them throughout my code?

// Not real code

constraint IsNumeric 't = 
      't : equality
  and 't : comparison
  and 't : (static member get_Zero : Unit -> 't)
  and 't : (static member (+) : 't * 't -> 't)
  and 't : (static member (-) : 't * 't -> 't)
  and 't : (static member (*) : 't * 't -> 't)
  and 't : (static member (/) : 't * 't -> 't)

[<Struct>]
type Vec2<'t when IsNumeric 't> =
  {
    X : 't
    Y : 't
  }

[<Struct>]
type AABB<'t when IsNumeric 't> =
  {
    Min : Vec2<'t>
    Max : Vec2<'t>
  }
3 Answers

A reasonable workaround in this case is to create an interface that represents the constraints. This does not automatically work as named constraint, but you can define a helper function that captures the required operations and then pass the interface around (so that you can invoke the operations you want).

Let's say we want just addition and multiplication:

type INumericalOps<'T> = 
  abstract Add : 'T * 'T -> 'T
  abstract Mul : 'T * 'T -> 'T

[<Struct>]
type Vec2<'T> =
  { X : 'T
    Y : 'T }

[<Struct>]
type AABB<'T, 'O when 'O :> INumericalOps<'T>> =
  { Min : Vec2<'T>
    Max : Vec2<'T>
    Ops : 'O }

Now, the AABB type also contains an implementation of the INumericalOps interface, which is somewhat shorter than specifying all the constraints. We can create an inline function that captures the implementation of * and + for any type that supports those:

let inline capture () = 
  { new INumericalOps<_> with
    member x.Add(a, b) = a + b
    member x.Mul(a, b) = a * b }

When creating a value, the type inference will make sure we get the right implementation of numerical operations:

let aabb = 
  { Min = { X = 1.0; Y = 2.0 }
    Max = { X = 1.0; Y = 2.0 }
    Ops = capture() }

Your example looks like generic math. This thing will be supported in net6 as preview feature. This has advantages over all others approaches: native support in runtime, C# support it (more libs available), no runtime cost (performance is same as with direct call)

Even though there's no support for this in F# yet, it could look like this (not final, syntax may change)

type IAdditionOperators<'a> =
    static abstract (+) : 'a * 'a -> 'a

type IAdditionIdentity<'a> =
    inherit IAdditionOperators<'a>
    static abstract Zero : 'a

/// This vector type supports addition of 2 generic values
[<Struct>]
type Vector2<'a when 'a : IAdditionIdentity<'a>>(x, y) =
    member val X = x
    member val Y = y
    static member (+) (left, right) =
        Vector<'a>('a.(+)(left.X, right.X), 'a.(+)(left.Y, right.Y))

/// This vector supports all methods from INumber
[<Struct>]
type RichVector2<'a when 'a : INumber<'a>>(x, y) =
    member val X = x
    member val Y = y
    static member (+) (left, right) =
        Vector<'a>('a.(+)(left.X, right.X), 'a.(+)(left.Y, right.Y))
    interface INumber<'a> with
        static member (+) (left, right) =
             RichVector<'a>.(+)(left, right)
        // Requires implementation for +, -, *, /, DivRem,
        // Zero, One, Abs, Max, Min, Sign, Clamp, 
        // Create, CreateSaturating, CreateTruncating, TryCreate,
        // Parse, TryParse
Related