Mutable function parameter of primitive type

I’m going through Julia manual and I’m having a bit of a problem comprehending mutability and pass by sharing concept as applied to primitive types. Can some show me an example where x will be modified by calling f(x) so the value printed is not 42:

x = 42
function f(x) ....... end
println(x)

Let’s put aside the bigger question of sound software engineering practices. I just want to see it done. BTW, I know that wrapping x in a mutable struct would do the trick, but is this the only way?

No, primitive types like Int are immutable. You can only change their binding, not mutate their value.

As you alluded, the way to do this is wrapping in a mutable type, usually Ref(x) or whatever.

You can use the isimmutable function to see if a value may be mutated or not:

julia> isimmutable(42)
true
1 Like

Note that rather than passing a number as a mutable parameter (e.g. wrapped in a Ref), it’s generally much better to simply return the updated value. You can return multiple values with a tuple.

3 Likes

Of course, but see my disclaimer about sound software engineering practices. :grinning:

I would not contemplate to check 42 for immutability in my exercise, I would check x. When I do so, it doesn’t make much sense on the surface for someone coming from C++ world:

julia> x=0
0
julia> isimmutable(x)
true
julia> x=42
42
julia> x
42

The difference is than in Julia, x = 0 is just saying “bind the value 0 to the variable x”. Then, when you later say x = 42, it’s again saying “bind the value 42 to the variable x”.

No mutation is happening here, all that’s happening is that the value x is bound to is changing. This is unlike when you have v = [1, 2, 3] and you do v[1] = 2 or something. That’s real mutation, not a changing of variable bindings.

2 Likes

This requires a bit of a neural network rearrangement for an old C++ programmer. I will chew on it.

Yes, Julia is quite different from C++ in this respect.

In Julia, values can be immutable or mutable. The value 42 is immutable because it a primitive (immutable) type. The tuple (1, 2) is likewise immutable. A value of a mutable struct x is mutable because you can reach into it and modify it via x.foo = 1.

Variables in Julia are just labels. Doing x = 0 attaches the label x to the value 0. Doing x = 42 attaches that label to a new value. It has no effect on the previous value that that label happened to be attached to.

This is completely different than C++. In C++, doing:

x = y

can have arbitrary side-effects because it calls the operator= method of whatever the type of x is.

In Julia, doing x = y just attaches the label x to the value of y. It never has side-effects.

5 Likes

The first thing I tried after I got to mutable struct in the manual is:

julia> mutable y=0
ERROR: syntax: extra token "y" after end of expression

There seems to be a bit of dichotomy between primitive and composite types in Julia, but I haven’t got to the end of the manual yet. Maybe it will become more clear later.

@pauljurczak maybe it helps to consider a similar example with a mutable value, for example an array?

x = [0,1]
y = x        # x and y both bound to the same value (a mutable array)
x[1] = 3     # change the array
y            # the change is also visible from the y binding
x = [5,6]    # x now bound to a new value: the array [5,6]
y            # y still bound to the first array: [3,1]

So x = ... is indeed treated the same for mutable and immutable values. This syntax doesn’t change the value, instead it changes the meaning of x.

2 Likes

Right, this doesn’t mean anything in Julia because values are mutable, but variable names are just labels. You can always attach a label to a new value–there is no concept of mutability of the labels themselves.

3 Likes

I was looking for the solution to this problem and it was never actually answered here. Here is an answer that is actually in the documention if you read far enough (which I didn’t and it took forever to figure out)

width = Ref{Cint}(0)
range = Ref{Cfloat}(0)
ccall(:foo, Cvoid, (Ref{Cint}, Ref{Cfloat}), width, range)

Upon return, the contents of width and range can be retrieved (if they were changed by foo) by width[] and range[]; that is, they act like zero-dimensional arrays.

It may not be sound practice, but C functions often modify their parameters to mimic return mulitple values. I my case I was trying to figure out how to use the SDL and this stumped.

For a pure-Julia example of mutating scalar inputs:

function increment!(val, incr)
  val[] += incr # use [] to access the contents of val
  return nothing
end

x = 10
y = Ref(x) # wrap a value in a mutable container
increment!(y, 5)
show(x)   # 10 # did not (and can not) mutate the value bound to x
show(y)   # Base.RefValue{Int64}(15)
show(y[]) # 15

This mutation pattern is frequently used for in-place functions operating on arrays, such as sort! or reverse!. It’s uncommon in the scalar case (it’s usually much nicer to just return the new value) except when using ccall to some foreign code.