To var or let struct properties?

I tried searching for guidance on this topic but all I could find were introductory discussions about var, let, and mutability in Swift. I've been doing some reading on OOP vs FP, and trying to better understand the benefits of immutability and when to best require it.

Our code base has a lot of model data stored in structs where all the properties are let, and some structs where the properties are var. I find the latter easier to use, in practice, because it means I can change one property of a struct without jumping through a lot of hoops.

Case in point: I just added code in our app to allow the user to change their profile image. After writing the image to the server, I update the user model's profile image URL, and SwiftUI ensures that the display is reloaded to reflect the new image. Unfortunately, the user struct was one of the all-let ones, and in the interests of time, I modified that one property to be var.

Which got me thinking, is there a good rule of thumb as to when one should use let or var in struct properties? Using var lets the type user dictate if it's mutable or not. It also makes it easier to create a copy of an immutable instance and update a subset of properties (I saw a talk about lenses that let you do this concisely with immutable structs if they all had memberwise initializers, but we don't have that).

4 Likes

I tend to use var when struct properties are independent of each other. So when I change one property, the overall struct should still make sense.

When not all combinations of properties make sense I tend to hide member-wise init and use let for properties. This way it is impossible to create a wrong combination of properties when using this struct.

struct Pair {
    var first: Float
    var second: Float
}

var pair = Pair(first: 1, second: 2)
pair.second = 3 // ok

struct Animal {
    let name: String
    let legs: Int

    private init(name: String, legs: Int) {
        self.name = name
        self.legs = legs
    }

    static var dog: Animal {
        return Animal(name: "dog", legs: 4)
    }

    static var spider: Animal {
        return Animal(name: "spider", legs: 8)
    }
}

var dog = Animal.dog
dog.legs = 6 // Cannot assign to property: 'legs' is a 'let' constant
6 Likes

A struct is always strictly immutable, because "mutating" it means (semantically) copying it, updating the copy and assigning it to a new variable. The reasoning behind using var or let on a struct properties has to do with the invariants that one wants to preserve (if any), thus, when talking about "mutation" for a struct, what we mean is the ability to produce an instance of a struct with arbitrary values for the properties.

The litmus test to me is to ask the following question: is the struct equipped with a memberwise initializer that only assigns values to properties, without extra logic or validation? If yes, there's no reason for the properties to be let, they should all be var.

For example, suppose you have this:

struct Person {
  var name: String
  var age: Int
}

This struct has an automatically synthesized memberwise initializer that will only assign values to properties, without extra logic: therefore, all properties should be var. One might think that this can't be true in general: what if I don't wan't the name to change? Because of the memberwise initializer, you can always build another instance of Person with all properties unchanged but the name, so there's no point, even within the context of a mutating func on the struct, because you can assign self there.

But what if a Person is built from an underlying model, and has not memberwise initializer?

struct Person {
  let name: String
  let age: Int

  init(underlyingModel: PersonModel) {
    // perform logic and validation
    // assign properties
  }
}

well in this case you can't just build a Person with any name or age but it must come from an underlying model, and the specialized init must be implemented in order to preserve some invariants. In this case the properties should be let, but some could still be var if mutating them doesn't break any invariant.

If you need to be able to mutate properties privately, within the context of the struct, in a way that will preserve invariants (because you are encapsulating logic in the struct, and you know what you're doing), you can use private(set) var in order to prevent uncontrolled mutation from the outside:

struct Person {
  let name: String
  private(set) var age: Int

  init(underlyingModel: PersonModel) {
    // perform logic and validation
    // assign properties
  }

  mutating func updateAge() {
    // update the age in a invariant-preserving way
  }
}
8 Likes

I would say, to a first-order approximation, all struct properties should be var.

If there are invariants which must be maintained, then access control such as private(set) should be used.

Only in very specific situations where particular properties need to remain constant even when the rest of the struct is mutated, does it make sense to have let properties. A unique identifier, for example, might warrant the use of let.

8 Likes

let it be. i always use let unless i need var. indeed i have to jump and change it from let to var if i need to mutate the property. unfortunately i didn't establish a good practice of changing it back from var to let once the code is reverted to not require var-ness anymore. compiler warning would be appreciated ("the property marked var but never mutated").

1 Like

Agree. There are reasons why a stored property should be let just like there are reasons why a computed property should be get-only. However, in the case of modeling data, I am hard-pressed to think of a scenario which would be an exception to the rule of thumb that all properties should be var.

6 Likes

example scenario where let prevents a typo:

struct Person: Decodable {
    let name: String
    let dob: Int
    var expanded: Bool?
}

var p: Person = ... e.g. from json
// apart from expanded field Person shall be immutable
p.expanded = ...
p.dob = curDob // a typo here, should be "curDob = p.dob"

The issue with var is that it adds cognitive load and investigation time:

var, eh? Why might someone need to mutate this property?

shift-command-A to "Show Code Actions", let's see who the "Callers" are…

…Wait. There are no callers? Even in @testable-tests?

…So it's a constant. Should it even really be an instance variable? Let's check to see if it's set by any initializers…

It's the same issue with not using private or private(set). var by default makes code fast to write and requires no forethought about mutation, but that time will be lost later, potentially exponentially.

If you're going to have something be a var, then you should either mutate it in its own module, or in your tests. Otherwise, it won't appear deliberate.

The test may be as simple as just using a setter, to ensure it is public, as long as it is documented in the test where that feature was requested.

3 Likes

A key concept behind the use of any technique for the separation of code into discrete units, such as the use of types to encapsulate data, is the separation of concerns. This is why separating code into discrete units is helpful in the first place—so that there is less to reason about at any one time.

As you describe it very well, attempting to “prevent typos” by declaring stored properties with let and then having to go back to change the definition of your type based on how you later use it violates the separation of concerns here.

Semantically, there is no reason why a model of a person wouldn’t require at some point being able to change the name or date of birth. That sort of question is within the scope of the type’s concerns.

3 Likes

in real world a person can change name, but neither gender at birth nor date of birth (typos in data aside). a model type that reflects these real world constraints makes total sense. furthermore if the app in question doesn't allow name change within the app - name is as immutable as dob for this app (a different app might allow name change - there name would be mutable). if the only way to populate the person object is from some external json i don't see why dob and name fields need to be mutable. i deliberately showed an example that prohibits using let for the whole person value (to change the expanded property, if not that the whole person value would be immutable). thus the person is modelled as var and all of its fields unless those which need modification are modelled as let.

struct Person {
    let dob: Int
    var expanded: Bool = false
}
var people: [Person] = ...

var person: Person

let e = 2.718281828

above shows a few data models. that one is an array of structs of fields, another is a struct of fields, and another is just one value of type Double - doesn't make much difference. my general guideline - always use let unless you really need var. if in doubt and quickly sketching an API - use let by default, compiler would prompt you if the let thing is trying to be mutated - at which point you decide whether the bug is the mutation itself or whether the thing is actually variable. keeping your app state as immutable as possible keeps your app as pure as possible.

I disagree.

There is a large, although subtle, difference between a local variable, which holds a concrete instance of a value, and a stored property of a struct, which describes the shape of a certain type of data.

The declaration of a local variable uses let or var to specify whether the instance can be mutated. This is local control: the decision about mutability takes place in the same scope as any potential mutations.

The declaration of a struct defines the ways in which instances of that struct can be used. This is non-local control: the author of the struct controls what users can do with their own instances of it.

• • •

Declaring a struct property with var says, “You can modify this part of the value on your local vars, if you like.”

Declaring a struct property with let says, “You cannot modify this part of the value, not even on your own local vars.”

That is quite a restrictive statement. It amounts to the author of the struct deciding, “If someone creates a mutable instance, I still won’t let them modify this part of it even if they want to, despite the fact they declared their instance with var.”

That requires a strong reason.

• • •

The decision about mutability of a concrete instance rightly belongs to the local scope where that instance was created. The programmer who declares a local variable is in the best position to decide whether it should be mutable or immutable. The author of the struct should not lightly prevent them from doing so.

Only when the author of the struct knows that letting users modify a property would cause problems, such as broken invariants, does it make sense to prevent that.

And even then, those properties should still generally be declared private(set), so that the author of the struct can provide functions which perform mutations while maintaining the invariants.

The situations where let makes sense for a stored property of a struct are vanishingly rare.

Declaring a struct property with let means, “There is no possible situation in which anyone, anywhere, could ever have a valid reason to change this part of the value of any instance of this struct, even an instance they themself created with the express purpose and intention of changing its properties.”

7 Likes

i strongly disagree. i totally believe that type is king. if it decides that its users can't mutate its variables (after construction) users must not. in other words whether a field of type is mutable or not is solely discretion of the type itself. and if it wants so it might even prohibit you to construct a value of a variable of your choice:

struct S {
    let alwaysOdd: Int

    init(mustBeOdd: Int) throw {
        if (mustBeOdd & 1) == 0 { throw ... }
        alwaysOdd = mustBeOdd
    }
}

this type decides it's "alwaysOdd" field must always be odd, and it is not up to a user of this type to disagree.

1 Like

If it is possible to create an instance with a given set of properties, then definitionally it is also possible to replace an existing instance stored in a var with that new instance.

The only question is, can the programmer who created the var directly mutate their existing instance to have the properties they want, or are they forced to construct an entirely new instance from scratch.

If the author of the struct declares such a property as let when it could have been var, the only effect is to require unnecessary boilerplate at the point of mutation.

When SomeStruct.property is a var, you can do this:

var x = SomeStruct(property: a)
x.property = b

But if it is a let you can get the exact same result, only the code is longer, not as readable, and potentially less efficient:

var x = SomeStruct(property: a)
x = SomeStruct(property: b)

• • •

In any struct where it is possible to do the latter, there is objectively no reason to prohibit the former. They achieve identical results, and the direct mutation is better code.

3 Likes

just step back a little and consider the above "always odd" example.
you'll not be able to construct SomeStruct(property: 0) to begin with, let alone change its property to x.property = 0. type is always king, it rules the show.

That should be written with private(set), and there should be mutating functions which allow modifications that maintain the invariant.

imagine 20 fields like this. and some invariant that must be withheld across all them. you'll end up with a bunch of throwing mutating function parallel to a bunch of private(set) properties... within each you'd call something that.. might throw. not a good design. especially if you are doing this just... for the sake of a possible mutation of a field ... something the user of the type might not even need!

I agree. That would not be a good design.

A good design would have simple types, each with a single responsibility.

While I'm sure there's some merit in making things immutable, I also think that defaulting properties to let is a pretty aggressive practice, warranting strong reasoning.

I'm not sure if it's worth all the hassle compared to var-by-default when we seem to be discussing rules of thumb. Especially when there’s virtually no difference compared to private(set) properties (with accessors).

1 Like

ok, please show the equivalent that uses mutable fields.

struct UnitVector {
	let x: Double
	let y: Double
	let z: Double
	// or 20 fields for 20 dimentional vector

	init?(x: Double, y: Double, z: Double) {
		guard abs(x*x + y*y + z*z - 1) <= eps else {
			return nil
		}
		self.x = x
		self.y = y
		self.z = z
	}
}