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!