What is the philosophy behind arrays not being copied when assigned to another variable?

Very basic question, but I am hoping there might be an underlying principle which could help me understand other (at first) puzzling aspects of Julia.

As a heavy Matlab and C user, it is hard to imagine why someone would want to have changes to a alter b after b=a. If I later need to use the new value of a for some other computation, well, why not just use a?. Why would I have created b in the first place?

Confusion over Assignment vs. Shallow copy vs. Deep copy is also one of the most common sources of error for (novice) Python users, so I assume this choice wasn’t made to make Julia more accessible to the Python crowd.

So how should I think about this? Is it about performance? Wouldn’t lazy copies a la Matlab/R be a better fix?

4 Likes

It is a matter of the most useful semantics, IMO. No copy (explicit copy) is generally better, I believe.

It’s (at least in part) about consistency. Assignment a = b is always just binding a new name a to an existing object b, and is never (conceptually) making a copy (although the compiler is allowed to copy data as an optimization if the object is immutable). See also the manual section on assignment vs. mutation.

Nor is Julia unusual — this behavior is pretty typical for a lot of mainstream languages. Matlab’s copy-on-write semantics are the odd one out here. (And C++ is completely inconsistent because assignment is overloadable, so you never know what it is going to do.)

Realize also that passing function arguments is equivalent to assignment. Matlab’s copy-on-write argument-passing semantics make it impossible to implement functions that act in-place on array arguments.

If assignment made a copy, you’d want another syntax to “opt out” of copying. What would it be? Whereas opting into copying with a = copy(b) is pretty explicit, clear, and familiar in a lot of languages.

35 Likes

why someone would want to have changes to a alter b

struct Cars
     models
     ages
end

cars = Cars(["Camery", "RAV4"], [1, 5])

# At the end of the year, we need to do some summary stats, 
# save them, and increment
ages = cars.ages
ave = average(ages)
# other stuff
ages .= ages .+ 1

Just a bit contrived case where it’s nice to pull out an attribute of another object, modify it, and have it be meaningful that the original object should stay in sync.

3 Likes

Two things.

  1. There is a previous answer of mine that helps to internalize the memory model. On boxes vs labels: Is it appropriate to say a variable is a label or box? - #19 by Henrique_Becker
  2. In C, you can have static-sized arrays and (mutable) structs that when assigned to a variable will copy the value to the variable because the variable is kinda guaranteed to be a memory space. However, any array with dimensions only known at runtime (and a lot of structs that have pointers stored) will have no-copy/shallow-copy problems a lot of times.
6 Likes

Strictly speaking the change is to 1 mutable instance assigned to both a and b. It’s pretty common to have multiple references to 1 instance like this:

a = Ref(0)    # assigned to `a` in global scope

increment!(b) = b[] += 1 # simplest mutating method
increment!(a) # assigned to `b` in the method body, just like `b = a`

A = []
push!(A, a) # assigned to an element in array A
A[1] === a  # same instance

For the record, the behavior in the copy-on-write example currently on the Mathworks website also happens in Julia, it just happens a different way because of the separation between variables and instances in Julia (though we commonly optimize by making sure a variable is always assigned to instances of 1 type during a call, this is “type stability”).

% MATLAB copy-on-write example, verbatim
A = rand(1e7,1);
B = f2(A);

function Y = f2(X)
X = X.*1.1; % X is an independent copy of A
Y = X;      % Y is a shared copy of X
end

# Julia version
function f2(X) # local X assigned to input array via input A
  X = X .* 1.1 # X.*1.1 makes new array, reassign X to it
  Y = X        # Y assigned to new array via X
  return Y
end

# Julia no-copy version that mutates the input array
function f2!(X) # local X assigned to input array via input A
  X .= X .* 1.1 # no new array. reassigns the elements, not X
end

What MATLAB does that Julia doesn’t do is copy-on-write on lines like mat[i] = num (in MATLAB, mat(i) = num;). There are countless questions in MATLAB Answers asking why mutation within a custom function doesn’t affect the input array, and the answer is generally to return the copy and reassign the input variable in the call scope. Turns out you can mimic the mutating function style in MATLAB, but it really just moves the call scope reassignment code into the function. No point in hiding the copy-on-write semantics like that.

2 Likes

Fyi, even in Matlab this behavior can be achieved via handle classes and I have the feeling there’s a trend towards it.

1 Like

Just so it’s only not only @Henrique_Becker 's self-promotion, I just wanted to highly endorse this recommendation - I remember clearly reading it the first time and having all the pieces fall into place in my brain.

4 Likes

See also this example and this quiz on the subject.

@kevbonham Thanks! I shamelessly self-promote that link, XD. Someday it will be the post with most backlinks (except by Tama’s PSA probably). There is another example that is very good, that I say in a post here but was not able to locate this time: a cat alternates between living two houses, in one of them it is called Tiger, in the other it is called Fluffy, the cat is the same, and it does not know any of its names.

@sylvaticus I need to start referencing these links too. They are good code examples.

1 Like

That’s not so nice :wink:

Thank you all for the very insightful comments. I will soon tag @stevengj 's reply as the solution as it gives the “philosophical” background I asked for. His third argument in particular seems to seal the deal.

EDIT: Said something silly. Thank you all again for the patience.

It does not work differently for arrays and scalars.

a = 3
b = a

makes b refer to the “same object” 3 as a. (Because 3 is immutable, the compiler is free to make a copy if it wants to, but this is purely an implementation detail and is not visible to the user: a === b will be true regardless.)

6 Likes

… hmm… but here we are dealing with operations as copy an object (or assigning a parameter to a function).
These are not mathematical concepts, but computer ones, so I am not surprised that computer implementations, as the fact if the object is mutable, matters… scalars and vectors are implemented very differently… the memory they use, the operations that a CPU can do at the bare metal level… they are fundamentally different and so on a computational level they behave very different, even if they are very close conceptually in math.

1 Like

That a and b themselves do not count as true “objects” in this sense was perhaps the insight I was missing. This is clear now.

1 Like

There’s a difference between the semantics of a language — which are a mathematical concept — and the implementation. The former are what are necessary to understand syntax and the meaning of a program. The latter is more relevant when you are optimizing.

For example, a variable may not actually be stored anywhere — the compiler could optimize it out completely. e.g. in

function f(x)
    a = 3
    b = 2a
    return b + x
end

the variable a will actually not exist in the implementation, because the compiler will constant-fold this into b = 6 (or equivalent — e.g. for x::Int it may turn into an immediate value).

Confusing semantics for implementation will just lead you into perplexity.

4 Likes

There is another typical confusion (I made it), regarding assigning and mutation syntax, as both can use the = operator. I have a note summarizing previous answers about that here: Assignment and mutation ¡ JuliaNotes.jl

1 Like

Yes, that’s exactly the insight — the names themselves aren’t Julia objects or boxes or pointers or memory locations. I’m fully on board with @Henrique_Becker’s post linked above — I don’t even like the word “variable” because it has a lot of the “box” baggage in my experience. Assignments are just choosing a name for a Julia object to make your life easier.

You can name numbers, you can name arrays, you can name functions, you can name pretty much anything in Julia, and it’s all assignment. You can decide that you have a better use for that name and use the name for something new (as long as it wasn’t const). Or you might decide that it’s useful to have two different names that refer to exactly the same thing.

Somewhat confusingly, the poorly named “updating” operators don’t actually “update” (or mutate) a Julia object. Doing something like x += 1 is simply saying that you have a better use for the name x — and it’s going to be one more than whatever the value of x previously was.

Syntactically similar — but hugely different — is that you can use “indexed assignment” (y[1] = ...) or field mutation (y.foo = ...) to change some property about the thing you named y. Or you can broadcast into the object named y with y .= 1. Or you can pass it to a mutating function (like normalize!(y)). All of those things don’t really change anything about the name “y” itself, but rather they change the object that you chose to refer to by the name “y”. And there could be other places in your code where you refer to that same object by other names, it doesn’t matter, it’s all one object.

9 Likes

This blog post helped me understand the same issues many years ago, it’s still accurate.
https://www.juliabloggers.com/values-vs-bindings-the-map-is-not-the-territory-3/

4 Likes

Thanks, but I believe @kevbonham just chose a word with a negative baggage, what I did is by definition ‘self-promotion’, and the rest of his comment makes clear he agree with both the content of my post and me linking it here.