Fable icon indicating copy to clipboard operation
Fable copied to clipboard

python: passing function reference introduces unnecessary closure

Open joprice opened this issue 1 year ago • 2 comments

Description

When using the python backend and passing a function reference, it gets wrapped in an extra closure, with the outer function environment's arguments appended. This prevents passing a function to constructors like multiprocessing.Process, which require the function to be pickleable, failing with AttributeError: Can't pickle local object.

Repro code

[Please provide the F# code to reproduce the problem or a link to the REPL. Ideally, it should be possible to easily turn this code into a unit test.](https://fable.io/repl/#?code=DYUwLgBA5hAUCUEC8d4Cg2kgJwK4DsIAzZYhDLCXABwBMBDMECAfXuxmTQh70KiA&html=Q&css=Q)

Expected and actual results

When passing a function as an argument

let g () = ()

let run f = f()

let update _arg  =
   run g

the function should be called directly as in the JS output:

export function g() {
}

export function run(f) {
    return f();
}

export function update(_arg) {
    run(() => {
        g();
    });
}

Instead, a local function _arrow1 is introduced with an extra _arg: Any=_arg param:

from collections.abc import Callable
from typing import (Any, TypeVar)

__A = TypeVar("__A")

def g(__unit: None=None) -> None:
    pass


def run(f: Callable[[], __A]) -> __A:
    return f(None)


def update(_arg: Any | None=None) -> None:
    def _arrow1(__unit: None=None, _arg: Any=_arg) -> None:
        g()

    run(_arrow1)

Related information

  • Fable version: 4.19.3
  • Operating system: OSX

joprice avatar Aug 15 '24 16:08 joprice

Hey, thanks for the issue. This is unfortunately not that easy to fix. The Python version is actually doing the same thing as the JS version. The JS version also has the arrow function () => { g(); }. The reason why Python takes the extra arguments is that F# might sometimes call it with a unit () argument i.e None. This will be silently ignored by JS, but will fail with Python if we don't declare it. The reason for the _arg is that for Python we need to append the TC-arguments to any declared (arrow) function inside the while-loop of the TCO. We will set them as default values to themselves e.g i=i to capture the value and not the variable. This is not related to this arrow function at all, but to others 🙈 so again, not trivial to fix.

I see that there is a possibility of doing eta-reduction on the expression tree to remove the arrow function for this particular case, so that is something we could consider to investigate.

dbrattli avatar Apr 29 '25 20:04 dbrattli

That makes sense. I assumed it was some machinery intended for something along those lines. Could there perhaps be a less general solution that is meant only for interop cases that behaves like a typed nameof like functionRef(g), which emits a reference to the function? I

For the above use case, I ended up using a hack to interop with the multiprocessing lib:

let inline typed (_x: 'a) (name: string) = emitPyExpr<'a> () name

let fnRef = typed file_writer "MyNamespace_file_writer"

Unfortunately, the name has to be hardcoded and so it's brittle. Even a helper to get the fully-qualified python transpiled name of a symbol would be helpful. Maybe that's already possible, but I didn't find a way to do that.

joprice avatar Apr 29 '25 20:04 joprice