12

Coming from Wolfram Mathematica, I like the idea that whenever I pass a variable to a function I am effectively creating a copy of that variable. On the other hand, I am learning that in Julia there are the notions of mutable and immutable types, with the former passed by reference and the latter passed by value. Can somebody explain me the advantage of such a distinction? why arrays are passed by reference? Naively I see this as a bad aspect, since it creates side effects and ruins the possibility to write purely functional code. Where I am wrong in my reasoning? is there a way to make immutable an array, such that when it is passed to a function it is effectively passed by value?

here an example of code

#x is an in INT and so is immutable: it is passed by value
x = 10
function change_value(x)
    x = 17
end

change_value(x)

println(x)

#arrays are mutable: they are passed by reference
arr = [1, 2, 3]

function change_array!(A)
    A[1] = 20
end

change_array!(arr)

println(arr)

which indeed modifies the array arr

Dario Rosa
  • 251
  • 1
  • 6
  • 1
    Maybe https://github.com/JuliaArrays/StaticArrays.jl is what you want. – Jun Tian May 25 '20 at 04:33
  • thank you. I'll take a look. at first sight I would say that the repository you suggested has not directly to do with immutable arrays, but it looks like it works also on immutable arrays. So it looks like there are ways to make immutable the arrays, perhaps in some other github repos. – Dario Rosa May 25 '20 at 04:47

1 Answers1

18

There is a fair bit to respond to here.

First, Julia does not pass-by-reference or pass-by-value. Rather it employs a paradigm known as pass-by-sharing. Quoting the docs:

Function arguments themselves act as new variable bindings (new locations that can refer to values), but the values they refer to are identical to the passed values.

Second, you appear to be asking why Julia does not copy arrays when passing them into functions. This is a simple one to answer: Performance. Julia is a performance oriented language. Making a copy every time you pass an array into a function is bad for performance. Every copy operation takes time.

This has some interesting side-effects. For example, you'll notice that a lot of the mature Julia packages (as well as the Base code) consists of many short functions. This code structure is a direct consequence of near-zero overhead to function calls. Languages like Mathematica and MatLab on the other hand tend towards long functions. I have no desire to start a flame war here, so I'll merely state that personally I prefer the Julia style of many short functions.

Third, you are wondering about the potential negative implications of pass-by-sharing. In theory you are correct that this can result in problems when users are unsure whether a function will modify its inputs. There were long discussions about this in the early days of the language, and based on your question, you appear to have worked out that the convention is that functions that modify their arguments have a trailing ! in the function name. Interestingly, this standard is not compulsory so yes, it is in theory possible to end up with a wild-west type scenario where users live in a constant state of uncertainty. In practice this has never been a problem (to my knowledge). The convention of using ! is enforced in Base Julia, and in fact I have never encountered a package that does not adhere to this convention. In summary, yes, it is possible to run into issues when pass-by-sharing, but in practice it has never been a problem, and the performance benefits far outweigh the cost.

Fourth (and finally), you ask whether there is a way to make an array immutable. First things first, I would strongly recommend against hacks to attempt to make native arrays immutable. For example, you could attempt to disable the setindex! function for arrays... but please don't do this. It will break so many things.

As was mentioned in the comments on the question, you could use StaticArrays. However, as Simeon notes in the comments on this answer, there are performance penalties for using static arrays for really big datasets. More than 100 elements and you can run into compilation issues. The main benefit of static arrays really is the optimizations that can be implemented for smaller static arrays.

Another package-based options suggested by phipsgabler in the comments below is FunctionalCollections. This appears to do what you want, although it looks to be only sporadically maintained. Of course, that isn't always a bad thing.

A simpler approach is just to copy arrays in your own code whenever you want to implement pass-by-value. For example:

f!(copy(x))

Just be sure you understand the difference between copy and deepcopy, and when you may need to use the latter. If you're only working with arrays of numbers, you'll never need the latter, and in fact using it will probably drastically slow down your code.

If you wanted to do a bit of work then you could also build your own array type in the spirit of static arrays, but without all the bells and whistles that static arrays entails. For example:

struct MyImmutableArray{T,N}
    x::Array{T,N}
end
Base.getindex(y::MyImmutableArray, inds...) = getindex(y.x, inds...)

and similarly you could add any other functions you wanted to this type, while excluding functions like setindex!.

Colin T Bowers
  • 18,106
  • 8
  • 61
  • 89
  • 4
    (+1) Great answer. I'd add that arrays are **not** mutable, their elements are, however. Look at this example: `arr = [1, 2, 3]; function change_array!(A) A = [20, 2, 3] end; change_array!(arr); arr` and you get: `3-element Array{Int64,1}: 1 2 3`. This is very convenient and useful, sometimes people ask for the inverse; StaticArrays with mutable elements. – AboAmmar May 25 '20 at 06:12
  • 2
    You definitely don't want to use StaticArrays for Arrays much larger than ~100 elements, since that get's really taxing on the compiler and the code generated won't really be that efficient. https://github.com/JuliaLang/julia/pull/31630 might also be interesting, but that is still very speculative and probably still requires some work. – Simeon Schaub May 25 '20 at 06:47
  • 1
    Mutating functions being indicated by `!` is not completely enforced in Base for some functions that keep some "internal state", like `rand` or `deepcopy`. – phipsgabler May 25 '20 at 07:09
  • 3
    And the real solution to keep an immutable style is to use [purely functional data strutures](https://github.com/JuliaCollections/FunctionalCollections.jl) (but I believe that this package is not very well maintained, currently). – phipsgabler May 25 '20 at 07:12
  • 4
    @AboAmmar That's not correct. `Array`s _are_ mutable, while their elements are not necessarily so. For example, for an `Array` of `Int`s, you can mutate the array by replacing its elements, but you cannot mutate the elements, since `Int`s are immutable. You can test it yourself: create an array of ints: `a = [1,2,3]`, then run `isimmutable(a)` and `isimmutable(a[1])`. – DNF May 25 '20 at 09:05
  • I think ending functions with `!` is enforced for functions that mutate an input argument, but not necessarily otherwise. – DNF May 25 '20 at 09:19
  • @SimeonSchaub I thought this might be the case but wasn't 100% sure. Thanks for chiming in, I'll add this info to my answer. – Colin T Bowers May 25 '20 at 12:09
  • 1
    Thank you, your answer is great and complete! Thanks also to @phipsgabler for signaling that project. I will consider it too. – Dario Rosa May 25 '20 at 12:09
  • @phipsgabler I'd never encountered FunctionalCollections before. Great suggestion, I'll add it to my answer. – Colin T Bowers May 25 '20 at 12:14
  • @DarioRosa I'm glad my answer was helpful. It's a good question. – Colin T Bowers May 25 '20 at 12:18
  • @ColinTBowers It's very Clojure-ish, and I think a good port of the HAMT-based types there, but I don't like the interface -- some things that should work for Julian iterables are broken (or have been the last time I tried). – phipsgabler May 25 '20 at 12:20
  • @phipsgabler Yes agreed it looks like OP will need to do a bit of work if that's the path they choose. – Colin T Bowers May 25 '20 at 12:22
  • @AboAmmar I also believe your statement isn't really correct. What you do in the function inside your example is to rebind `A` to a new object (another array), this is why your `arr` doesn't change. To see that array ARE mutable, just change the body of the function to `A[1]` = 20`. – Antonello Jan 02 '22 at 05:47