Overloading anonymous functions

I was playing around with anonymous functions a bit and found that you can use where clauses with them, if you wrap the header in brackets:

julia> ((x::T) where {T<:Int}) -> x
#11 (generic function with 1 method)

julia> ans(5)
5

This clearly means that anonymous functions participate like “normal” functions in multiple dispatch. What I’m wondering then is whether it’s possible to add methods to this same anonymous function. It seems like technically it would be possible, but is there syntax for it?

2 Likes

The below works:

using Test
anon = x::Int  -> x^2 
(f::typeof(anon))(x::Float64) = x *2.  
@test anon(3) == 9 
@test anon(3.) == 6.0

Although why use anonymous function if you are going to end up writing the second line? By writing a second line, you are no longer achieving brevity by using anonymous functions. And by assigning the anonymous function to a variable, you are already polluting the namespace.

5 Likes

You can use the typeof trick for conveniently dispatching in other weird contexts as well.

NamedTuples are basically anonymous structs, and sometimes it can be convenient to specialize a function on them. For example:

coef_hat = (intercept = 3.0, educ = .03, educ2 = -.002, age = .02) 
(p::typeof(coef_hat))(df::DataFrame) = @. p.intercept + df[:, educ] * p.educ + (df[:, educ]^2) *  p.educ + df[:, :age]*p.age

using ‘typeof’ is more convenient and robust than determining and typing out the type signature of the NamedTuple.

Thanks, that’s neat!

The why wasn’t really part of the question, I agree it’s questionable to use this. But I can adapt your example like this:

julia> begin 
         local anon = x::Int -> x^2
         (::typeof(anon))(x::Float64) = x * 2.
         anon
       end
#2 (generic function with 2 methods)

julia> anon
ERROR: UndefVarError: `anon` not defined in `Main`
Suggestion: check for spelling errors or missing imports.

In which case I can define the anonymous function without polluting the namespace, so that’s nice. But yes, it stops being very concise at this point.

1 Like

That makes sense. Cool!

1 Like

Seems that the standard way to define functions also works for local definitions:

julia> begin
           local anon2(x::Int) = x^2
           anon2(x::Float64) = 2.0 * x
           anon2
       end
(::var"#anon2#5") (generic function with 2 methods)

julia> begin
           local function anon2(x::Int); x^2 end
           function anon2(x::Float64); 2.0 * x end
           anon2
       end
(::var"#anon2#6") (generic function with 2 methods)

julia> anon2
ERROR: UndefVarError: `anon2` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
1 Like

The function is there, but it seems the methods are local:

julia> begin
           local function anon2(x::Int); x^2 end
           function anon2(x::Float64); 2.0 * x end
           anon2
       end
(::var"#anon2#1") (generic function with 2 methods)

julia> dump(var"#anon2#1")
var"#anon2#1" <: Function

julia> methods(var"#anon2#1")
# 0 methods for type constructor

Interesting, seems to be the same when using an anonymous function though:

julia> begin 
           local anon = x::Int -> x^2
           (::typeof(anon))(x::Float64) = x * 2.
           @show typeof(anon)
           anon
       end
              
typeof(anon) = var"#7#8"
#7 (generic function with 2 methods)

julia> dump(var"#7#8")
var"#7#8" <: Function

julia> methods(var"#7#8")
# 0 methods for type constructor

Does that mean that every function creates a global struct that is never garbage collected?

Yes, structs are global. The type definition doesn’t take up much memory:

julia> dump(Base.DataType)
mutable struct DataType <: Type{T}
  const name::Core.TypeName
  const super::DataType
  const parameters::Core.SimpleVector
  types::Core.SimpleVector
  const instance::Any
  layout::Ptr{Nothing}
  const hash::Int32
  flags::UInt16

julia> dump(Base.DataTypeLayout)
struct Base.DataTypeLayout <: Any
  size::UInt32
  nfields::UInt32
  npointers::UInt32
  firstptr::Int32
  alignment::UInt16
  flags::UInt16

And then a bit more, for methods etc., but not much. And you typically don’t make that many functions. Even if you do things like this:

for i in 1:10^8
   sum(i -> i^2, 1:1000)
end

Only a single struct is created for the function i -> i^2.

Actually, everything is there:

julia> f() = (show(() -> "fun 42"); nothing)
f (generic function with 1 method)

julia> a = f()
var"#f##12#f##13"()
julia> typeof(a)
Nothing

julia> var"#f##12#f##13".instance()
"fun 42"