ANN LegibleLambdas.jl

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

julia> D(sin)
(x -> (sin(x + 1e-10) - sin(x)) / 1e-10)

instead of something like

julia> D(sin)
#1 (generic function with 1 method)
17 Likes

It would be nice to do something like this for display of short functions in base; see also

https://github.com/JuliaLang/julia/issues/24347

6 Likes

Nice work! I’m already using this in our next release of Yao.jl!

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…

2 Likes

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!

1 Like

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)
julia> D1(f, ϵ=1e-10) = @λ(x -> (f(x+ϵ) - f(x))/ϵ);

julia> D2(f, ϵ=1e-10) =    x -> (f(x+ϵ) - f(x))/ϵ;

julia> let x = Ref(1.0)
           @btime D1(sin)($x[])
           @btime D2(sin)($x[])
       end;
  12.975 ns (0 allocations: 0 bytes)
  12.975 ns (0 allocations: 0 bytes)

julia> D1(sin)
(x -> ((sin)(x + 1.0e-10) - (sin)(x)) / 1.0e-10)

julia> D2(sin)
#14 (generic function with 1 method)

Happy λs.

9 Likes

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

1 Like

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).

2 Likes