Function definition (unexpected functionality)

Consider the following simple example. This behavior seams rather strange to me. I would have expected the output of myF(1) stay the same regardless of redefinition of ‘a’, or at the very least not be impacted by the last redefinition.

a = vec([1 2 3]);
myF(t) = t*a[2];
myF(1)

2
a[2] = 1;
myF(1)
1
a = 0;
myF(1)
BoundsError

Please quote your code using backticks (`) or the “</>” button.

1 Like

It is also not clear what is input and what is output. You can just post the complete content of a Julia REPL (terminal) session.

This happens because a is a global variable that is being looked up each time you call myF.

Here is the attempt to present the code as you asked:

Here is the part I don’t understand. I can pass the function ‘myF’ to other parts of the code that may have no information about ‘a’. So the function will not be computeable there, right? This behavior is very counter-intuitive.

This is especially counter-intuitive due to the concept of object types not being associated with the variables but rather with instances themselves. So the function ‘myF’ will have completely unpredictable behavior depending on what ‘a’ happens to be bound to at the moment. And ‘a’ is not even a part of explicit argument of ‘myF’.

Now, assuming that this weirdness is a part of life. What would be a good modification of the function definition if I want it to depend on the value that is sitting in ‘a[2]’ at the moment of function declaration?

It’s interesting to play around with @code_* macros. For example, @code_lowered gives you:

julia> @code_lowered myF(1)
CodeInfo(:(begin 
        nothing
        return t * (Main.getindex)(Main.a, 2)
    end))

Note Main.a part - Julia identified that a belongs to module Main (current module), so wherever you pass this function later, a will refer to Main.a, not something else.

Now take a look at the output of @code_warntype:

julia> @code_warntype myF(1)
Variables:
  #self#::#myF
  t::Int64

Body:
  begin 
      return (t::Int64 * (Main.getindex)(Main.a, 2)::Any)::Any
  end::Any

You see these Any? In the REPL, they are even colored in red because you have introduced type instability - one of the major reasons for code slowdown. Julia doesn’t restrict you, but usually, you don’t want/need to have variables with variable type.

The solution is to try to avoid using variables from the global scope, and if really needed, make them const.

Thank you! Very much appreciate the details.

How should I change the code to use the present value stored in ‘a[2]’ (at the time of declaration of function) vs looking up what will be stored there at the time the function is called?

One way is to use closures, e.g.:

julia> a = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

julia> makeMyF(x) = t -> x * t
makeMyF (generic function with 1 method)

julia> myF = makeMyF(a[2])  # we capture current _value_ of `a[2]`, no binding to _variable_ `a` is preserved
(::#1) (generic function with 1 method)

julia> myF(1)
2

julia> a[2] = 42
42

julia> myF(1)
2

This works! Now I just need to figure out what exactly is happening here that makes it work different for closures than for the outer functions.

Also, I would expect there to be a more straightforward/elegant solution here that does not require creating a dummy internal function just to capture a present value of ‘a[2]’. An in-line solution (inside definition of the function myF) would be ideal. This is both a theoretical and a practical question, since I anticipate using this solution often.

The fact that this is happening for you with a closure is actually a bit of a red herring. This is just the normal way that variables are bound in Julia. As you might already know, assigning a variable doesn’t create a copy, just a new name for the same object. So we can do:

julia> a = [1, 2, 3]
3-element Array{Int64,1}:
b =  1
 2
 3

julia> b = a
3-element Array{Int64,1}:
 1
 2
 3

julia> a[1] = 5
5

julia> b
3-element Array{Int64,1}:
 5
 2
 3

The value in b changes because b is just another binding for the same data as a. Compare this with:

julia> a = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

julia> c = a[1]
1

julia> a[1] = 5
5

julia> c
1

In that case, c is bound to whatever was originally in a[1] (the value 1 in this case). Changing a[1] has no effect on c because c just points to the value 1 regardless of what a happens to contain right now.

The reason this is confusing in your particular case is that the closure myF(t) = t * a[2] creates a local binding for a (like the first case) not a local binding for a[2] (like the second case). That’s why changing a changes the value returned by myF.

The code from @dfdx works because calling the makeMyF function creates a new local variable bound to a[2], and your closure captures that variable instead of capturing the entire a array. If you don’t want to introduce a special makeMyF function, you can also get the behavior you want with a let block like this:

julia> a = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

julia> myF = let
       y = a[2]
       t -> t * y
       end
(::#1) (generic function with 1 method)

julia> myF(1)
2

julia> a[2] = 1
1

julia> myF(1)
2

julia> a = 0
0

julia> myF(1)
2

Note that the y created inside the let block just lives inside that block and doesn’t escape or interfere with any other variable named y. Even creating a new variable that happens to also be called y won’t break myF:

julia> y = 10
10

julia> myF(1)
2
2 Likes

Thank you! This is perfect!

As you might already know, assigning a variable doesn’t create a copy, just a new name for the same object.

Yes, I knew that. The part that I did not quite understand was how my original definition of ‘myF’ is interpreted.