One of the features I miss from Scheme is the “named let.” It’s a let expression that allows its own body to invoke itself recursively, by giving it a name that can be used to run another instance of the body on the call stack, with new values for the variables.
For example, the following Scheme code defines the variable fib10 (the 10th Fibonacci number) by using a named let that starts with n=10 and will call itself recursively until the computation is done:
(define fib10
(let loop ((n 10))
(if (<= n 1)
1
(+ (loop (- n 1))
(loop (- n 2))))))
The most straightforward translation to Julia would be a recursive function defined inside a local scope and invoked rightaway:
fib10 = let
function loop(n = 10)
if n ≤ 1
1
else
loop(n-1) + loop(n-2)
end
end
loop()
end
But I think that the named let could be added to Julia as a let statement where the variable initialization section is given a name, in the style of function parameters, that can then be invoked from within the body:
# proposed "named let" syntax:
fib10 = let loop(n = 10)
if n ≤ 1
1
else
loop(n-1) + loop(n-2)
end
end
This syntax is currently rejected by the compiler, so it would not break any existing code.
I, for one, would use it very often, namely every time I need to express a computation structure that is branching, or is more complex than a simple “while” or “for” loop.
What do you think?
PS. I’ll probably make my own macro that approximates this syntax, but I think it’s general and useful enough to propose for inclusion in the language.
PS. I’ll probably make my own macro that approximates this syntax, but I think it’s general and useful enough to propose for inclusion in the language.
This is the right idea. Given that some schemes already implement let as a macro on top of lambda, I’d suggest doing the same in Julia
From how you’re describing it, the named let really does sound like a closure in Julia and your macro seems sound. I don’t know Scheme so I don’t know what its distinction between named lets and nested functions are (which exist as far as I can Google), but if there is one besides calling it right away, then that’d be an issue.
Recursive closures currently have type inference issues though because whether the captured variable has an inferrable type or an uninferrable Core.Box is currently determined at parsing, far before any type inference in a JIT-compiled call. There have been discourse musings on how it could be improved, but AFAIK there hasn’t been work done on it. A simple demo with your function:
julia> function loop(n = 10)
if n ≤ 1
1
else
loop(n-1) + loop(n-2)
end
end
loop (generic function with 2 methods)
julia> @code_warntype loop()
MethodInstance for loop()
from loop() @ Main REPL[1]:1
Arguments
#self#::Core.Const(loop)
Body::Int64
1 ─ %1 = (#self#)(10)::Int64
└── return %1
julia> letloop = let
function loop(n = 10)
if n ≤ 1
1
else
loop(n-1) + loop(n-2)
end
end
end
(::var"#loop#1") (generic function with 2 methods)
julia> @code_warntype letloop()
MethodInstance for (::var"#loop#1")()
from (::var"#loop#1")() @ Main REPL[3]:2
Arguments
#self#::var"#loop#1"
Body::Any
1 ─ %1 = (#self#)(10)::Any
└── return %1
Possible workaround is to evaluate the recursive function but with a generated name to the global scope, which closures do now anyway, but that gets in the way of capturing other variables.
I decided to use the regular function syntax for my macro argument, otherwise I would have to parse a name, a list of variable definitions, a body, and turn it all back into a function anyways.
So I came up with this:
macro call(f)
if !isa(f, Expr) || f.head != :function
error("Usage: @call function(...) ... end")
end
fName = f.args[1].args[1]
:(let
$f
$fName()
end)
end
Example usage:
fib10b = @call function loop(n = 10)
if n ≤ 1
1
else
loop(n-1) + loop(n-2)
end
end
Still, if others find it useful, it could be considered as an addition to the language, with the named let syntax in the OP.
Can be immediately available as a package, and this is what the successful syntax extensions do. The trend is toward separation into optionally loaded code, even trimming the sysimage.
Yes, I realized right after typing it. I can just use this:
fib10 = (function loop(n = 10)
if n ≤ 1
1
else
loop(n-1) + loop(n-2)
end
end)()
So unless one of the core developers is also a fan of Scheme’s named let, and decides to add something like let loop(n = 10) ... end to the core language, I’ll just use this version.
Note that let introduces a new scope, but parentheses don’t. So loop would remain in scope after calling it. That’s probably fine in most cases, just something to keep in mind.