LegibleLambas.jl is a package for making anonymous functions whose names are their expressions, ie.
julia> f = @λ(x -> x + 1)
(x -> x + 1)
julia> f(1.0)
2.0
The primary use-case I see for this functionality is for functions which return functions (closures). Suppose I have a function D which operates on a function and gives it’s (finite difference) derivative, I can use LegibleLambdas
D(f, ϵ=1e-10) = @λ(x -> (f(x+ϵ)-f(x))/ϵ)
so that when a user wants to know what D(sin) is, they are shown
Will it be possible to have this feature directly in julia? So short lambda’s printing will be something like LegibleLambdas by default. We do get nothing from current printings…
I should have mentioned in the original post that this would probably have just rotted away on my list of abandoned repositories if you hadn’t come along with your PR to overhaul the internals and fix a few problems I was unable to figure out, so thank you Roger!
Just an update, it had been bothering me in the back of my head for some time now that LegibleLambdas was producing slow closures. I’ve now updated it to version 0.3.0 where closures do not impose a runtime performance overhead! I’ve also lightened the dependancies.
julia> using LegibleLambdas
julia> f(x) = @λ y -> x + y;
julia> f(1)
(y -> 1 + y)
julia> f(2)
(y -> 2 + y)
julia> let x = Ref(1), y = Ref(2)
@btime f($x[])($y[])
end;
1.299 ns (0 allocations: 0 bytes)
What was the performance culprit behind the slow closures? I have some vague memory that there’s a famous issue with closure performance, is that this?
The culprit was relying on Base.@locals to fetch variable names and values which is slow and won’t be optimized away by the compiler. What I realized recently was that closures actually store the names and values of the bound variables in themselves, so there was no need for Base.@locals.
Here’s a minimal working example of the problem:
julia> f(x) = (Base.@locals(), y -> x + y)
f (generic function with 1 method)
julia> let x = Ref(1), y = Ref(2)
@btime f($x[])[2]($y[])
end
103.571 ns (4 allocations: 608 bytes)
3
That’s a separate issue. LegibleLambdas will suffer from that issue to the same extent that regular illegible closures do. Here’s an example:
julia> using LegibleLambdas
julia> f(x) = y -> begin
x = x + 1.0
x + y
end
f (generic function with 1 method)
julia> g(x) = @λ y -> begin
x = x + 1.0
x + y
end
g (generic function with 1 method)
julia> let x = Ref(1), y = Ref(2)
@btime f($x[])($y[])
@btime g($x[])($y[])
end
48.969 ns (3 allocations: 48 bytes)
48.582 ns (3 allocations: 48 bytes)
4.0
One thing that’s nice though about LegibleLambdas is that it can help diagnose this problem:
julia> g(1)
(y -> begin
Core.Box(1) = Core.Box(1) + 1.0
Core.Box(1) + y
end)
Though, the ‘legible’ printing leaves something to be desired here. I basically just walked through the syntax tree and replaced x with 1, so in the presence of rebindings and such, it’s kinda nonsense. Where is says Core.Box(1), it’s really Core.Box(x).