I think @StefanKarpinski answered exhaustively the questions about the immutability in the thread @haberdashPI referred to:
Julia already distinguishes assignment syntactically from both mutation and equality. There are the following assignment-like syntaxes:
x = ...
is assignment. The name x
is bound to the value that the ...
evaluates to. Local bindings never leak out of their scope. It doesn’t matter what x
was bound to before this happens, that value is not affected in any way. No object is mutated by this and no other binding besides x
is changed.
x.f = ...
is equivalent to setproperty!(x, :f, ...)
which, by default mutates the object x
by changing its field f
to the value of the expression ...
. If x
is visible in another scope or by another bindings, this change will be seen everywhere. No bindings are changed by this.
x[i] = ...
is equivalent to setindex!(x, ..., i)
which, for arrays mutates the array x
by changing its i
th slot to refer to the value of the expression ...
. If x
is visible in another scope or by another bindings, this change will be seen everywhere. No bindings are changed by this.
There are equality operators ==
and ===
which check for value-based equality and identity-based equality, respectively. There is also already a syntax for creating a constant binding as opposed to a variable binding:
const x = ...
Indeed, this is very clear in relation to mutability/immutability, and in my experience this was practically sufficient for Julia code I’ve written so far. A good balance between expressiveness and conciseness IMHO. I dig @StefanKarpinski’s stance on this.
However, Julia 1.0 broke my code in more subtle ways than I anticipated. Upon deeper analysis, I realized that the issues appear to stem from the Julia’s dissymmetry between assignment and mutation.
Let’s get back to @StefanKarpinski’s examples:
-
When I see x.f = … or x[i] = … equivalent in pretty much every statically compiled language, including Julia, it tells me that binding between name x and a value of a mutable type is expected to already exist in an accessible scope before the code analysis gets to this line. A concrete binding is found by the name resolution algorithm, potentially including a binding in the current scope. If such binding isn’t found, compiler reports an error.
-
When I see x = … equivalent in a statically compiled language, the semantics, potentially, could be one of:
A. Symmetrical to the treatment of x.f = … or x[i] = …: a binding between x and some value is already expected to exist in an accessible scope, in which case the statement is interpreted as re-assignment of a new value to that binding. If a binding doesn’t yet exist, compiler reports an error. Creation of a binding is supposed to be explicit, happens in the current scope, and can be very syntactically similar to the assignment. This is how compilers for most statically compiled languages tuned for large-scale programming operate.
B. Same as above, except if binding doesn’t yet exist in an accessible scope, then the statement creates a new binding in the current scope. As I understand, this is how Julia behaved prior to 1.0, at least in regard to a current scope accessible inside a for loop.
C. Binding between x and some value may already exist in the current scope, in which case the statement re-assigns a value to that binding. If it does not yet exist in the current scope, then the statement creates it. This is how Julia 1.0 behaves in regard to a current scope accessible inside a for loop.
This discussion thread exists because significant number of people are perplexed by the defaulting to the choice C for the x = …. I also understand why defaulting to B wasn’t 100% desirable either.
Doesn’t it leave A as the logical default choice for the next stage of Julia’s evolution? Naturally, any move in this direction ought to be backward-compatible.