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?
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.
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.
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.
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.
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.
@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.
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.)
⌠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.
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.
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
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.
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.