Error missing about side-effect on immutable type

docs mentioned that:

Julia function arguments follow a convention sometimes called “pass-by-sharing”… Modifications to mutable values (such as Array s) made within a function will be visible to the caller.

I think it causes a bit of confusion, especially if we’re defining “wrong” side-effect functions on immutables, e.g.

julia> x = 1
1

julia> f!(a) = a+=2
f! (generic function with 1 method)

julia> f!(x)
3

julia> x
1

julia> isimmutable(x)
true

here we call f() with the immutable x does not throw any error.

moreover, sometimes isimmutable() is not a reliable check:

julia> y = [11, 12, 13];

julia> vy = @view y[1:3];

julia> isimmutable(y)
false

julia> isimmutable(vy)
true

julia> f2!(b) = b .+= 3
f2! (generic function with 1 method)

julia> f2!(y)
3-element Array{Int64,1}:
 14
 15
 16

julia> f2!(vy)
3-element view(::Array{Int64,1}, 1:3) with eltype Int64:
 17
 18
 19

noted that a view is immutable! (according to isimmutable()), yet apparently it’s often been used in ! functions.

another example:

julia> t = (1, 2)
(1, 2)

julia> g!(a) = a[1]+=2
g! (generic function with 1 method)

julia> g!(t)
ERROR: MethodError: no method matching setindex!(::Tuple{Int64,Int64}, ::Int64, ::Int64)

now an MethodError is thrown, but it’s not about side-effect on immutables.
so, if I’m crazy enough to do:

julia> Base.setindex!(tup::Tuple{Int, Int}, v::Int, i::Int) = setfield!(tup, 1, v)

julia> g!(t)
ERROR: setfield! immutable struct of type Tuple cannot be changed

finally a relevant error is thrown, yet it is of the generic type ErrorException.

should we define an error type of it? and throw the error as long as it happens?
thanks.

That code doesn’t mutate the contents a at all. You would need .+= or similar (if it was an array).

I am not sure what kind of error you were expecting, and why.

In general, it is not the job of the compiler to figure out the intent of your code. You can also write things like

g!(a) = a

which, again, does not mutate anything at all.

Or one can design an API that may or may not mutate (depending on the actual type), and asks you do use the returned value in all cases (eg this is common for dealing with potentially immutable <:AbstractArrays).

All errors you get above are relevant. And this one has a perfectly fine error message. Why do you want a narrower error type — do you need to dispatch on this?

yes I know. But the confusion I mean is the sentence [Julia function arguments follow a convention sometimes called “pass-by-sharing"].
In Julia, f(x) is actually passing by value(not sharing) if x is atomic, but the same syntax f(x) is passing by reference if x is a Vector. So, the above sentence in docs is not entirely correct I think.

by the way, what is f(x) passing if x is a struct? the copied structure or the reference? thanks.

No this is totally wrong. If you do f!(x) = (a = a .+ 2) and do x = 1; f!(x) or x = [1]; f!(x), x will not be mutated in either case.

There’s basically nothing to error in this case since the only mistake you make as far as the compiler could possibly tell is that you are doing an assignment that is not used again. Such thing should be at most a warning and really belongs to a linter instead of a compiler.

The error about setindex! on tuples could be better. It would not change the error thrown but would be a logic in printing MethodError on setindex! with immutable object as first argument. We have a lot of special case to print user friendly error messages like this already so that’s a pretty reasonable thing to do.

As I said, it’s always by reference semantically.

Everything is passed “by value”. That value can be a container, potentially with mutable contents. But it is still a value.

The whole “by value” / “by reference” distinction, as understood in languages like C/C++, is not really useful for programming Julia.

sorry … I’m still confused…
by experience, yes, I know the correct use cases for various kinds of variables. Yet conceptually I’m confused. (and sorry for not able to explain well…)

as far as I understand, please correct me for any wrong concept:

  1. x = 1 binds the name x to the value 1 (which is stored somewhere).
  2. a later expression x = 2 would (re)bind the name x to another value (which is stored elsewhere)

but… why is the following?

julia> x
1

julia> Ref(x)[] = 2
2

julia> x
1

(again, as far as I understand) this should set the value where x has bound to (i.e. overwrite the value 1 by 2) ? but why not happening??? also, no error is thrown but the assignment has no effect…

  1. Forget about the name Ref. It does not create a mutable reference to the object that is passed in. It does create a reference to the object passed in, but it does not mean you can mutate that object. If you expect such a semantics from other language then no this is not a reference in that sense. If you aren’t expecting that from the name then never mind about this point…

  2. Ref(x)[] = 2 is equivalent to (bearing additional variable conflicts…) y = Ref(x); y[] = 2. Similar to what you said. Ref(x) creates a reference that’s bound to 1. This object is then assigned to y. y[] = 2 then store a different object into y. Since y isn’t x (only the reference of the object was passed in, not the reference to the variable) the second assignment has absolutly no effect on x. (In fact, nothing can, except assignment to x directly).

I’m not sure what you are confused about but a few other things to emphasis again:

  1. There is no referene to variable. All reference are reference to objects (values)

  2. Ref(x) indeed creates an object that refer to the same object as x. However, this does not mean that the reference cannot be rebound. In fact, that’s exactly what y[] = 2 does. Given the previous emphasis point, this is the only reasonable behavior.


I know these expressions can sometimes be useful but no they are not stored anywhere. (i.e. don’t take these expression literally and don’t use these as argument for anything…).

Storage location doesn’t exist as a concept when you talk about high level semantics (an object (value) is an object (value), it’s abstract and isn’t bind to any memory). At low level this is also not the case. 1 may or may not be anywhere. 2 may or may not be stored at the same location as the 1 or it may or may not be stored anywhere.

There really isn’t a level where you can talk about 1 and 2 stored at different locations. You can talk about a equivalent unoptimized implementation using other languages (like C++) where these could be the case if that helps your understanding. Just be careful what you are talking about is a property of the language and what is the property of your implementation.

2 Likes

There may not be multiple kind of passing semantics but saying everything is passed “by value” would be very misleading. The different names (by value, by reference, by sharing) is indeed a bit ambiguous but I believe by value usually does not mean what we have. I usually use by reference since it seems to be understood by most people but I’m also fine with by sharing.

AFAICT, “by value” usually imply copying, and mutating the passed object will have no effect on the original one. Such semantics doesn’t exist in julia but that doesn’t mean this is a correct or good way to describe the argument passing semanatics. Copying (and aliasing) is definately an object that exist in julia and calling this by value is only going to cause unnecessary confusion.

2 Likes

I mean, the following example really confuses me:

julia> x = 1;

julia> y = Ref(x);

julia> y == Ref(x)
false

julia> Ref(x) == Ref(x)
false

by any sense, a Ref is “not a reference” is … just confusing. I think we need another term to clarify the situation…

Certainly — hence the quotation marks. As I said above, this kind of terminology is not a useful one for Julia. It is unfortunate that it is so widespread.

Think of Ref as a container for a single value. It has nothing to do with the & operator in C.

Maybe this will help: Ref(x) creates a new object referring to x. Ref(x) == Ref(x) creates two distinct objects referring to x. The equality check returns false because there is no specialised method for ==(::Ref, ::Ref), and thus the generic fallback to identity/=== happens.

julia> x = 1
1

julia> @which Ref(x) == Ref(x)
==(x, y) in Base at operators.jl:83

# @edit Ref(x) == Ref(x) shows us this:
# ==(x, y) = x === y

Now, as for this:

Your function is equivalent to the following definition:

function f!(a)
   a = a+2
   return a
end

The variable a in the body of the function is reassigned a new value and that new value is returned, because in julia the value of the last executed statement is also returned from the function. The variable outside of f! is still referring to the old value, because it is in a different scope and nothing inside of the function is modifying what’s inside of that value.

Well it’s not a C++ reference, which is by no mean the only meaning of it. It is refering to a single object.

Well, try this in C. int x = 0, y = 0; return &x == &y;.