Julia pass-by-sharing : cautions and benefits

Jeff Fessler
2019-02-06

Julia uses a convention called "pass-by-sharing" for passing arguments to functions [https://docs.julialang.org/en/latest/manual/functions/#Argument-Passing-Behavior-1]

Here is an illustration / cautionary note.

In [1]:
function f1(x)
    x = x .+ 1
    return x
end

function f2(x)
    x[1] += 1
#   x[:] = x[:] .+ 1
#   push!(x, 5)
    return x
end
Out[1]:
f2 (generic function with 1 method)
In [2]:
a = [3,3]
b = f1(a)
@show a,b

c = [3,3]
d = f2(c)
@show c,d
;
(a, b) = ([3, 3], [4, 4])
(c, d) = ([4, 3], [4, 3])

The same thing happens with variables, even without functions

In [3]:
a = [3,3]
b = a
b = b .+ 1
@show a,b, b===a

c = [3,3]
d = copy(c)
d[1] += 1
@show c,d, c===d
(a, b, b === a) = ([3, 3], [4, 4], false)
(c, d, c === d) = ([3, 3], [4, 3], false)
Out[3]:
([3, 3], [4, 3], false)

The reason is that d = c does not copy c but just makes "another pointer" to the same data, and then d[1] = 5 is "syntatic sugar" for setindex!(d, 1, 5) and the ! there reminds you that d will be modified, but d points to the same data as c so c is also modified here.

Why call-by-sharing is useful

In [4]:
function gd!(x, g::Function, L::Number, niter::Number)
    for iter=1:niter
            x .+= g(x)/L # in-place update - for large-scale problems
    end
end
Out[4]:
gd! (generic function with 1 method)

Surprise: += is not in place but .+= is

In [5]:
function g1(x, y)
    x += y # this does not mutate input x
    return x
end

function g2(x, y)
    x .+= y # this does modify input x !
    return x
end
Out[5]:
g2 (generic function with 1 method)
In [6]:
a = [3,3]
u = [1,1]
b = g1(a,u)
@show a,b

c = [3,3]
d = g2(c,u)
@show c,d
;
(a, b) = ([3, 3], [4, 4])
(c, d) = ([4, 4], [4, 4])

Bottom line: to be safe, avoid using function arguments on LHS unless you want it to be mutating, like gd! above, in which case use ! in function name.