Closed rock88 closed 2 years ago
Thanks for taking the time to open a PR, and sorry for the delayed response!
We definitely see the appeal of such a shorthand, but unfortunately, because of current limitation in Swift's accessors, it introduces nil
writability, which is a bit confusing.
foo[/Foo.bar] = nil // no-op
What we'd really want is behavior like optional chaining, where the getter is optional and the setter is non-optional:
struct Foo { var bar: Int }
var foo: Foo? = .init(bar: 123)
foo?.bar = 42 // ✅
foo?.bar = nil // 🛑 forbidden by the compiler
But this just isn't possible in Swift today. 😔 Because of this, for the moment I think we want to avoid introducing such an API to the library. We're definitely open to further discussion, though. If you're interested, please open a GitHub discussion where others can weigh in and offer their suggestions.
@stephencelis maybe something like this could work
extension AssociatedValueAccessible {
public subscript<Value>(casePath: CasePath<Self, Value>) -> Value? {
casePath.extract(from: self)
}
@_disfavoredOverload
public subscript<Value>(casePath: CasePath<Self, Value>) -> Value {
@available(*, unavailable, message: "only available as optional getter")
get { fatalError() }
set { self = casePath.embed(newValue) }
}
}
var foo = Foo.bar(123)
foo[/Foo.bar] = 42 // ✅
foo[/Foo.bar] = nil // 🛑 forbidden by the compiler
let bar: Int = foo[/Foo.bar] // 🛑 forbidden by the compiler
I think this might be better suited for OptionalPaths
though, which should allow setting nested values.
I'm not sure if the above is a good idea. More just sharing in case others wanted to implement themselves.
I just realised you might be able to use _modify
to allow setting optionally chained values
extension AssociatedValueAccessible {
public subscript<Value>(casePath: CasePath<Self, Value>) -> Value? {
get { casePath.extract(from: self) }
_modify {
var value = casePath.extract(from: self)
yield &value
guard let value = value else { return }
self = casePath.embed(value)
}
@available(*, unavailable, message: "only available as non-optional setter")
set { fatalError() }
}
public subscript<Value>(casePath: CasePath<Self, Value>) -> Value {
@available(*, unavailable, message: "only available as optional getter")
get { fatalError() }
set { self = casePath.embed(newValue) }
}
}
Edit: This might not be viable because of the ambiguous getter
okay... the experiment may have gotten slightly out of hand... This seems to keep the compiler relatively happy...
public protocol CaseAccessible {}
extension CaseAccessible {
public subscript<Value>(casePath: CasePath<Self, Value>) -> Value? {
casePath.extract(from: self)
}
@_disfavoredOverload
public subscript<Value>(casePath: CasePath<Self, Value>) -> Never? {
@available(*, unavailable, message: "only available as optional getter")
get { fatalError() }
@available(*, unavailable, message: "only available as non-optional setter")
set { fatalError() }
}
@_disfavoredOverload
public subscript<Value>(casePath: CasePath<Self, Value>?) -> Value? {
get { casePath?.extract(from: self) }
set { try? casePath?.modify(&self) { $0 = newValue ?? $0 } }
}
@_disfavoredOverload
public subscript<Value>(casePath: CasePath<Self, Value>) -> Value {
@available(*, unavailable, message: "only available as optional getter")
get { fatalError() }
set { self = casePath.embed(newValue) }
}
}
Which allows the following:
struct Baz: Equatable { var array: [Int] = [1, 2, 3], string: String = "Blob" }
indirect enum Foo: CaseAccessible {
case foo(Foo)
case bar(Int)
case baz(Baz)
}
var foo = Foo.bar(42)
foo[/Foo.foo] = .baz(Baz()) // foo == Foo.foo(.baz(Baz())
foo[/Foo.baz]?.string = "Blobby" // no-op
foo[/Foo.foo .. /Foo.bar] = 42 // foo == Foo.foo(.bar(42))
foo[/Foo.baz] = Baz() // foo == Foo.baz(Baz())
foo[/Foo.foo]?[/Foo.baz] = Baz() // no-op
foo[/Foo.foo .. /Foo.baz] = Baz() // foo == Foo.foo(.baz(Baz())
foo[/Foo.foo]?[/Foo.baz]?.array = [3, 2, 1] // foo == Foo.foo(.baz(Baz(array: [3, 2, 1]))
foo[/Foo.foo] = nil // 🛑 forbidden by the compiler
foo[/Foo.foo]?[/Foo.baz] = nil // 🛑 forbidden by the compiler
foo[/Foo.foo .. /Foo.baz] = nil // 🛑 forbidden by the compiler
foo[/Foo.foo] = .none // 🛑 forbidden by the compiler
foo[/Foo.foo] = Optional<Foo>.none //❗️ work around no-op
I don't really think this should be used. Just exploring what's possible.
@stephencelis is this your most recent proposal for derived properties? I might start digging into the compiler a bit and see what I can learn. nonoptional set
seems like a sensible first step.
@iampatbrown Sorry! Thought I responded, but that proposal is very old :) I don't know if it captures the nuance around nonoptional set
.
@rock88 I'm going to close this out for now, as the optional writability requires us to make a choice that we're not sure the main library should have an opinion on. If you release a companion library that introduces this protocol, please do share it with us in the GitHub discussions!
Hi!
I just added AssociatedValueAccessible protocol which allow more easily and convenience methods for get/set values over subscript with CasePath.
Example: