Keyword for immutable instances

I think I've made it clear that I like C++ const, so I don't have a problem with that :slight_smile:

But I do also like @karim's protocol approach. Very Swifty.

Lots of data needs to be immutable or mutable based on how and where it's used in the code, not just basic types like Int or String. Besides the Opinion/MutableOpinion pair, which is just a silly example, think about mathematics, like a Matrix and MutableMatrix. Or think of all the details of a video game, and how some are fixed and some are changeable but they're the same concept. Like you might have a building that's immutable because nothing can collide with it, but another mutable copy of the same building that can be blown up.

I don't think using value types to make such a bimodal class pair "go away" is a universal solution. I'm suggesting that Swift embrace the duality of fixed and mutable objects. My idea would allow coalescing the two mutable & immutable classes which represent the same idea into one class, whose objects are determined to be mutable or immutable in the declaration, much as Int and String are.

If you want the same semantics the language gives String and other value types, you should embrace the pattern they demonstrate. You can achieve the same effect with your own classes by wrapping them in structs that forward methods to internal storage. You even have a choice as to whether to implement CoW, or to treat different copies of the struct as if they were the class itself, by never copying the internal storage.

3 Likes

Seems like a lot of boilerplate to me. Imagine if you had to do that for every mutable Int in your programs, and for every mutable String. I want a language to simplify. I don't want to be distracted by implementation details.

But if the language doesn't support that pattern except for Int, Float, String etc., I think it will confuse anyone who looks at the code.

These types are not special in this regard. Any type can implement the same semantics, using the same tools from the stdlib.

You can check out the Swift's github (should be in public/core). These types are implemented using (mostly) standard Swift. They do go at length to make these standard types not special.*

Even things like String has a lot of optimization to avoid unnecessary copy, and are implemented using Swift. You probably don't need to go as far as they do though. You likely don't need to support different underlying storage types, different encoding, etc.

* Well, there are exceptions, like how String is preferred for string literal, but they're not special in terms of implementation. You can even use those underscored feature yourself. A serious kicker would be how integer types are tightly tied to llvm that they need to use BuildIn, though I don't think that'd really be relevant.


I have no opinion about the feature you're pitching. I don't think it's a consistent model, but that is essentially every pitch. With that said, I want to make sure that you're already familiar with the way Swift does things, and that it precisely does not apply to your use case. In so far, Swift's response to what you explained would be just use value semantic.

1 Like

If we're talking about run-time exceptions when trying to mutate fixed instances, that sounds like it would be a horrendous footgun generator unless any method/function/property that returned a fixed instance had to mark it as such, otherwise how will you know that some object vended by another framework or code you're not familiar with isn't about to trip you up? Defensive guards all over the place?

But then if you do mark return values as fixed or not, you have to propagate that fixed attribute all the way along any methods that take that value because otherwise it won't be provably safe for them to mutate it, and you'll have to propagate it all the way along methods that return it, because it wouldn't be provably safe to return a fixed as a non-fixed. Or, again, you have a ton of boilerplate to guard against fixed-ness when you're mutating.

Not being able to trust that an instance is quite what it says it is, having to code defensively around it etc...sounds really familiar...this sounds just like null references!

@karim's method is very low on boilerplate, type-safe, adds no complexity to the language, and communicates fixedness very clearly. I've used similar techniques in C# projects and it works very nicely.

2 Likes

I don't understand this as a use case. Honestly, no offense intended, but wanting every Int across a program to be locked down in the way you describe seems nonsensical, and not a real use-case.

What I described was one solution to the specific concrete problem you put forward. If you can articulate some additional realistic concrete use-cases, I'm sure there are patterns that can solve those too. And if there are holes surfaced by real examples, then that's something we can (and definitely should) discuss in more depth.

The problem is that the C++ model is unsafe (just like the Obj-C model). The simple fact that you can cast a non fixed instance into a fixed instance (a NSMutableString to a NSString, …) create a big hole.

The receiver of such type can't have any guarantee that it will not change under the hood.

class Foo { 
  private var _bar: Bar
  let bar: fixed Bar
    get() { return _bar }

  func something() {
    _bar.mutatingCall()
  }
}

let foo = Foo()
let b: fixed Bar = foo.bar
foo.something() // mutates bar even if it is fixed

The fact that a lots of classes return NSArray that was in reality NSMutableArray was the source of subtle bugs. So instead of resurrecting an old class of problems from the past, shouldn't we simply use the tools that where designed to avoid them: struct.

1 Like

That's exactly the point of C++'s const methods, which Objective-C doesn't have. A const method is not allowed to mutate the object, and only const methods may be called on a const object, so you do have that promise.

For it to be really safe though, the fixed-ness needs to be declared as early as the initializer. At which point we'd just be having a (Swift) struct with more boilerplate.

1 Like

But you also have the problem mentioned above by @Pampel. Making your code const correct or fixed correct is a kind of cancer that will spread throughout your entire code. Perhaps type inference will reduce the number of times you need to type in fixed but you will need to type it in. Using let and var with structs seems to be the Swift way.

This problem still surfaces in C++ though.

#include <cstdio>

struct Bar {
    int value;
    constexpr void mutatingMethod() noexcept {
        value += 3;
    }
};

class Foo {
    Bar _bar;
public:
    constexpr Foo(Bar b) noexcept : _bar{b} { }
    constexpr auto bar() const noexcept { return _bar; }
    constexpr void doSomething() noexcept {
        _bar.mutatingMethod();
    }
};

int main() {
    auto foo = Foo({42});
    // ref is fixed.
    auto const& ref = foo;
    // This is a compile error.
    // ref.doSomething();
    printf("ref.bar() = %d\n", ref.bar().value);
    foo.doSomething();
    printf("ref.bar() = %d\n", ref.bar().value);
    // Oops! We changed ref!
}

True, const does not guarantee that your object will not change. What it does say is that the things you explicitly do with it will not cause it to change. Dealing with references, I think you have to be prepared to deal with that kind of distinction.

As for it being cancerous, I haven't experienced that as a bad thing. Yes, it is more work than expected sometimes. But I find the result tends to be more expressive and correct.

The problem with C++ const, aside from the fact that it isn't the default, is that it lies to you. If you have an immutable reference to mutable data, the data can still mutate. The Swift solution is: don't give out references to mutable data unless you're willing to have it mutate.

If you really, really want a class that can't mutate, then make all of its instance variables let bindings. Of course, at that point it's basically a struct with extra steps.

Swift already has guards and checks for mutability of Int, String, etc. I'm suggesting extending that to classes.

I don't understand what you're misunderstanding. Can you perhaps show me an example of what you're proposing, so that we're on the same page?

If the signal that bar is fixed is that a single high bit in the object pointer is set, then any attempt to cast it to the mutable type should not unset that bit or removed its fixedness.

But what does this achieve? Why should I care not to mutate it if someone three modules away decides that it is read-only? Why shouldn't I just make a copy when I want to mutate so that nobody outside of my locality notice if the data has ever changed?