Reputation: 469
I am trying to make sense of how Julia copies and treats variables. Take a look at the following examples and the following questions:
a = 1
b = 1
a === b #why do they share the same address? I defined them independently
a = 1
b = a
a === b #true, this makes sense
a = 10 #b still has value of 1 though? why is that when they share the same address as per above?
a = (1,2)
b = (1,2)
a === b #how can they have the same address?
a = [1,2]
b = [1,2]
a !== b #this is true as I would expect (thanks to phipsgabler)
Upvotes: 5
Views: 254
Reputation: 33259
Something that none of the other answer's touch on is that x === y
does not mean "Do x
and y
have the same address in memory?" as your questions suggests. Rather, as the documention for ===
states, what the x === y
operation does is to:
Determine whether
x
andy
are identical, in the sense that no program could distinguish them.
This is the “egal
“ predicate, as introduced in Henry Baker's famous paper "Equal Rights for Functional Objects or, The More Things Change, The More They Are the Same". This paper has the insight that when comparing two objects, you don’t really care where they happen to live in memory, which is an implementation detail that could change. Instead what you really want to know is if there's some program you could write that would distinguish between them, which depends only on the semantics of the language. The paper then goes on to explore what that means for mutable and immutable data structures.
If two objects are immutable and have the same type and value, then you cannot tell them apart, even if there happen to be two copies of the value somewhere in the memory of the program. Therefore they are ===
(i.e. egal
). It doesn't matter whether 1
originates from two different assignments or not, as your examples show. Your intuition seems to be that when someone writes a = 1
and later b = 1
a new 1
object is created each time 1
appears. But there could equally be a single globally shared 1
object which the literal syntax 1
fetches a reference to. In fact, this is case: small integer values are allocated statically and the same instance is used any time a small integer literal occurs. This is true in other languages as well, such as Python:
# Python
>>> a = 1
>>> b = 1
>>> a is b
True
Languages do this if they regularly need “boxed” instances of integer values because allocating many boxed copies over and over again would be inefficient. Instead it’s standard to have a static “cache” of small boxed integers. However, there are too many integers to cache them all, so for large enough integers this doesn't happen:
# Python
>>> a = 1234
>>> b = 1234
>>> a is b
False
The cutoff in the Python version I'm using (3.9) happens to be 256:
# Python
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
But of course, this could change if they decide to make the cache bigger or smaller, which is somewhat unfortunate. Even if it never changes it’s definitely a leakage of an implementation detail. This aside about languages caching small integers serves to show you that even if we based ===
on address like Python does 1 === 1
would still be true. It isn't based on address, however, it compares types and values for integers of any size:
julia> a = 9223372036854775807; # typemax(Int)
julia> b = 9223372036854775807; # typemax(Int)
julia> a === b
true
Why not compare integers by address? We don't want to force immutable objects to even have a well-defined notion of address. In a well optimized program, an integer value will often not be stored in memory at all. It may only ever exist in registers, or it might be eliminated entirely. If the same constant value is used in multiple places, you may want to avoid wasting multiple registers storing it and just keep it in a single register that you use multple times. If the compiler had to keep track of allocation semantics for integers, then it would prohibit all sorts of optimizations.
Next let's look at your last example:
julia> a = [1,2]
2-element Vector{Int64}:
1
2
julia> b = [1,2]
2-element Vector{Int64}:
1
2
julia> a !== b
true
Why does the same array literal syntax produce arrays that are !==
? Why doesn't the same logic that we used for integers above apply? First, let's show that you can write a program that distinguishes a
from b
:
julia> a[1] = -1
-1
julia> a == b
false
We mutated a
and didn't mutate b
after which, they no longer have equal contents, so they are easily distinguishable. Thus they cannot be ===
. Why couldn't we do the same thing with integers? Because they're immutable, so you cannot change their contents. This isn't just some incidental feature of mutable arrays, rather it is their defining feature: they are containers with identity independent of their contents, which must therefore be associated with some location in memory so that they can behave coherently when one part of a program changes their content and some other part of a program observes that change.
What about the middle example with tuples? Unlike arrays, tuples are immutable—like integers, they are their values, they have no identity apart from what they contain. Thus, (1, 2) === (1, 2)
doesn't care if there's one tuple literal or two: they're indistinguishable, identifiable only by their contents, which is, in this case, the same. Here's a slight variation:
julia> a = (1, [2])
(1, [2])
julia> b = (1, [2])
(1, [2])
julia> a === b
false
Why are these not ===
? They're immutable tuples with the same contents? Well, what does "the same" mean? The correct definition is to apply ===
on the contents recursively, and since the two arrays are not the same array even though they both currently hold the value 2
, these tuples are not recursively indistinguishable. Or to put it more succinctly:
julia> a[2][1] = 0
0
julia> a
(1, [0])
julia> b
(1, [2])
julia> a == b
false
They're not ===
because we can reach into the array in each tuple and change its contents. Here's one final example:
julia> v = [2];
julia> a = (1, v)
(1, [2])
julia> b = (1, v)
(1, [2])
julia> a === b
true
Now the two separately constructed tuples containing mutable arrays are the same! That's because they both contain the same mutable vector as their second component. In other words:
===
if their components are all recursively ===
1
and 1
are ===
because integers are immutable and they have the same type and valuev
and v
are ===
because they're the same vectorHopefully that clears things up.
Upvotes: 8
Reputation: 432
The behaviour you describe is due to the fact that integer are primitive type (according to the doc: "A primitive type is a concrete type whose data consists of plain old bits.").
Therefore when you write:
julia> a = 1
The variable a
doesn't contain an adress to some object whose value is 1, but does contain indeed the value 1.
This is why when you do the following comparison it returns true even if a
and b
are different variables, they are both equal to 1:
julia> a = 1
julia> b = 1
julia> a === b
And this is why when you do:
a = 1
b = a
a === b # true
a = 10 # but b is still equal to 1
b
is still equal to one. Because the content of a
has changed, but not the content of b
.
Immutable type (like tuples of integer like in your 3rd example) behave as primitive type in that regards.
Finally, arrays being mutable, when you write:
julia> a = [1,2]
The variable a
does indeed contain an adress, and when you do
julia> b = a
You put in b
the adress to the array [1,2]
. Therefor modify the content of a
will modify the content of b
.
Those behaviours are the same in a lot of other programming language, C/C++ and java included.
Additionnally, you may want to read the following article: https://en.wikipedia.org/wiki/Pointer_(computer_programming)
Upvotes: 2
Reputation: 20960
It's not so much about variables, but more about where the values are stored.
Quoting the docs:
An object with an immutable type may be copied freely by the compiler since its immutability makes it impossible to programmatically distinguish between the original object and a copy.
In particular, this means that small enough immutable values like integers and floats are typically passed to functions in registers (or stack allocated).
Mutable values, on the other hand are heap-allocated and passed to functions as pointers to heap-allocated values except in cases where the compiler is sure that there's no way to tell that this is not what is happening.
In particular, tuples of integers are immutable. This is a recursively determined property:
julia> a = (1, [2])
(1, [2])
julia> b = (1, [2])
(1, [2])
julia> a === b
false
Upvotes: 5