Spooky action at distance not applicable to vectors?

I have been using Julia for all my codes. I was explaining all the advantages of Julia over Python to my lab members and one of them decided to give it a try. Since I had stumbled over scoping rules when I started, I warned about using global in for loop by showing a code which went something like this:

x = 0
v = [1, 2, 3]

for i=1:length(v)
	global x
	a = x
	x = i

	# No need of global for v
	a = v[i]
	v[i] = i
end

I explained that the reason for this was it helped avoid spooky action at a distance, as explained in the manual:

With the Julia ≤ 0.6 behavior, it’s especially concerning that someone might have written the for loop first, had it working just fine, but later when someone else adds a new global far away—possibly in a different file—the code suddenly changes meaning and either breaks noisily or, worse still, silently does the wrong thing. This kind of “spooky action at a distance” is something that good programming language designs should prevent.

His question was that why is the global annotation not required for vectors? I had no answer to this. What would be the correct explanation here? Are vectors not susceptible to spooky action at distance?

Assignment and mutation are different things:

2 Likes

Thanks a lot for the explanation. But what if someone defined another vector somewhere
v = ['a', 1, 'b']. That will break my code without warning. This is something sort of spooky action at distance. Or is this incorrect?

That’s assignment rather than mutation. Assigning to v somewhere else has no effect on your v. Mutation is visible everywhere because everyone has a reference to the same object.

1 Like

Sorry. I still do not understand. I understand the thing about mutation and assignment. I am trying to understand how the behavior can lead to spooky action for vectors. Here is the code in manual but I have made x a vector:

x = [123, "hello"]

# much later
# maybe in a different file

for i = 1:10
    x[1] = "hello"
    println(x)
end

# much later
# maybe in yet another file
# or maybe back in the first one where `x[1] = 123`

y = x[1] + 234

The annotation global is required to help with the spooky action right? Or is that understanding itself incorrect?

The syntax x = and the syntax x[i] = do completely different and unrelated things: the former associates a variable with an object; the latter modifies an object. Changing code from doing one to the other and then asking why they behave differently is like putting a sheep in a chicken coop and then putting a fox in a chicken coop and wondering why the results are different. They’re completely different animals, there’s no reason to expect them to behave the same in the first place. One is quite harmless and the other is not.

3 Likes

So the code I posted above is harmless and the original one in the manual is harmful. Can you please explain why is that the case? Maybe it will be helpful to clear up what I am thinking incorrectly.

Here’s a simplified version of your original code:

x = 0
v = [0]

let
    global x = 1 # global needed here
    v[1] = 1     # no global needed here
end

So the question is why is global needed to assign to x but not to assign to v[1]. The answer is because x is a variable and v[1] is not a variable—it’s a location in an array. The former is assignment (x =) and the latter is mutation (v[i] =)—you’re just doing different things with different syntaxes. The former requires global to affect a global variable from a local scope. The latter modifies the contents of an array and there’s no such thing as a global or local array, an array is just an array; it may be accessible by a global variable and/or a local variable (often both, e.g. if a global is passed as an argument); modifying it via either is the same and neither requires annotation.

Here’s a variation of your code with three different behaviors:

x = 0
y = [0]
z = [0]

let
    global x = 1   # global needed here
    global y = [1] # global also needed here
    z[1] = 1       # no global needed here
end

In this version:

  • x is changed from referring to the value 0 to the value 1; global is required here
  • y is also changed from referring to the vector [0] to referring to a new vector [1]; global is also required here
  • z refers to the same vector the whole time, but the content of the vector is modified; global is not required here

You only need global in order to change what value a global variable refers to from a local scope. Modifying the content of a value has no relation to scope and it doesn’t matter whether the value being modified is accessed via a global variable or a local variable; indeed, it would be incoherent to distinguish the two since the same value can be accessed via a local or a global. Regarding this last point, consider this example:

g = [0]

let
    # modify array via global variable
    g[1] = 1
end

let
    # modify the same array via local variable
    l = g
    l[1] = 2
end

By the logic that g is global and l is local you would want a global annotation on the former and not require it on the latter, but that makes no sense because both operations are doing the same thing and modifying the same array. Arrays are not global or local, only variables are.

14 Likes

I really appreciate you taking time to explain it in such detail. Thank you very much.
I understand the argument of mutability: 1. If I use mutable struct I do not need an global annotation to modify its fields. 2. Or for arrays the assignment calls the setindex! method and mutates the original array.

Can you also add to the answer how to explain the need for global annotation to a beginner? Can it go something like this:

  1. The global annotation for variables is required as it helps avoid spooky action at distance in many cases. (Is the mention of spooky action correct here?)
  2. If vectors are mutated the global annotation is not required as it is just modifying the original array and not changing reference.
  3. If a reference to vector is changed then global annotation is required.

Thank you again.

1 Like

I don’t see any spooky action at a distance in that code. First you assign x, then you mutate it, than you use it mutated.

Here is where it is spooky:

x = [1]
f() = x[1]
f() # returns 1
x[1] = 2
f() # returns 2

So, f uses a global array x and the behavior of f can be modified by changing the content of that array. That is the same if x was a scalar (except that you can get a warning if the scalar was declared constant).

So I don’t think Julia has a particular syntax to avoid that. We are recommended to not use global variables in general (for this reason in any language, for performance reasons specifically in Julia).

I am probably simplifying things here, but the scoping rules in Julia are more about keeping types constant as much as possible within scopes, than values.

(what is harmless is this:

x = [1]
function f()
    x = [2]
    return x[1]
end

here f() will return 2 always, independently of the global x being mutated or reassigned anywhere, here is where the local scope of x in f matters, this is independent of x being a scalar or an array; i. e. you don’t need to worry about the label binding outside the scope of the function if you are not using global variables inside it - this is what is really important, otherwise every function will be subject to these spooky actions. But I would be surprised if that was different in any language.).

2 Likes

I was referring to it as spooky in the sense that the code did not do what it was expected to do since an element of global array x was mutated in between. But that might not be what spooky action at a distance refers to.

I have started using functions and passing parameters in struct to avoid this issue. But the downside is debugging. Since all the variables in the function are local I cannot see what is happening to them in variable viewer or REPL.

1 Like

Having the code split in self contained functions will certainly payoff in the long term for debugging. To inspect what is going on inside the function either you print values (rough, but simple, that’s what I do most of the time) or use a debugger.

One remark about my comment above: the scoping rules in scripts are not exactly the same as those of the REPL or if you wrap the complete code inside a function or let block. So, for instance, in the REPL you will change the value of the global variable even if no global is used, and that is different from a script.

3 Likes

You can generally cut and paste code that’s local to a function into the REPL and just enter each successive expression to simulate stepping through your function. Even though more sophisticated debugging tools exist, this is still my preferred method of debugging since it is simple, reliable, flexible, intuitive and works in pretty much every situation.

“Spooky action at a distance” can refer to different things. In the case of global variables, it refers to the problem of accidentally trashing an existing global variable by assigning to it from a local scope. Prior to Julia 1.0, assigning to an existing global from a “soft” local scope modified that global by default. This made it unfortunately easy to accidentally stomp some global variable. This is not hypothetical: in the still relatively small pre-1.0 ecosystem there were hundreds of instances of this kind of bug—we did an analysis prior to the change of all registered packages to see the impact of the change and 100% of the time it fixed a bug.

Mutation of objects like arrays or structs that are accessible via other bindings is also spooky action at a distance. This is also not good, but it has nothing to do with scope because it’s not the binding that’s causing confusion, it’s the sharing and modification of the actual object. This is why immutable types and purely functional programming styles are such good idea and why we make immutable structs the default in Julia. Unfortunately, it remains a hard research problem to allow people to express all efficient operations on arrays in a purely functional style, so for the time being we’re stuck with mutable arrays in order to allow people to write code that has optimal performance.

6 Likes

This seems nice. I will give it a try.

I was confused about spooky action at distance and was going to ask another question to get some clarification. But this clears it up. Thanks a lot. I would have liked to mark this comment along with the marked solution as the complete solution. But I don’t think I can mark two solutions.

3 Likes

I think that’s ok—people can read further if that want to. Glad it was helpful!

3 Likes