Julia: Julep: "Named" anonymous functions in do syntax

Created on 11 May 2020  路  4Comments  路  Source: JuliaLang/julia

Makie.jl works with Observables. They are nice until you have to debug a chain of anonymous functions in a stack trace and you have no idea what any of those functions are doing, because they are all named something like ::var"#1262#1263".

Of course we could create named functions in all instances but many things don't really deserve a "real" name that then pollutes the name space. Also, the do syntax creates a single self-contained expression and is a bit easier to parse as a unit when reading than having one function definition and then a separate expression passing that to another function. I have just started writing a macro to alleviate these issues and found it quite useful. A generalized approach could be included in the base language.

The idea is to optionally give a function name in the do syntax like this:

x = Observable{Any}(1)
y = Observable(2)
z = map(x, y) do addnumbers(x, y)
    x + y
end

Instead of creating a named function addnumbers, the name is gensym'd so it doesn't pollute the name space but can still be identified easily in a stack trace. Compare an error message with and without such a "named" anonymous function:

Without:

julia> x[] = :a
ERROR: MethodError: no method matching +(::Symbol, ::Int64)
Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...) at operators.jl:529
  +(::Complex{Bool}, ::Real) at complex.jl:301
  +(::Missing, ::Number) at missing.jl:115
  ...
Stacktrace:
 [1] (::var"#1262#1263")(::Symbol, ::Int64) at ./Untitled-2:29
...more stacktrace

With:

julia> x[] = :a
ERROR: MethodError: no method matching +(::Symbol, ::Int64)
Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...) at operators.jl:529
  +(::Complex{Bool}, ::Real) at complex.jl:301
  +(::Missing, ::Number) at missing.jl:115
  ...
Stacktrace:
 [1] #1609#addnumbers(::Symbol, ::Int64) at ./Untitled-2:25
...more stacktrace

The syntax with the macro is actually z = @liftn(x, y) do addnumbers(x, y)... but that is only because I started out making a "named" version of the lift function. Here's the macro if that's of interest:

macro liftn(f, args...)

    @assert f.head == Symbol("->")
    @assert length(f.args) == 2
    funcdef, funcbody = f.args

    @assert funcdef isa Expr
    @assert funcdef.head == :tuple
    @assert length(funcdef.args) == 1

    callexpr = funcdef.args[1]

    funcexpr = Expr(:function, callexpr, funcbody)

    exp = quote
        lift($funcexpr, $(args...))
    end
end

Most helpful comment

Perhaps rather than changing the language for this, we could give the anonymous functions better names? E.g. the anonymous function argument to map could be called #map#arg or something like that. It could potentially be printed in a clearer way as well by just printing a function named #map#arg as (anonymous map argument) or something like that.

All 4 comments

Perhaps rather than changing the language for this, we could give the anonymous functions better names? E.g. the anonymous function argument to map could be called #map#arg or something like that. It could potentially be printed in a clearer way as well by just printing a function named #map#arg as (anonymous map argument) or something like that.

A language change does seem like the most drastic approach. But at least in my use case, with chained observables, naming the function #map#arg wouldn't help at all because then you'd have a chain of those names in your stack trace. I kind of like my proposal because it is more like an annotation that helps you both understand your written code and error messages, but is still very easy to use, and doesn't conflict with any working code. I guess the question is if many other people would like to annotate their anonymous functions this way or if it's mostly me. And whether the limitation to do syntax is bad thing.

The change I'm suggesting fixes stack traces for all Julia code that exists. Just adding a feature to give a name to an anonymous function for the sake of slightly improving stack traces just isn't compelling enough to add a language feature. Any new language feature starts out with -100 points and has to justify its addition by solving multiple problems and recovering those 100 points. If you really want to name your anonymous functions, you can already use named inner functions.

Good point. I guess I do not have the high level view that you do.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dpsanders picture dpsanders  路  3Comments

omus picture omus  路  3Comments

Keno picture Keno  路  3Comments

i-apellaniz picture i-apellaniz  路  3Comments

m-j-w picture m-j-w  路  3Comments