'Reducing' scope of global variables in functions

I have a question with respect to globals in functions. I rely a lot on generating functions inside functions that take in data arguments. However, I am finding that changing the data (eg. a and idxs in the global scope below) are changing my functions. I haven’t been able to figure out how to fix a and idxs when I generate the functions so the output does not change, eg. would always return 74 below.

a = [1,2,3,4,5];
idxs = [2,3,4];
x = [6,7,8,9,10];
fn = x -> sum(a[idxs].*x[idxs]);
fn(x)          # 74, initially.
idxs = [1,2];  # Changing indices...
fn(x)          # 20, changes value of function.
# Changes in 'a' have a similar effect;
a = [0,2,3,4,5];
fn(x) # 14

I have tried some global/local hackery and copying of a and idxs but that doesn’t help. Let me know how to get around this issue. Sorry in advance if this is really elementary, Julia is not my strong suit.

You can use a let block to ensure that the a and idxs inside fn refer to a captured copy of the external variables:

julia> a = [1,2,3,4,5];

julia> idxs = [2,3,4];

julia> x = [6,7,8,9,10];

julia> fn = let a = copy(a), idxs = copy(idxs)
         # inside this `let` block, `a` and `idxs` refer to the local *copies*
         x -> sum(a[idxs] .* x[idxs])
       end
#13 (generic function with 1 method)

julia> fn(x)
74

julia> idxs .= [1, 2]
2-element Array{Int64,1}:
 1
 2

julia> fn(x)
74
2 Likes

Thanks for the prompt reply. That was really helpful, but also really frustrating. Because yes, the above works in the ‘map’ method of writing functions, but if I try with a traditional function declaration, it fails!

a_mat = [[1,2,3,4,5],
         [2,3,4,5,6],
         [3,4,5,6,7]];
x = [6,7,8,9,10];
fns = [];
for i=1:3
    function fn(x)
        let a = copy(a_mat[i])
        return sum(a.*x);
        end
    end
    push!(fns, fn);
end
println([i(x) for i in fns]) # [130, 170,210]
a_mat[2][2] = 10;
println([i(x) for i in fns]) # [130, 219, 210]

What is the difference between what we were doing above and here? Thanks again.

Pass the global variables as arguments to the functions and you will avoid all such issues.

I would prefer to not have to pass optional arguments to each function, but I guess that would be a way to get around this issue. I just figured that there must be a less painful method. Also, frankly I don’t understand the difference between the two examples I provided.

Actually, rather hilariously, passing globals as optional arguments doesn’t work either, a la:

    function fn(x, a = copy(a_mat[i]))
        return sum(a.*x);
    end

When a is modified, the output is still changed. I guess I will keep searching for a solution.

You’ve put your let block inside the function body, while I put mine outside the function body. In your example, the global a_mat is copied every time you call fn, and then work is done with that fresh copy. That doesn’t accomplish your goal of isolating the behavior of the function from external changes to a_mat because every call to fn just copies whatever a_mat[i] happens to be right now. You need to do the copy exactly once, when the function is defined, and putting the function definition inside the let block is a nice trick for accomplishing that.

You can achieve the behavior you want by doing:

fn = let a = copy(a_mat[i])
  function (x)
    return sum(a .* x)
  end
end

or

let a = copy(a_mat[i])
  global fn  # we need this otherwise the `fn` will only
             # be a local variable in the `let` block
  function fn(x)
    return sum(a .* x)
  end
end
4 Likes

I misunderstood the question.
But it sounds like doing this and later modifying the global arrays will lead to confusing code, where the reader will exactly be wondering which version of what is being used where.

You can just copy the initial values into new arrays which are never modified and use those inside the functions.

Alright, now I am getting my head around it. I used the first for clarify and worked perfectly. Thanks!

1 Like

Thanks for the feedback and the help @dpsanders! Part of the issue is that the supposed ‘globals’ are products of functions called inside functions in a loop. It has been a serious challenge making code that is robust to potential scope issues.