Performant Recursive Anonymous Functions

I don’t really care what name it receives at the global scope; within a local scope, though, I’d rather have a name better than var"#self#" to call it by. Unless it becomes officially ordained.

This is a nuisance though (trying to nest local function definitions):

julia> foo = let
           function f end
           (f::typeof(f))() = begin
               let
                   function g end
         #          (g::typeof(g))() = begin
         #              1
         #          end
               end
           end
           f
       end
(::var"#f#41") (generic function with 1 method)

julia> foo()
(::var"#g#42") (generic function with 0 methods)

uncommenting the nested method definition:

julia> foo = let
           function f end
           (f::typeof(f))() = begin
               let
                   function g end
                   (g::typeof(g))() = begin
                       1
                   end
               end
           end
           f
       end
ERROR: syntax: Global method definition around REPL[58]:6 needs to be placed at the top level, or use "eval".

This language really doesn’t want me to get my type stability :sweat_smile:

It’s looking like @aplavin and @Mason may have the correct approach yet

I guess the question I should be asking though is:

If I define a function locally using named function syntax, why isn’t there a syntax transform imposed such that any instances of it calling its own name are replaced with var"#self#", and any attempts within that scope to re-define its name will throw an error?

This is a lot of hoop-jumping just so that we can maintain the ability to reassign the locally-defined name of locally-defined named functions, which is an ability we’re happy to live without at the global scope.

So for now, I’m using @sgaure’s idea to use var"#self#". It’s straightforward in my macro to search and replace symbols of the function’s name, since I know what it is, and this approach maximizes performance. However, this is very much a temporary fix; to me it feels like this is a bug in the language.

The funny thing is, Julia’s “normal” behavior toward recursion is exactly what we want here. What we don’t want is for the function’s reference to itself to be boxed. And the only reason to box it is because the language is insisting that its identifier could be reassigned during the function’s life, despite using named function syntax.

We have four different syntaxes to declare functions: two named function syntaxes and two anonymous function syntaxes. The point of the named function syntaxes is function identifier type declaration to stabilize the type. This is convenient (don’t need to type const), great for performance, and quite useful for declaring recursive functions.

For whatever reason though, in local scopes the named function syntax seems to behave for all intents and purposes like anonymous function syntax in allowing its identifier to be reassigned. That means that within a local scope, we have loss of performance and four redundant syntaxes for doing basically the same thing. It’s also a break from the behavior that you come to expect from interacting with these syntaxes at global scope. And because recursive functions require their identifier to be declared simultaneous to their function body, we can’t use the let block trick that works for other captured variables, and we can’t use type-annotation. And because const is disallowed in local scopes, it’s inescapable.

This does not seem like the correct behavior. It seems like the right thing to do is to disallow locally declared named functions from having their identifier reassigned, so that references to them don’t have to be boxed. Someone should open an issue for this, no?

Issue submitted.