Immutability and pass-by-value are inherently useful, and inherently related (in fact an immutable pass-by-reference type is essentially indistinguishable from a pass-by-value type). I didn’t really get this at first, so let me paraphrase the best explanations I’ve gotten so far here.
From the Swift team: Cocoa already simulates pass-by-value and its relationship to immutability quite explicitly. Wherever you do a "defensive copy", you're asking for pass-by-value specifically in order to avoid having multiple references to mutable state. Cocoa also uses the relationship between pass-by-value and immutability to optimize: -copy on an immutable object returns self, because copies are only distinguishable when mutable.
From a PSU Professor who I unfortunately don’t recall the name of: You can approximate the complexity of a typical object-oriented or procedural program as being the the number of states of the program times the number of places it changes state.
You can intuitively see that this is on the right track: the whole point of our much-loved encapsulation is to isolate these so that you can treat your program as being several separate less complex programs (i.e. (n1*m1) + (n2*m2) + (n…*m…) instead of (n1+n2+n…) * (m1+m2+m…) ).
Immutability changes this equation dramatically. If a function never mutates state, then its complexity is linear in its size, rather than quadratic.
From Sean Parent at Adobe: We tend to think of an array, dictionary, or the like as a single data structure. However, this is only really true in two cases: if the object in question is only referenced from one place, or if it’s immutable. If a mutable array (for example) is referenced from multiple places, then what you have is not a mutable array, but a mutable graph consisting of several edges and a vertex with a mutable array.
These implicit data structures form the large majority of our programs, and our code is largely implicit algorithms operating on them. By using pass-by-value types where possible, we limit object graphs to cases where we actually need graph semantics.
Because you can’t have multiple references to a non-pass-by-reference type, you automatically can’t have references from multiple threads, which makes pass-by-value intrinsically thread-safe. Because concurrent reads are safe as long as no mutations are happening, immutable values are also intrinsically thread-safe.
For these reasons and more, the Swift standard library strongly prefers pass-by-value (structs and enums) rather than pass by reference (classes). String, Array, Set, Dictionary, etc... are all value types, and all support being declared immutable by using let.
Given the usefulness of these concepts, why aren’t we using them more already? My guess is that the answer lies in the fact that mutability is really convenient, and switching to immutability requires changing the way we think about programming. Let's walk through the conceptual leap from a simple mutable loop to the equivalent immutable code.
var randoms = [Int]() for i in 1...1000 { randoms.append(Int.random(in: 1..<100)) }Nothing after this code modifies randoms, so we’d like to make it immutable
let randoms = [Int]() //no error for i in 1...1000 { randoms.append(Int.random(in: 1..<100)) //error }In both cases we’re assigning a value to randoms, but only one of them works. The key distinction here is that initialization is an expression, while mutation is a statement. So, we need a way to write a loop as an expression returning a value. Let’s imagine such a thing:
let randoms = for i in 1...1000 { /* well, this doesn’t make sense anymore, since what we really want is for it to be appending to an array internal to the loop, which we’ll initialize our array constant with */ randoms.append(Int.random(in: 1..<100)) }We’ll encapsulate the mutability of that loop-private array by making a function that runs the loop, and passing a closure as the loop body:
func for(range:Range<Int>, body:(Int)->Int) -> [Int] { var tempArray = [Int]() for i in range { tempArray.append(body(i)) } return tempArray } ... let randoms = for(1...1000) { _ in Int.random(in: 1..<100) }Now the mutability is encapsulated away in its own tiny function and it even still looks almost identical to a regular loop thanks to Swift’s syntax sugar, but at the cost of making a tiny function for each loop we have like this. Let’s generalize that function to be applicable to any loop that initializes an array by running the body of the loop once per element.
func for<S:Sequence, R>(sequence: S, body: (S.Element)->R) -> [R] { var tempArray = [R]() for i in sequence { tempArray.append(body(i)) } return tempArray } ... let randoms = for(1...1000) { _ in Int.random(in: 1..<100) } let squares = for(1...1000) { x in x * x } let repeatedLetters = for(["a", "b", "c"]) { letter in letter + letter }Now we can use our new expression-form loop to initialize any array. One final simplification remains: delete the function! The standard library already contains it under a different name!
let randoms = (1...1000).map { _ in Int.random(in: 1..<100) } let squares = (1...1000).map { x in x * x } let repeatedLetters = ["a", "b", "c"].map { letter in letter + letter }
We’ve now derived the existence of one of functional programming’s mainstays, the map operation, simply from the existence of immutability in the language.