mardi 13 août 2019

Can F# functions be specialized at runtime?

Let's assume I have an F# function that's supposed to run both locally and remotely. I want to create a proxy of the function, and let that proxy decide where to run the function, so the remote call is completely invisible to the outside world. If it's going to run locally, the proxy would just return the function itself, like so:

let proxy_local f = f

If not, the function must collect all its arguments, then send them over a connection and get the result back. This is where it gets hard: I need to know exactly which function the user is hoping to proxy, and also to know the arguments to that function so I can collect them before sending them over the wire.

AFAIK I can't inspect the function argument itself, since at runtime it'll be some unknown subclass of FSharpFunc which collects the arguments and invokes the function with them. To work around this, I think I need to use a quotation (BTW, is there a better way to do this part?):

let getMethodInfo = function
    | Call (_, mi, _) -> [], mi
    | Lambdas (vs, Call(_, mi, _)) -> List.map (fun (v: Var list) -> (List.head v).Type) vs, mi
    | _ -> failwith "Not a function"

let proxy_remote (f: Expr) =
    let (argTypes, methodInfo) = getMethodInfo f
    … ? // What to return?

let proxy f = if isLocal then proxy_local f else proxy_remote f

getMethodInfo above won't work for methods with tuple arguments, and we'd need to modify proxy_local as well, but let's leave that aside for now. The problem is the return value of proxy_remote. It should be a function with the same arguments as the original, but it should send them the over the wire at the end. Something like this:

let callRemote (method: MethodInfo) a b c … = doHttpConnection()

But, the arguments need to be typed. This is the real problem: since F# function calls are translated into FSharpFunc subclasses at compile time, there's no way that I know of to get an unspecialized representation that'll work with reflection. My real question is this: can F# functions be specialized at runtime with unknown types?

I can think of two ways to solve this. The first would be to have proxy be generic itself:

let call1<'p, 'res> (method: MethodInfo) (p: 'p) = method.Invoke(null, [| p :> obj |]) :?> 'res
let call2<'p1, 'p2, 'res> (method: MethodInfo) (p1: 'p1) (p2: 'p2) = method.Invoke(null, [| p1 :> obj; p2 :> obj |]) :?> 'res
…

let proxy1 (f: Expr<'a -> 'b>) (s: string) : 'a -> 'b =
    let (types, mi) = getMethodInfo f
    match types with
    | [_] -> call1<'a, 'b> mi
    | _ -> failwith ""

let proxy2 (f: Expr<'a -> 'b -> 'c>) : 'a -> 'b -> 'c =
    let (types, mi) = getMethodInfo f
    match types with
    | [_; _] -> call2<'a, 'b, 'c> mi
    | _ -> failwith ""
…

This would certainly work, but it requires the programmer to think of the number of inputs to each function in advance. Worse, a function with more parameters will work with all proxy methods that accept less arguments:

let f a b = a + b
let fproxy = proxy1 f // The result will be of type int -> (int -> int), not what we want at all!

The other way is to create special subclasses of FSharpFunc for this purpose:

type call1<'a, 'res>(id, ps) =
    inherit FSharpFunc<'a, 'res>()
    override __.Invoke(x: 'a) = callImpl<'res> id ((x :> obj) :: ps)

type call2<'a, 'b, 'res>(id, ps) =
    inherit FSharpFunc<'a, FSharpFunc<'b, 'res>>()
    override __.Invoke(x) = call1<'b, 'res>(id, x :> obj :: ps) :> obj :?> FSharpFunc<'b, 'res>

… 

let proxy (f: Expr<'a -> 'b>) : 'a -> 'b =
    let (types, methodInfo) = getMethodInfo f
    match types with
    | [a] ->
        let t = typedefof<call1<_,_>>.MakeGenericType([| a; methodInfo.ReturnType |])
        t.GetConstructors().[0].Invoke([|methodInfo; []|]) :?> ('a -> 'b)
    | [a; b] ->
        let t = typedefof<call2<_,_,_>>.MakeGenericType([| a; b; methodInfo.ReturnType |])
        t.GetConstructors().[0].Invoke([|methodInfo; []|]) :?> ('a -> 'b)
    … 
    | _ -> failwith ""


It'll work, but it probably won't be as performant as what the F# compiler generates, specially with all the dynamic casts. So, again, is there a way to specialize F# functions at runtime, so typedefof<call1<_,_>>.MakeGenericType([| a; methodInfo.ReturnType |]) can be replaced with a direct call to the function?





Aucun commentaire:

Enregistrer un commentaire