Search code examples
haxe

Why can't you modify closure parameters of inline methods?


I've got this section of code:

class Main {
    static inline function difference(a:Int, b:Int, ?f:(Int, Int) -> Int):Int {
        if (f == null) {
            f = (a, b) -> a - b;
        }
        return f(a, b);
    }
    
    static function main() {
        trace(difference(42, 37));
        trace(difference(42, 37, (a, b) -> a - b));
    }
}

Which, when I compile using haxe --main Main, fails with this error:

Main.hx:11: characters 15-50 : Cannot modify a closure parameter inside inline method
Main.hx:11: characters 15-50 : For function argument 'v'

If I change Main.difference to not be inline, this error doesn't come up and everything compiles fine.

Why does this error occur?


Edit: I've found out I can also assign the argument to a variable first, and then pass the variable to Main.difference, like this:

    static function main() {
        var f = (a, b) -> a - b;
        trace(difference(42, 37, f));
    }

Which works fine with Main.difference being inlined. How does assigning the function to a variable first change things though?


Solution

  • This is related to how inline functions are unwrapped by the compiler. Let us take a simpler variant of your code:

    class HelloWorld {
    
        static inline function difference(a:Int, b:Int, ?f:(Int, Int) -> Int):Int {
            return f(a, b);
        }
        
        static function main() {
            trace(difference(42, 37, (a, b) -> a - b));
        }
    }
    

    When disabling optimizations, this will yield the following JavaScript:

    HelloWorld.main = function() {
        console.log("HelloWorld.hx:14:",(function(a,b) {
            return a - b;
        })(42,37));
    };
    

    So the body of difference has been incorporated into main using a JavaScript closure. My best guess for what is happnening in your exact case is something like this:

    HelloWorld.main = function() {
        var v = function(a,b) {
            return a - b;
        }
        console.log("HelloWorld.hx:14:", (function(a,b) {
            if (v == null) {
                v = function(a, b) { 
                    return a - b;
                }
            }
            return v(a, b);
        })(42, 37));
    };
    

    This alters the value of v, which exists outside of difference, which has been automatically placed there as a binding for the anonymous lambda. This is what the compiler is trying to avoid. This would not be the end of the world in your case, but in general this is bad and would lead to issues in many programs.

    There is a way to inline this code perfectly by hand without this, but I think that there is some weirdness surrounding how annonymous lambdas are currently handled. The situation may improve in the future.

    When you explicitly defined f in main, the compiler is intelligent enough to rename the nested f as f1, which is why the issue does not occur:

    HelloWorld.main = function() {
        var f = function(a,b) {
            return a - b;
        };
        var f1 = f;
        if(f1 == null) {
            f1 = function(a,b) {
                return a - b;
            };
        }
        console.log("HelloWorld.hx:14:",f1(42,37));
    };
    

    But this would also work if the inline part of this function is important to you:

    class HelloWorld {
    
        static inline function difference(a:Int, b:Int, ?f:(Int, Int) -> Int):Int {
            var h = f;
            if (h == null) {
                h = (a, b) -> a - b;
            } 
            return h(a, b);
        }
        
        static function main() {
            trace(difference(42, 37, (a, b) -> a - b));
        }
    }