Keyword for immutable instances

It would be nice to be able to have a way to ensure that an instance of a class will be immutable after initialization.

For instance, I have a class called Opinion, that after instantiation is always unchangeable. However I have a method called changeTo, which mutates some ivar. I want to be able to do this:

let stubborn = fixed Opinion("ferrets are stinky")
let openMinded = Opinion("ferrets are stinky")
openMinded.changeTo("ferrets smell nice") 

where changedTo is defined as:

mutating func changeTo(_ newOpinion:String) {
    self.opinion = newOpinion
}

So what the "fixed" keyword does is, it disables all mutating class methods.

This seems like a reasonable way to avoid resorting to contorted workarounds to provide the same distinction between immutable and mutable class instances using e.g. structs or protocols.

What happens with the following?

let stubborn = fixed Opinion("ferrets are stinky")
let notSoStubborn = stubborn

notSoStubborn.changeTo("ferrets are smelly")

In that case notSoStubborn would inherit the fixed quality and the changeTo call would give an exception.

Imagine a case where 1000 objects are put into an array, and only two of them are fixed, then the array is randomized and an attempt to made to send changeTo to a random element. The compiler wouldn't be able to know which one is fixed until runtime. Therefore, there has to be a runtime check.

It would nice to also have a compile time check to identify the simple mistakes too.

And of course it would be nice to have an feature where at runtime an object can become fixed such that that can't be undone.

So what happens if your code tries to mutate a fixed object? An exception is thrown? fatalError? Does fixed change the type of the object? How does operator== work with fixed and not fixed objects?

In general, the behavior you're describing isn't useful for classes. (I'm ignoring the performance penalty of the run-time check, which isn't going to be popular.)

It's typical for classes to have private mutable state. For example, a class may want to keep a count of how many times a particular (non-mutating!) method is called for each instance.

The only way to write a class that was safe from problems when locked down (e.g. by your fixed keyword) is to write a class that has no mutable state. In that case, a value type is likely a better choice.

This isn't as likely to be an issue for a value type, because such values can exist in an indefinite number of copies, and it's hard to know how to interpret that kind of mutable state in the face of copying.

2 Likes

What does this achieve? You're restricting a class functionality based on an existence of a fixed reference, which may or may not actually be used (and so be optimized away). Its immutability seems to also tie to the lifetime of that fixed reference, RAII-style.

Wouldn't it be better if the objects in the array adopt value semantic so each time you retrieve the data it behaves as if it makes a copy? Or if the object represents an underlying physical object, wouldn't it be better to use a callback style to enforce the scope of the ownership and immutability (like most of the Unsafe APIs), which also gives you enough room for synchronisation code if needed.

1 Like

I think what is actually more useful in practice is the ability to vend a read-only class reference, through which mutation is disallowed.

Thus, an instance might be owned somewhere, and the owner of the instance can change it. Then the owner can vend read-only references to that instance for other APIs to use, safe in the knowledge that no one except the owner can change the data.

Of course, in Swift today, methods on a class cannot be marked mutating, so there is no obvious way to indicate which ones would be usable through the read-only reference.

Currently, the way to achieve this behavior is with the mutable-subclass pattern as seen in Objective-C. However even that is not a perfect solution, since someone could cast the immutable reference to the mutable subtype, and thus gain write-access to the instance.

1 Like

Maybe we can evolve the subclass technique into two views pointing at the same storage, like how String works. So now we'd get two unrelated classes :thinking:.

I am not, in principle, against the idea of a way to use a language feature (such as a keyword) to generate an on-the-fly immutable version of a class, but you will need to account for a lot more details before you can advance from the pitch phase.

The most significant, off the top of my head, is how the compiler knows which methods are mutating and which are not.

In addition, it would be necessary to have a way to query this readonly nature at runtime, to be able to avoid crashing. Alternatively, it would be necessary for this feature to only be checked at compile time, but that would almost certainly allow bugs, as the compiler can be tricked.

And what happens at module boundaries? If I pass a fixed object to an API that expects to be able to modify it, what happens? Is that simply a programmer error, much like passing an immutable subclass would be?

1 Like

I would expect fixed to work like const in C++, which is the thing I miss most about C++ when working in Swift (and any other language that doesn't have it). It's another way of being able to communicate your intent to the compiler: "this object is not going to change".

To answer that question more directly - if it's analogous to const, then attempting to mutate it would be a compile time error. It becomes a different type.

One of my favorite uses for const is in function parameters, where it enables you to say "the object will not be mutated by this function". You can even pass in a normal reference, but as far as the function is concerned it is const. Then you can also have const methods, which are pretty much the opposite of mutating methods on Swift structs.

OP provides an example where there's an array containing a mixture of fixed and non-fixed values and then a mutating method is called on all the objects in the container. To make that a compile time error it would have to be illegal to add fixed and non-fixed values to the container or three would have to be a way to distinguish between the fixed and non-fixed values when iterating the container. I don't think how that works has been made clear.

Yes an exception. The same as if you tried to mutate and NSString in Obj-C.

I think mutating should be applicable to class methods and it would start out as a good-faith way of declaring that no state will change.

As the compiler improves though, it will do some smarter things:

  1. Scan the method's code for obvious changes to ivars.
  2. At runtime, take a snapshot of an object when you enter the method and compare the method at the return point(s), and generate an exception if different.
  3. For large objects, or very important objects, the OS can use the CPU's virtual memory write-prohibition to prevent writing to the object memory; but IIRC this locks a 4kB area (on Intel).

My idea was to use a single bit or boolean to signify that an object is read-only. So it could be a 1-byte boolean or one of the unused high bits of the 64-bit object pointer address.

Regarding the array example, I only gave that example to demonstrate that the fixedness check has to be done at runtime for sure, in addition to the compile-time check, because the compiler can't predict the result of a random shuffling of an array.

But I suppose there would be use cases there mixing fixed and non-fixed in a container would be useful. Think of JSON and how you see a variety of object types in a JSON dictionary.

That said, if I could say in code:

let json : fixed [String:Any] = ...

and have that mean that every object in the JSON is guaranteed to be immutable, that would be appealing to people who think in terms of contracts and testing.

Personally I still think compile time enforcement is more useful - final should be part of the type. For the array example, you would either have [fixed String] that can hold both mutable and immutable instances (but they all appear immutable), or regular [String] that can only hold mutable ones. And I suppose being able to test someFixedString as? String to see if your instance is mutable after all would be a reasonable thing.

Runtime enforcement reminds me of the throw() specifier in C++, which was also enforced at runtime and was eventually pretty much abandoned. (Disclaimer: I don't know the full story there, and I'm sure there were other contributing factors to its demise.)

I see a lot of discussion about "immutable" class instances, but it's nowhere near obvious what that might mean.

Is a class with no mutable public APIs immutable even if it has private mutable state? What about a class with an immutable reference to a second class that is mutable — is the first class mutable or not?

The fact is that constraining classes to immutable behavior is a huge philosophic shift within Swift in particular and OOP in general. Leaving at least some mutability as an implementation detail of the class is very much an accepted part of what classes are.

Beyond that, I don't think we've seen any actual use cases for immutable class instances. Genuinely immutable values can be had via a struct.

What are the use cases for immutable classes?

(Note that Obj-C has use cases for things like NSArray vs. NSMutableArray, or NSString vs. NSMutableString, but we've already made those cases go away in Swift by using value types for arrays and strings.)

3 Likes

I think we would need to also add the concept of fixed member functions (or require the use of mutating as with structs, which would be more consistent but possibly more onerous and maybe even source-breaking). Then you know which methods you're allowed to call on a fixed instance, and within those methods you would also be restricted to fixed or non-mutating methods on your properties.

As for use cases, I've had some situations where I started out making something a struct but ended up switching to class for various reasons - wanting inheritance and/or polymorphism because of how my data types overlap, needing to inherit from NSObject for use with some Cocoa API, etc. The instances are still conceptually data objects, so having it well-defined when those objects can change is valuable.

Which would prevent the declaration of fixed function that try to mutate private members (like caching a result on first call, and then return it on subsequent calls).

Unless you plan to also support explicitly mutating members that would be modifiable from a fixed function. But at this point, we are just reinventing C++ const.

Not all const, just the other half :wink:.

The way I've achieved this pattern in the past (fairly cleanly, IMHO) is to have the "mutable" class as described, and the owner vend a read-only struct (via method on the class instance) representing the values, with both class and struct conforming to a protocol which describes the read-only properties they both provide.

Then the class owner can mutate the values, and non-owners can access the current state of the values via the read-only struct.

To me, this seems a clear expression of the described contract, and honestly very little boilerplate or extra hoops are involved.

In some cases where it made sense, I've even made the state-managed mutable class private, truly locking down all mutations to the scope of the instance owner.

7 Likes
Terms of Service

Privacy Policy

Cookie Policy