How this type annotation works, and why the other one does not?
Asked Answered
K

1

7

Please explain the magic behind drawShape function. 1) Why it works at all -- I mean how it calls the Draw member, 2) why it needs to be inline?

type Triangle() =
    member x.Draw() = printfn "Drawing triangle"

type Rectangle() =
    member x.Draw() = printfn "Drawing rectangle"

let inline drawShape (shape : ^a) =
    (^a : (member Draw : unit->unit) shape)

let triangle = Triangle()
let rect = Rectangle()

drawShape triangle
drawShape rect

And the next issue is -- is it possible to write drawShape function using parameter type annotation like below? I found that it has exactly the same signature as the first one, but I'm unable to complete the body.

let inline drawShape2 (shape : ^a when ^a : (member Draw : unit->unit)) =
    ...

Thanks in advance.

Kibbutz answered 18/5, 2015 at 13:7 Comment(5)
the magic is all in the F# compiler - don't know what more to say - for your second issue: without seeing the body/error you have it's quite hard to tellPeptize
I tried just put shape.Draw() in the body without success. The error is: 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.Kibbutz
ah I see - well sadly you have to repeat the same thing you did in drawShap yet again (the part inside the (^a : (member Draw : ...) shape)Peptize
just a comment on this: IMHO you should really only use this feature when you absolutely have to - here it would be much easier to have a IDrawable interface with the abstract Draw method instead.Peptize
This is exactly what I'm trying to avoid -- OO inspired explicit interfaces. ;-)Kibbutz
A
13

This Voodoo-looking syntax is called "statically resolved type parameter". The idea is to ask the compiler to check that the type passed as generic argument has certain members on it (in your example - Draw).

Since CLR does not support such checks, they have to be done at compile time, which the F# compiler is happy to do for you, but it also comes with a price: because there is no CLR support, there is no way to compile such function to IL, which means that it has to be "duplicated" every time it's used with a new generic argument (this technique is also sometimes known as "monomorphisation"), and that's what the inline keyword is for.

As for the calling syntax: for some reason, just declaring the constraint on the parameter itself doesn't cut it. You need to declare it every time you actually reference the member:

// Error: "x" is unknown
let inline f (a: ^a when ^a: (member x: unit -> string)) = a.x() 

// Compiles fine
let inline f a = (^a: (member x: unit -> string)( a )) 

// Have to jump through the same hoop for every call
let inline f (a: ^a) (b: ^a) = 
  let x = (^a: (member x: unit -> string)( a ))
  let y = (^a: (member x: unit -> string)( b ))
  x+y

// But can wrap it up if it becomes too messy
let inline f (a: ^a) (b: ^a) = 
  let callX t = (^a: (member x: unit -> string) t)
  (callX a) + (callX b)

// This constraint also implicitly carries over to anybody calling your function:
> let inline g x y = (f x y) + (f y x)
val inline g : x: ^a -> y: ^a -> string when  ^a : (member x :  ^a -> string)

// But only if those functions are also inline:
> let g x y = (f x y) + (f y x)
Script.fsx(49,14): error FS0332: Could not resolve the ambiguity inherent in the use of the operator 'x' at or near this program point. Consider using type annotations to resolve the ambiguity.
Avid answered 18/5, 2015 at 13:53 Comment(2)
Thank you for great explanation and the links. So, concluding, this (^a: (member x: unit -> string)( a )) roughly means "make sure that type ^a contains member x which is a parameterless function returning string, and call it on instance a with no arguments.Kibbutz
regarding member constraints see also: #4695133Anselm

© 2022 - 2024 — McMap. All rights reserved.