genericsf#type-constraintsarrayaccess

Adding member constraint to an inlined function's paramater causes FS0752 on array accessor


I've read again and again the official Microsoft docs on type constraints, but I can't understand why this code doesn't compile :

let inline transform<'A, 'a when 'A : (member Item : int -> float)> (a: 'A) : 'a =
    a.[0]

error FS0752: The operator 'expr.[idx]' has been used on an object of indeterminate type based on information prior to this program point. Consider adding further type constraints

And with :

let inline transform<'A, 'a when 'A : (member f : int -> float)> (a: 'A) : 'a =
    a.f(0)

error FS0072: Lookup on object of indeterminate type based on information prior to this program point. A type annotation may be needed prior to this program point to constrain the type of the object. This may allow the lookup to be resolved.

Obviously I didn't understand how to use member constraint with generics in f#. The general problem I'm facing is that I want to make generic functions over "vector-like" types like standard float[], or Vector<float> from MathNet.Numerics or even DV from the DiffSharp package. Right now I have to get an ad hoc function for each type, like (full code) :

#I ".paket/load"
#load "mathnet.numerics.fsharp.fsx"
#load "diffsharp.fsx"

open DiffSharp.AD.Float64

open MathNet.Numerics
open MathNet.Numerics.LinearAlgebra
open MathNet.Numerics.LinearAlgebra.Double

let l1 = 4.5
let l2 = 2.5

let a0 = [1.1; -0.9]

let inline transformVec (a:Vector<float>) =
    let x1, y1 = l1 * cos a.[0], l1 * sin a.[0]
    let x2, y2 = x1 + l2 * cos (a.[0] + a.[1]), y1 + l2 * sin (a.[0] + a.[1])
    vector [x1; y1; x2; y2]

let inline transformDV (a:DV) =
    let x1, y1 = l1 * cos a.[0], l1 * sin a.[0]
    let x2, y2 = x1 + l2 * cos (a.[0] + a.[1]), y1 + l2 * sin (a.[0] + a.[1])
    toDV [x1; y1; x2; y2]

As you can see this functions do exactly the same things but operates on different types.

I would like to get a generic function like (not working code) :

let inline transform<'A, 'a when 'A : (member Item : int -> 'a)> (toExt : 'a list -> 'A) (a: 'A) : 'A =
    let x1, y1 = l1 * cos a.[0], l1 * sin a.[0]
    let x2, y2 = x1 + l2 * cos (a.[0] + a.[1]), y1 + l2 * sin (a.[0] + a.[1])
    toExt [x1; y1; x2; y2]

let transformVec = transform vector
let transformDV = transform toDV  

What am I missing?


Edit: I've got it half-working with Mathnet.Numerics

let inline transform (toExt : 'a list -> 'A) (a: 'A) : 'A = 
    let inline get i : 'a = (^A : (member get_Item: int -> 'a) a,i)
    let x1, y1 = l1 * cos (get(0)), l1 * sin (get(0))
    let x2, y2 = x1 + l2 * cos (get(0) + get(1)), y1 + l2 * sin (get(0) + get(1))
    toExt [x1; y1; x2; y2]

(transform vector) (vector a0)

Because it enforces 'a (warning FS0064) to be float, which I don't want... (DV from DiffSharp returns D type on get_Item, not float.)

replacing the declaration by

let inline transform<'a> (toExt : 'a list -> 'A) (a: 'A) : 'A = 

makes the compiler croaks:

error FS0001: The declared type parameter 'a' cannot be used here since the type parameter cannot be resolved at compile time


Solution

  • You need to call the member Item like this:

    let inline transform (a: 'A) : 'a = (^A : (member get_Item: _ -> _) a, 0)
    

    However you'll get a warning,

    ~vs72B.fsx(2,5): warning FS0077: Member constraints with the name 'get_Item' are given special status by the F# compiler as certain .NET types are implicitly augmented with this member. This may result in runtime failures if you attempt to invoke the member constraint from your own code.
    

    because some primitive types use "simulated members". So for a list it will work:

    transform ["element"]
    // val it : string = "element"
    

    but not for arrays

    transform [|"element"|]
    System.NotSupportedException: Specified method is not supported.
    at <StartupCode$FSI_0009>.$FSI_0009.main@()
    Stopped due to error
    

    This is because the F# compiler pretends that arrays have that member, but in fact they don't.

    If this is a problem, you can use a more complex solution with overloads to add special implementations for specific types, which is not straight-forward but I can show you how, or you may consider using F#+ which has an Indexable abstraction for types which typically have an Item property.

    Of course you can ignore that warning, with a #nowarn "77" but as you've seen the compiler can not check that someone will call your function with an array and fail at runtime.

    UPDATE

    Since you asked as follow up question how to use it, here's an example:

    #r "MathNet.Numerics.dll"
    #r "MathNet.Numerics.FSharp.dll"
    #r "FSharpPlus.dll"
    
    open FSharpPlus
    open MathNet.Numerics.LinearAlgebra
    
    let x = item 1 [0..10]
    let y = item 1 [|0..10|]
    let z = item 1 (vector [0.;1.;2.])
    
    // val x : int = 1
    // val y : int = 1
    // val z : float = 1.0
    

    I'm not sure if it will work with DiffSharp and I don't know which type are you using from that library, I found DV multiple times.

    UPDATE2

    Regarding your follow up question for your generic transform function, using a simple member constraint will not be enough, you will also need to solve generically the conversion from a list to the destination type and also create a different generic multiplication which works generically but with the types you need to deal with. You can use overloads combined with member constraint to get the desired functionality:

    let inline item (i:int) (a: 'A) : 'a = (^A : (member get_Item: _ -> _) a, i)
    
    type T = T with
        static member ($) (T, _:Vector<float>) = fun (x:float list) -> vector x
        static member ($) (T, _:Matrix<float>) = fun (x:float list) -> matrix [x]
        static member ($) (T, _:DV           ) = fun (x: D list  ) -> toDV (List.toArray x)
    
    let inline toDestType (x:'t list) :'D = (T $ Unchecked.defaultof<'D>) x
    
    type V = V with
        static member ($) (V, x:float        ) = fun (y: float) -> x * y : float
        static member ($) (V, x:D            ) = fun (y: float) -> x * y : D
    
    let inline mult (y:float) (x:'t)  :'t = (V $ x) y
    
    let inline transform (a:'T) :'T =
        let x1, y1 = mult l1 (cos (item 0 a)), mult l1 (sin (item 0 a))
        let x2, y2 = x1 + mult l2 (cos ((item 0 a) + (item 1 a))), y1 + mult l2 (sin ((item 0 a) + (item 1 a)))
        let g = toDestType [x1; y1; x2; y2]
        g 
    
    let b = transform  (DV [| 1. ;  2.|])
    let a = transform  (vector [1. ; 2.])
    

    I still get a runtime error each time I reference DiffSharp, however intellisense shows the right types inferred.