Again on closures and type instability

Consider the simple functions:

inner(x,a) = a*x
outer(f,x) = f(x)

If one calls outer using a closure and global, non-constant parameter a , we have a type-instability:

julia> a = 5; x = 2;

julia> @code_warntype outer( x -> inner(x,a), x)
Variables
  #self#::Core.Compiler.Const(outer, false)
  f::Core.Compiler.Const(var"#5#6"(), false)
  x::Int64

Body::Any
1 ─ %1 = (f)(x)::Any
└──      return %1

This is solved by declaring a constant or wrapping the call to outer into a function which, by itself, receives a as a parameter, i. e. f(x,a) = outer(x->inner(x,a),x)).

Yet, from a non-specialist point of view, these alternatives should not be necessary if the parsing of outer(x->inner(x,a),x) translated directly to that, or to something like:

function outer2(a,x)
  clos = x -> inner(a,x)
  outer(clos,x)
end

That seems to be possible, since the anonymous function defined as a parameter cannot change, that is, it seems to be as constant as any other data passed to the outer function which are constant from the point of view of the function even if they are non-constant in the global scope. The function then would specialize to the input parameters of inner as well.

Is there a fundamental reason for that not being possible (or simple?)

The x -> inner(x,a) get evaluated (in the scope containing f) before it gets passed into outer. That is always the case, e.g.:

julia> a,b = 1,2
(1, 2)

julia> f(x) = x
f (generic function with 1 method)

julia> f(a + b)
3

there a+b gets evaluated first and then it’s passed to f. The same occurs in the case of the anonymous function and thus it binds a non-const global. This cannot be changed willy-nilly.

4 Likes

Any idea if that is because of a technical difficulty, or if that behavior is desirable for some reason, or if it is because it would brake anything?

Edit: Now I see that your example, with two parameters, indicated that changing that would brake a lot of things… A very specific parsing of the arguments and of the closure would be needed.

Edit2: My “solution” button disappeared :grimacing:

I think in certain categories there are no “solutions”. Not sure though.

1 Like

I am writing some of the things I learn here in this docs and sharing with some students, and I have written a section on closures now, incorporating what I have learnt here:

The section on closures of the manual (https://docs.julialang.org/en/v1/devdocs/functions/#Closures) is very succinct, so this may be useful for others. If someone happens to read that and finds errors, please let me know.

2 Likes

There’s nothing really particular to closures here. In your example a is a global. Don’t use non-constant globals if you care about performance.

julia> a = 5; x = 2;
julia> @code_warntype outer( x -> inner(x,a), x)

If you do this in a local scope, all is well:

julia> inner(x,a) = a*x
inner (generic function with 1 method)

julia> outer(f,x) = f(x)
outer (generic function with 1 method)

julia> function doit()
           a = 5; x = 2;
           return outer( x -> inner(x,a), x)
       end
doit (generic function with 1 method)

julia> doit()
10

julia> @code_warntype doit()
Variables
  #self#::Core.Compiler.Const(doit, false)
  #1::var"#1#2"{Int64}
  a::Int64
  x::Int64

Body::Int64

(edit: I also just enabled solutions on this category)

4 Likes

It was not completely clear to me the scope in which the closure, inside a function call, could be evaluated. The previous answer made that clear to me.