Optimal way to scope mutability with Swift 6.4

Hello!

First of all apologies if this topic has been discussed before.

Working on a highly concurrent code base I was tempted to make all my structs completely immutable. All members are other constant structs and are declared with let all the way down.

Of course this made the occasional mutation very verbose as I basically had to recreate the entire hierarchy of the value only to make a tiny change deep down within the structure.

So I whipped up a little protocol to simplify the process:

public protocol MutableProxy: ~Copyable {
    /// Explicitly bind the proxy back to its immutable layout type
    associatedtype Target: Mutable where Target.Proxy == Self

    init(_ target: Target)
}

public protocol Mutable {
    /// Bind the target back to its non-copyable proxy.
    ///
    /// While cannot be enforced, it is highly recommended that the proxy itself is of an ~Copyable type.
    associatedtype Proxy: MutableProxy, ~Copyable where Proxy.Target == Self

    init(_ proxy: consuming Proxy)
}

extension Mutable {
    public func withProxy(_ mutate: (inout Proxy) -> ()) -> Self {
        var proxy = Proxy(self)
        mutate(&proxy)
        return Self(proxy)
    }

    public mutating func mutate(_ mutate: (inout Proxy) -> ()) {
        self = withProxy(mutate)
    }
}

I made a point of leveraging ~Copyable as it should make it harder to misuse the protocol. But as it turns out, seems like you can't enforce non-conformance of the Proxy associated type to the Copyable protocol.

Either way I made my own structs provide a ~Copyable proxy. Here's one such example:

extension Transaction: Mutable {

    public init(_ proxy: consuming Proxy) {
        version = proxy.version
        locktime = proxy.locktime
        ins = proxy.ins
        outs = proxy.outs
    }

    public struct Proxy: MutableProxy, ~Copyable {

        public init(_ target: Transaction) {
            version = target.version
            locktime = target.locktime
            ins = target.ins
            outs = target.outs
        }

        public var version: Version
        public var locktime: Locktime
        public var ins: [Transaction.Input]
        public var outs: [TransactionOutput]
    }
}

Now I can mutate at various levels at will like so:

var spendingTransaction = …
spendingTransaction.mutate {
    $0.outs[0].mutate {
        $0.value += 1000
    }
}

Which is great except I'm still able to leak the proxy which is unfortunate:

var spendingTransaction = …
var proxy: Transaction.Proxy?

spendingTransaction.mutate {
    // Escapability test
    proxy = consume $0 // If the proxy wasn't ~Copyable (unenforceable) we could even assign by copy.
    $0 = .init(block2.transaction)
}

So I guess my question is… can an implementation like this be improved in Swift 6.4? I tried using the experimental lifetime attributes but had problems applying them to the initializers.

Is this whole exercise in immutability totally pointless/unidiomatic and I should make all my struct members var and call it a day?

Any help/tip/opinion I will greatly appreciate.

Thanks!

1 Like

In my opinion, yes. The only strong reason to make your struct members immutable is if you want to forbid changing one member without updating any others, or if some values for a property would be invalid and your init enforces that. (And these are arguments for private(set) more than they are for let.) There is no performance benefit to it since the struct will be stored in a containing let or var anyway. It is a weird way in which the rules for structs are different from the rules for classes and for locals and for globals, but that's because…well, the thing I already said: the struct will be stored in a containing let or var anyway.

(If you have a public memberwise initializer, you definitely don't need let properties.)

10 Likes

Was there ever a pitch or discussion for a set throws accessor?

It looks like SE-0310 briefly discussed effectful setters but this was out of scope at the time.

1 Like

Just want to point out that this doesn't work with private(set).

I wonder why leaking proxy is an issue? IIUC what you were looking for is an approach to avoid modifying proxy by mistakes. I doubt that there is such an approach. Even if you declare all properties with let and reconstruct the entire hierarchy every time, you may also make mistakes by copying a wrong value.

I had similar convension in my code. One example is to convert between in-memory data and serialized data. Another example is two types sharing the same underlying type and I converted them back and forth for modification. In my experience having such code always make the code unnecessarily complex (not because the code is difiicult to write, but because it's difficult to browse). So I'd suggest to just use var unless you have strong reason.

1 Like

Thank you all for the great feedback.

I decided to make all members variables and simplified mutation across the entire project.

At the beginning I thought about enforcing certain rules in the initializer – like checking that all transactions have at least one input and one output – but even that ended up being a total overkill.

I guess I thought being more restrictive (let members and ~Copyable ~Escapable proxies) would lead to better guarantees for the rest of the codebase but I can't really think of any clear advantages.