Fable: Multi-arity function generates wrong JS code

Created on 3 Oct 2020  路  4Comments  路  Source: fable-compiler/Fable

I've had issues with this in the past when writing bindings and we've traditionally solved it by using System.Func<...>, but in this case it's not looking like a solution.

The issue where this comes into play is I wrote bindings for the Javascript Proxy api, and it works correctly when the function I proxy is single arity:

Jest.test("Can intercept a single artity function", fun () ->
    let addOne (x: int) = x + 1

    let proxy =
        JSe.ProxyHandler<int -> int>()
        |> JSe.ProxyHandler.setApply (fun f _ args -> JSe.apply f args |> (+) 1)
        |> JSe.Proxy.create addOne

    Jest.expect(addOne 1).toBe(2)
    Jest.expect(proxy 2).toBe(4)
)

Which is generated as this:

test("Can intercept a single artity function", function () {
  const addOne = function addOne(x$$1) {
    return x$$1 + 1;
  };

  let proxy;
  let arg10$$4;
  const ph = Object.create(null);

  ph.apply = function (pFun, this$0027, args$$1) {
    let value;
    const y$$1 = pFun.apply(this, args$$1) | 0;
    value = 1 + y$$1;
    return value;
  };

  arg10$$4 = ph;
  proxy = new Proxy(addOne, arg10$$4);
  expect(addOne(1)).toBe(2);
  expect(proxy(2)).toBe(4);
});

The issue arises with the next test case:

Jest.test("Can intercept a function", fun () ->
    let add (x: int) (y: int) = x + y

    let proxy =
        JSe.ProxyHandler<int -> int -> int>()
        |> JSe.ProxyHandler.setApply (fun f _ args -> JSe.apply f args |> (+) 1)
        |> JSe.Proxy.create add

    Jest.expect(add 1 2).toBe(3)
    Jest.expect(proxy 1 2).toBe(4)
)

Which generates this:

test("Can intercept a function", function () {
  const add$$1 = function add$$1(x$$3, y$$2) {
    return x$$3 + y$$2;
  };

  let proxy$$1;
  let arg10$$5;
  const ph$$2 = Object.create(null);

  ph$$2.apply = function (pFun$$1, this$0027$$1, args$$3) {
    let value$$1;
    const y$$3 = (0, _Util.curry)(2, pFun$$1).apply(this, args$$3) | 0;
    value$$1 = 1 + y$$3;
    return value$$1;
  };

  arg10$$5 = ph$$2;
  proxy$$1 = new Proxy((0, _Util.curry)(2, add$$1), arg10$$5);
  expect(add$$1(1, 2)).toBe(3);
  expect(proxy$$1(1)(2)).toBe(4);
});

The problematic section is proxy$$1(1)(2) where it should be proxy$$1(1, 2).

The actual binding returns a type alias of the input function: Proxy<'T> which is defined as type Proxy<'T> = 'T. I've also tried just returning 'T but it still does this odd method of trying to apply the arguments to the function.

You can clone the repo and run the tests if you want to reproduce.

Most helpful comment

Whoa boy was that difficult to solve, but I managed to figure it out once you pointed me on the right track! I'm not sure if this is intended, but it was next to impossible to call myFunction.length and get the correct value.

I ended up having to parse the GetType().FullName string and split on , to get the actual arity.

The problem was also twofold, both things in the Proxy.create method needed to be changed as well as creating a applyCurried function helper.

This was the solution in case you're curious:

[<Emit("$1.reduce((p, c) => p(c), $0)")>]
let applyCurried<'T,'U> (f: 'T) (arguments: obj [])  : 'U = jsNative
[<EditorBrowsable(EditorBrowsableState.Never)>]
let dynamicProxy<'T,'V when 'T : not struct> (target: 'T) (ph: obj) (targetFullName: string) (executor: obj -> obj -> obj) : 'V =
    match JSe.typeof target with
    | JSe.Types.Function ->
        match targetFullName.Split([|','|]).Length - 1 with
        | 1 -> executor target ph
        | 2 ->
            let execFunc : System.Func<obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj> (unbox target)) ph
            unbox (fun a b -> execFunc.Invoke(a, b))
        | 3 ->
            let execFunc : System.Func<obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c -> execFunc.Invoke(a, b, c))
        | 4 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d -> execFunc.Invoke(a, b, c, d))
        | 5 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d e -> execFunc.Invoke(a, b, c, d, e))
        | 6 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d e f -> execFunc.Invoke(a, b, c, d, e, f))
        | 7 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d e f g -> execFunc.Invoke(a, b, c, d, e, f, g))
        | 8 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d e f g h -> execFunc.Invoke(a, b, c, d, e, f, g, h))
        | _ -> failwith "Currying to more than 8-arity is not supported for proxies."
    | _ -> executor target ph
    |> unbox<'V>
[<Erase>]
type Proxy =
    [<EditorBrowsable(EditorBrowsableState.Never)>]
    [<Emit("new Proxy($0, $1)")>]
    static member createInternal<'T when 'T : not struct> (target: 'T) (ph: ProxyHandler<'T>) : Proxy<'T> = target

    /// Creates a proxy from the specified handler.
    static member inline create<'T when 'T : not struct> (target: 'T) (ph: ProxyHandler<'T>) : Proxy<'T> =
        dynamicProxy<'T,Proxy<'T>> target ph (target.GetType().FullName) (unbox Proxy.createInternal)

Ths end result is pretty nice though!

Jest.test("Can intercept a function", fun () ->
    let add (x: int) (y: int) = x + y

    let proxy =
        JSe.ProxyHandler<int -> int -> int>()
        |> JSe.ProxyHandler.setApply (fun f _ args -> JSe.applyCurried f args |> (+) 1)
        |> JSe.Proxy.create add

    Jest.expect(add 1 2).toBe(3)
    Jest.expect(proxy 1 2).toBe(4)
)

Jest.test("Can intercept a many arity function", fun () ->
    let addMany (a: int) (b: int) (c: int) (d: int) (e: int) = a + b + c + d + e

    let proxy =
        JSe.ProxyHandler<int -> int -> int -> int -> int -> int>()
        |> JSe.ProxyHandler.setApply (fun f _ args -> JSe.applyCurried f args |> (+) 1)
        |> JSe.Proxy.create addMany

    Jest.expect(addMany 1 2 3 4 5).toBe(15)
    Jest.expect(proxy 1 2 3 4 5).toBe(16)
)

All 4 comments

This is gonna be tricky. Actually this is correct behavior by Fable but I understand it produces unexpected results here.

Uncurrying in Fable happens mainly when passing functions as arguments, in these cases Fable doesn't use the actual arity of the function but the expected one by the argument type in the signature. Why is that? Consider the following example:

[|"a"; "b"; "c"|] |> Array.mapi (fun i x y -> string i + x + y)

This produces a list of string -> string functions and gets translated to something like:

mapIndexed(((i, x) => (y) => int32ToString(i) + x + y), ["a", "b", "c"]);

If we create a function with 3 arguments instead, mapIndexed will fail because it's expecting a function with 2-arity.

The tricky part in your code is the argument of JSe.Proxy.create is just a generic 'T which means Fable won't try to uncurry functions passed as argument (in fact, in the code above it's _recurrying_ them). "Fixing" this behavior would be complicated and I'm not sure we want to do that. I know APIs with delegates are not nice, but I would do recommend to use them in this case:

type Proxy =
    static member Func(f: Func<'T1, 'TOut>): Func<'T1, 'TOut> = jsNative // Func<'T1, 'TOut> = jsNative
    static member Func(f: Func<'T1, 'T2, 'TOut>): Func<'T1, 'T2, 'TOut> = jsNative // Func<'T1, 'T2, 'TOut> = jsNative

let test() =
    let p1 = Proxy.Func(fun x -> x * x)
    let p2 = Proxy.Func(fun x y -> x * y)
    p1.Invoke(4) + p2.Invoke(4, 5)

Ah, look at that it works!

Jest.test("Can intercept a function", fun () ->
    let add (x: int) (y: int) = x + y

    let proxy =
        JSe.ProxyHandler<System.Func<int,int,int>>()
        |> JSe.ProxyHandler.setApply (fun f _ args -> JSe.apply f args |> (+) 1)
        |> JSe.Proxy.funcTest (System.Func<int,int,int> add)

    Jest.expect(add 1 2).toBe(3)
    Jest.expect(proxy.Invoke(1, 2)).toBe(4)
)

A fair bit more ugly, but it's something I think I can work with.

Side note: I think this repo might be a another decent place to submit PRs against with Fable 3 alpha builds. I do some wonky stuff to ensure type safety that may help find any more potential bugs. It doesn't have hundreds of tests like Fable.Jester but it's a good 70 or so.

Whoa boy was that difficult to solve, but I managed to figure it out once you pointed me on the right track! I'm not sure if this is intended, but it was next to impossible to call myFunction.length and get the correct value.

I ended up having to parse the GetType().FullName string and split on , to get the actual arity.

The problem was also twofold, both things in the Proxy.create method needed to be changed as well as creating a applyCurried function helper.

This was the solution in case you're curious:

[<Emit("$1.reduce((p, c) => p(c), $0)")>]
let applyCurried<'T,'U> (f: 'T) (arguments: obj [])  : 'U = jsNative
[<EditorBrowsable(EditorBrowsableState.Never)>]
let dynamicProxy<'T,'V when 'T : not struct> (target: 'T) (ph: obj) (targetFullName: string) (executor: obj -> obj -> obj) : 'V =
    match JSe.typeof target with
    | JSe.Types.Function ->
        match targetFullName.Split([|','|]).Length - 1 with
        | 1 -> executor target ph
        | 2 ->
            let execFunc : System.Func<obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj> (unbox target)) ph
            unbox (fun a b -> execFunc.Invoke(a, b))
        | 3 ->
            let execFunc : System.Func<obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c -> execFunc.Invoke(a, b, c))
        | 4 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d -> execFunc.Invoke(a, b, c, d))
        | 5 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d e -> execFunc.Invoke(a, b, c, d, e))
        | 6 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d e f -> execFunc.Invoke(a, b, c, d, e, f))
        | 7 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d e f g -> execFunc.Invoke(a, b, c, d, e, f, g))
        | 8 ->
            let execFunc : System.Func<obj,obj,obj,obj,obj,obj,obj,obj,obj> = unbox <| executor (System.Func<obj,obj,obj,obj,obj,obj,obj,obj,obj> (unbox target)) ph
            unbox (fun a b c d e f g h -> execFunc.Invoke(a, b, c, d, e, f, g, h))
        | _ -> failwith "Currying to more than 8-arity is not supported for proxies."
    | _ -> executor target ph
    |> unbox<'V>
[<Erase>]
type Proxy =
    [<EditorBrowsable(EditorBrowsableState.Never)>]
    [<Emit("new Proxy($0, $1)")>]
    static member createInternal<'T when 'T : not struct> (target: 'T) (ph: ProxyHandler<'T>) : Proxy<'T> = target

    /// Creates a proxy from the specified handler.
    static member inline create<'T when 'T : not struct> (target: 'T) (ph: ProxyHandler<'T>) : Proxy<'T> =
        dynamicProxy<'T,Proxy<'T>> target ph (target.GetType().FullName) (unbox Proxy.createInternal)

Ths end result is pretty nice though!

Jest.test("Can intercept a function", fun () ->
    let add (x: int) (y: int) = x + y

    let proxy =
        JSe.ProxyHandler<int -> int -> int>()
        |> JSe.ProxyHandler.setApply (fun f _ args -> JSe.applyCurried f args |> (+) 1)
        |> JSe.Proxy.create add

    Jest.expect(add 1 2).toBe(3)
    Jest.expect(proxy 1 2).toBe(4)
)

Jest.test("Can intercept a many arity function", fun () ->
    let addMany (a: int) (b: int) (c: int) (d: int) (e: int) = a + b + c + d + e

    let proxy =
        JSe.ProxyHandler<int -> int -> int -> int -> int -> int>()
        |> JSe.ProxyHandler.setApply (fun f _ args -> JSe.applyCurried f args |> (+) 1)
        |> JSe.Proxy.create addMany

    Jest.expect(addMany 1 2 3 4 5).toBe(15)
    Jest.expect(proxy 1 2 3 4 5).toBe(16)
)

Yes, sorry, the uncurrying mechanism in Fable is a bit complicated. The .length property may give false results because when the (un)currying cannot be done at compile time, the function will be wrapped.

In any case, it's great you found a workaround! :clap: Please let me know if I can help with something else.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

forki picture forki  路  3Comments

jwosty picture jwosty  路  3Comments

MangelMaxime picture MangelMaxime  路  3Comments

forki picture forki  路  3Comments

nozzlegear picture nozzlegear  路  3Comments