python: passing function reference introduces unnecessary closure
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
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.
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.