Replies: 1 comment 4 replies
-
Hello @mbrandonw,
This is an interesting scenario: thanks for discussing about it. Is it possible to make the motivation explicit? What do you mean with "we want the functionality of It looks like you aim at this behavior: extension Player: PersistableRecord { }
let player1 = Player(...) // let
try player1.insert(db) // id is nil in this case
var player2 = Player(...) // var
try player2.insert(db) // id is not nil in this case This is indeed not how the current API works. If you want insertion to set the id, then you opt in with extension Player: MutablePersistableRecord { }
let player1 = Player(...) // let
try player1.insert(db) // does not compile
var player2 = Player(...) // var
try player2.insert(db) // id is never nil The fact that the above And if you don't want insertion to mutate the record, then you opt in with Also consider that the current api makes sure that all defined persistence callbacks are called (not only With your suggested change, the user has to care as soon as a callback must be called in both mutating and non-mutating scenarios. It is not trivial: One mutating and one non-mutating callbackprotocol MutableP {
mutating func callback()
}
protocol P: MutableP {
func callback()
}
struct Convoluted: P {
@_implements(MutableP, callback())
mutating func mutatingCallback() {
print("Convoluted.mutatingCallback")
}
@_implements(P, callback())
func nonMutatingCallback() {
print("Convoluted.nonMutatingCallback")
}
}
func run() {
// Convoluted.mutatingCallback
var x: some MutableP = Convoluted(); x.callback()
// Convoluted.nonMutatingCallback
let y: some P = Convoluted(); y.callback()
}
run() It is desirable to drive users into writing this kind of code? Debatable.
This would be unsound. Non-mutating is a subtype of mutating, not the other way around. A generic method that accepts all records must deal with |
Beta Was this translation helpful? Give feedback.
-
Due to how Swift resolves overloads, there is the potential for a gotcha when using
insert(db)
. Consider the following record type:Note that
didInsert
is provided in order to update theid
after insertion.With that it would be reasonable to write code like this:
This works just fine and we can be assured that
id
is safe to force unwrap because of thedidInsert
.The problem is if someday in the future we decide that we want the functionality of
PersistableRecord
and so we upgrade ourPlayer
type:We have unfortunately now created a problem everywhere we are doing
player.insert
and would need to audit all usages. The problem is thatplayer.insert
now resolves to the non-mutating version ofinsert
, and through that version the callbacks are not invoked and the ID is not updated:Even though the
player
is avar
, Swift is still preferring the overload onPersistableRecord
overMutablePersistableRecord
because it is more specific.Here is a toy example of how this resolution problem manifests:
Both
x.foo
andy.foo
resolve to thefoo
defined onP
. One might hope thaty.foo
resolves toMutableP
'sfoo
sincey
is mutable, but that's not how the resolution works.If this is thought to be a legitimate problem, then one possible fix is to just mark all of the non-mutating methods as
@_disfavoredOverload
. Here is the fix for the toy example above:Now
x.foo
resolves to the non-mutating method andy.foo
resolves to the mutating one sincey
is mutable. I guess technically this could be a breaking change, and it is debatable whether it is a bug fix or not.Another path forward, though far more invasive, is to consider whether the protocols should be flipped. Should
MutablePersistableRecord
inherit fromPersistableRecord
, much like howMutableCollection
inherits fromCollection
? I am not well-versed enough in the library to know if that makes sense, but it’s just an armchair observation.Beta Was this translation helpful? Give feedback.
All reactions