Skip to content

Is mutability always bad? #30

@ahmedk92

Description

@ahmedk92

Consider the following Swift struct definition:

struct Person {
  var name: String
}

If you think the var in the struct above should have better been declared as let, then this article is for you.

Immutability has been one of the best practices in class design. That is, everything should be immutable unless mutability is actually needed and cannot be avoided. This can be seen in modern languages like Swift and Kotlin where they have keywords to control mutability like var, let, val, …etc. This have evidently worked well, and everyone started to apply it everywhere possible, even when making Swift structs. I used to think the same, until I saw this tweet from Nick Lockwood.

Screen Shot 2023-02-12 at 8 42 00 PM

It was an eye-opener for me. Recall the struct example above. The issue with it was that seemingly the properties could be mistakenly mutated from anywhere in the code, and thus cause bugs. Except that it’s not that easy to happen when working with value semantics. One reason to this surely is that sharing value types causes them to be copied, and the mutable copies will only be mutated in isolation to the others. The other reason - which this article is about - is that property mutation is possible only if the encapsulating instance is also mutable. Let’s make a small experiment. Let’s declare an instance from that struct:

let person = Person(name: "Adam")
person.name = "Ali"

That code won’t compile, which is the same effect as if the name property was declared as let. That is because the struct instance is declared as let, although the property is declared as var.

Let’s make another small experiment. Let’s do the opposite this time; which is, we will declare the name property as let, but the struct instance will be declared as var, and try to mutate the name property again:

struct Person {
  let name: String
}

var person = Person(name: "Adam")
person.name = "Ali"

That won’t compile either, which makes it safer than if the name property was declared as var, right? Not really. The following compiles:

struct Person {
  let name: String
}

var person = Person(name: "Adam")
person.name = "Ali" // ❌ Doesn't compile
person = Person(name: "Ali") // ✅ Compiles just fine

We effectively succeeded in mutating the name property, although it is declared as let. And what I mean by “effectively” here is that although the name property was not mutated the way you’d conventionally expect, the code that will be using the mutable person copy above will get the new name value anyway as if it was conventionally mutable.

Also, using var by default in structs brings some convenience when manipulating such values. Consider:

var frame = makeFrame()
if style == .centered {
  frame.origin.x = parentBounds.midX - frame.width / 2
}

If we assume CGRect had let properties, we would need to do the following:

var frame = makeFrame()
frame = CGRect(
  x: parentBounds.midX - frame.width / 2, 
  y: frame.minY, 
  width: frame.width, 
  height: frame.height
)

And you can see where it is going if you really want to eliminate entirely the use of var in this example.

Conclusion

The takeaway here is not against the immutable-by-default principle. Rather, it feels like this is similar to some design patterns that don’t really fit in a language that has superior features the solve the same problems. This idea was discussed in this famous article by Paul Graham, especially the note about Peter Norvig findings:

Peter Norvig found that 16 of the 23 patterns in Design Patterns
 were "invisible or simpler" in Lisp

And I find this a great opportunity to express some admiration of some of the philosophies of Swift. I find value semantics one of the most unique and impressive features of Swift. What I like about it is that it combines the safety of the immutable functional programming style with the intuitive convenience of the imperative style. You can notice this best-of-both-worlds philosophy in other features as well:

  • Async/await enables doing async programming in a familiar serial structured style (compared to the somewhat steep learning curve of the functional reactive style of Combine for example)
  • Type inference combines the cleanliness of dynamically typed languages with the safety of statically-typed languages

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions