Return a function defined in a function

#1

Is anyone able to explain this behaviour?

function fx(n)
    if n == 1
        f(x) = 2x
    else
        f(x) = 3x
    end
    f
end

Seems simple enough, but…

julia> fx(1)(1)   # expecting 2
3

julia> fx(2)(1)   # expecting 3
ERROR: UndefVarError: f not defined
Stacktrace:
 [1] fx(::Int64) at .\REPL[1]:7
 [2] top-level scope at none:0

I tried a few things like declaring f as global, but I’m still stumped. A pointer to the relevant docs would be really appreciated!

#2

It’s an unfortunate and long-standing bug. See: Confused by behaviour of nested functions for more discussion.

Fortunately, it’s easy to avoid by defining an anonymous function instead:

function fx(n)
  if n == 1
    f = x -> 2x
  else
    f = x -> 3x
  end
  f
end
2 Likes
#3

if the function if not complicated, you can do the conditional first and define a general function:

function fx(n)
    if n == 1
        a = 2
    else
        a = 3
    end
    f(x) = a*x
    return f
end
1 Like
#4

This particular way of doing it is not a good idea since it generates type unstable code:

julia> @code_typed fx(1)(1)
CodeInfo(
1 ─ %1 = (Core.getfield)(#self#, :a)::Box
│   %2 = (Core.isdefined)(%1, :contents)::Bool
└──      goto #3 if not %2
2 ─      goto #4
3 ─      $(Expr(:throw_undef_if_not, :a, false))::Any
4 ┄ %6 = (Core.getfield)(%1, :contents)::Any
│   %7 = (%6 * x)::Any
└──      return %7
) => Any

Compare to @rdeits suggestion:

julia> @code_typed fx(1)(1)
CodeInfo(
1 ─ %1 = (Base.mul_int)(2, x)::Int64
└──      return %1
) => Int64

However, that code may suffer from similar issues if either the input n is not compile time known:

julia> fxwrapped(x) = fx(x)(x);

julia> fxwrapped(2)
6

julia> @code_typed fxwrapped(3)
CodeInfo(
1 ─ %1 = (x === 1)::Bool
└──      goto #3 if not %1
2 ─ %3 = %new(Main.:(##133#135))::##133#135
└──      goto #4
3 ─ %5 = %new(Main.:(##134#136))::##134#136
4 ┄ %6 = φ (#2 => %3, #3 => %5)::Union{##133#135, ##134#136}
└──      goto #5
5 ─ %8 = (%6)(x)::Any
└──      return %8
) => Any

Or if f is subsequently used:

function fx2(n)
  if n == 1
    f = x -> 2x
  else
    f = x -> 3x
  end
  x -> f(2x)
end

Problem:

julia> fx2(1)(1)
4

julia> @code_typed fx2(1)(1)
CodeInfo(
1 ─ %1 = (Core.getfield)(#self#, :f)::Box
│   %2 = (Core.isdefined)(%1, :contents)::Bool
└──      goto #3 if not %2
2 ─      goto #4
3 ─      $(Expr(:throw_undef_if_not, :f, false))::Any
4 ┄ %6 = (Core.getfield)(%1, :contents)::Any
│   %7 = (Base.mul_int)(2, x)::Int64
│   %8 = (%6)(%7)::Any
└──      return %8
) => Any

A workaround for both issues is to have a single definition of f, and also ensure that there’s no uncertainty of existence of variables:

function fx3(n)
    a = ifelse(n == 1, 2, 3)
    f(x) = a*x
    return x -> f(2x)
end

fx3wrapped(x) = fx3(x)(x)

Type stability ensues:

julia> @code_typed fx3(2)(1)
CodeInfo(
1 ─ %1 = (Core.getfield)(#self#, :f)::#f#160{Int64}
│   %2 = (Base.mul_int)(2, x)::Int64
│   %3 = (Core.getfield)(%1, :a)::Int64
│   %4 = (Base.mul_int)(%3, %2)::Int64
└──      return %4
) => Int64

julia> @code_typed fx3wrapped(2)
CodeInfo(
1 ─ %1 = (x === 1)::Bool
│   %2 = (Main.ifelse)(%1, 2, 3)::Int64
│   %3 = (Base.mul_int)(2, x)::Int64
│   %4 = (Base.mul_int)(%2, %3)::Int64
└──      return %4
) => Int64
4 Likes