Two classes, one concept (mutable & immutable)

I have two classes

  • Opinion
  • MutableOpinion

I want to use these in the same facile way that one uses String or Int e.g.

let stubborn = Opinion(.NoLikeCookie)
print(stubborn.oppositeOpinion)
var openMinded = MutableOpinion(.LikeCookie)
if day == .Tuesday {
    openMinded.changeTo(openMinded.oppositeOpinion)
}

Of course the mutable class is a subclass of the immutable one that simply adds the changeTo method.

How can I inform Swift that I want to use this pair of classes, one immutable and the other mutable, using the casual let / var syntax?

String and Int are structs

1 Like

Are the other mutable/immutable pairs also structs?
Dictionary, Set, and so on?

Yes.

Are the other mutable/immutable pairs also structs? Dictionary, Set,
and so on?

Keep in mind that these structs are backed by shared storage using copy-on-write.

Share and Enjoy

Quinn β€œThe Eskimo!” @ DTS @ Apple

This is the design of Foundation in Obj-C where everything is a class. You have NSString and NSMutableString and many other pairs like. (Maybe that's what you're thinking of.) The swift standard library is based on structs and using let and var to control mutability. So you only need one struct in your example.

It's important to keep track of the difference between mutable and immutable values, and read-only and read-write variables.

let and var declare variables, and so they don't directly represent mutability.

For value types (e.g. Array) there aren't mutable and immutable variants. Arrays are mutable, but they can't actually be mutated when used in a read-only context. The compiler can use that fact to handle mutable values as if they were immutable. (In particular, all copies of an immutable value are equivalent, which allows optimizations.)

For reference types, the references themselves (that is, more or less, the pointers to the objects) are immutable, but the objects can be mutable or immutable**. When used with read-only or read-write contexts, that makes 4 total (possible) accessing scenarios. That complexity is one reason why Swift prefers that you use value types when there is a choice.

Unless you parse these things correctly, you'll find it very hard to talk usefully about mutability.

** Actually, they can be anything in between, too. For example, an object may not have API to mutate it, but it may secretly have mutable state inside it. This sort of complexity has traditionally been a major source of bugs in (say) Objective-C.

2 Likes

So if I have a struct in Swift that I defined, can I make it both mutable and immutable such that I can use let/var with it?
Surely Swift doesn't limit that syntax to only its internal struct types?

let/var is tied to mutability, which applies to every type. But, there are differences between what mutations mean to struct and class.

In particular, classes mutate when they are replacing self, which can occur in special circumstances. Normal class methods/accessors can not mutate because you're still operating on the same instance of the class. OTOH, struct (and enum) mutates when their storage changes their values. You'll see many methods/accessors marked as mutating due to that.

All that let does is restricting the use of mutating function. So you can use a struct and mark any relevant functions as mutating. Standard collections use this tricks a lot to implement its CoW semantic, by having class-bound storage wrapped inside a struct.

3 Likes

A struct can have both mutable and immutable behaviors. If it has no mutable behaviors, the struct's values are always immutable.

For example:

struct A {
  let v0: Int
  var v1: Int
}

The v0 property is an immutable behavior (because you can't change the struct via the property), while v1 is a mutable behavior (because you can, larger context willing). Now put this in context:

  let a0 = A(v0: 0, v1: 1)
  var a1 = A(v0: 2, v1: 3)

You can never assign to .v0 because it is an immutable behavior:

  a0.v0 = 99 // error
  a1.v0 = 999 // error

You can assign to .v1 in a1 because it's a mutable behavior of a read-write variable:

  a1.v1 = 555 // OK

but you can't assign to .v1 in a0 because it's a read-only variable:

  a0.v1 = 55 // error

Informally, in Swift we tend to call a0 immutable, but what we actually mean is that we can't use its value's mutable behaviors, because it was declared with let.

Would this trick of using mutating work for classes as well, such that I could tag the changeTo method of Opinion as mutating and then an immutable Opinion wouldn't allow calls to that method?

Normally, you can't mutate a class using methods as you're still working with the same instance of the class throughout. You'll even get errors if you try to mark class methods as mutating. There are some workaround when you work with protocol, though. Specifically, you can replace self in protocol extension, which does pretty much as you'd expect (and you also need to mark the function as mutating). So you can do something like:

protocol P { }
extension P {
  mutating func update() {
    self = ...
  }
}
class C: P { }

Though, I wouldn't really recommend that. The point of class (in Swift) is to signify that it has some sense of identity. You'll easily confuse users if you use class that changes it's identity every time you update a value. Better to use struct with class-backed storage as others pointed out.

Classes are a bit different. If you have a variable (let or var) referring to a class instance, the variable actually contains a reference to the instance (basically, a pointer to the instance's memory, which is elsewhere).

As before, a let variable is read-only, so you cannot assign a different reference to it, and a var variable is read-write, so you can assign a different reference to it. Note, though, that you are only changing the reference held in the variable. You are not changing anything in the instance to which the variable points.

Let's take an example context to make this clearer:

class C {
  let v0: Int
  var v1: Int
}

  let c0 = C(v0: 0, v1: 1)
  var c1 = C(v0: 2, v1: 3)
  c1 = C(v0: 4, v1: 5)

You can't change c0, of course. It's read-only, so it always points to just that first instance of `C.

You can change c1. Above, it initially points to one instance, then later points to another.

Note that we created a total of 3 instances of C, but we didn't change any of them. We only changed a variable containing references to them.

Given the above, we can do this:

  c0.v1 = 44
  c1.v1 = 66

Both of those work, because we're allowed to invoke mutable behavior on any class instance regardless of the variable that holds the reference to the instance (i.e. both c0 and c1 work).

That's because the instance isn't in the variable, only a reference to the instance. That's what it is to be a "reference type".

You might wonder how you could make an instance immutable. The answer is that you can't, if it has any mutable behaviors (like the property v1). The nearest you can get is to split the class into two classes, an immutable one (i.e. a class with no mutable behaviors) and an immutable subclass. (Now we've gone round the circle to meet the question that started this thread.)

This inability to control the mutability of class instances directly is one major reason why classes are are regarded as treacherous in Swift, and generally discouraged when a choice between class and struct is feasible.

In Obj-C, for comparison, where all the interesting things like arrays are classes, you have to be very aware of which classes are immutable, which ones have mutable subclasses, and which ones are just mutable without having an immutable superclass. Yes, this is a big cause of bugs in Obj-C, at least for the unwary.

2 Likes

This inability to control the mutability of class instances directly is one major reason why classes are are regarded as treacherous in Swift, and generally discouraged when a choice between class and struct is feasible.

That seems like a false reaction, or an overreaction. It's like saying you have if a car has a leather interior you consider it treacherous, because you can't change it. But you can change it. No need for drama.

It sounds like Swift is just not a finished language. There should be a way to ensure that instances are immutable or locked down. Using structs all the time is resource intensive and costly. That's why references were always considered a good idea. If I have a movie object, and inside of it is cached 10,000 frames, it would crazy to implement that as a struct. But it would also be a bad idea to be able to let some code mutate it.

Therefore there's a need for a keyword that ensures mutable ivars can't be changed, or something similar.

Perhaps, but you're missing the point. A struct containing a single object (class or otherwise) will have the same layout as the object it contains. If it contains a single (reference to) class, referencing that struct will be the same as referencing that class directly.

The important point is to use struct as an API boundary while using a class for storage so that you can share the underlying storage when they store the same information. This is how Swift API signifies that this storage has no sense of identity, and what important is the stored content.

Class-back storage is how Array, Data, etc., implements their value semantics. They share the same storage when you move/copy them around. When mutating, they allocate a new buffer only if the data is shared with other variables and directly mutate the storage otherwise.

1 Like

Is there a manual page or primer that explains this? It's unclear to me what problem this solves.

Do you mean why "value semantics (wiki) are desirable?" I'm pretty sure they long predate Swift as semantics that are easier to understand and locally reason with. So :woman_shrugging:.

Or do you mean why "we use class-backed struct?" It's just to implement efficient value semantics. Copying array every time is, as you said, quite wasteful. So they instead do Copy-on-Write (CoW)–Optimization Tip on Swift github.

In all seriousness, it's an unwritten rule that struct uses value semantics, and class uses reference semantics as it would surprise Swift programmers otherwise. You can then do just about anything underneath, wrapping classes in struct, wrapping struct inside class (???), using unsafe constructs, etc.

1 Like

You can also use access control like private and private(set) to control mutability of a type's data.

I've never considered writing an immutable/mutable pair of classes/structs like is used in the Obj-C standard library for swift code or even Obj-C code.

Terms of Service

Privacy Policy

Cookie Policy