[swift-evolution] [Draft] Throwing Properties and Subscripts
Brent Royal-Gordon
brent at architechies.com
Wed Mar 16 04:19:27 CDT 2016
> A mutable return has to be invariant. It'd be more sound to model that as "exists T. (inout Foo) -> inout T", if our type system becomes powerful enough to represent that, than to erase the return type, since you could then conceivably still recover the T by opening the existential.
I'm trying to unpack this statement. I *think* what you're saying is that, since a given lens always returns the same type, the lens should itself be stored in an existential, rather than returning an existential, so you can discover the type it returns. In other words, instead of being "function from mutable Foo to mutable any", like this:
inout Foo throws -> inout Any
They should be "any function from mutable Foo to some mutable type", which would be something along the lines of:
Any<T in inout Foo throws -> inout T>
(Although that syntax is a catastrophe—best I could come up with, but still horrible. `T in` declares a generic type parameter, if it's not clear.)
Is that accurate?
If so, it still seems like Swift ought to let you set from Any using the existential so you don't have to explicitly unwrap it all the time. That would provide something equivalent to this:
extension<T> Any<inout Foo throws -> inout T> {
// Inside the extension, T is bound to the return type, so we can open `self` by casting it with T.
invoke(instance: inout Foo) throws -> inout Any {
get {
let concreteInvoke = self as inout Foo throws -> inout T
return try concreteInvoke(&instance) as Any
}
set {
let concreteInvoke = self as inout Foo throws -> inout T
guard let newValue = newValue as? T {
throw InvalidTypeError
}
try concreteInvoke(&instance) = newValue
}
}
}
And it also seems like, if we decide not to take the generics system far enough to support this kind of existential, an implementation using type `inout Foo -> throws inout Any` is a good compromise. (Though we shouldn't rush to offer it if we do intend to support that kind of existential later on.)
In any case, however, this is digressing pretty far from the proposal itself.
* * *
If I recall correctly, Joe, your objections were only to throwing getters, and were as follows:
— Abstracting over the property might become more complicated if there are many possible combinations of throwabiity.
Is it enough to say that we can make a lens throw if either accessor can throw, as we've been discussing?
Do you think it doesn't make sense to support `get throws set` configurations? (Given that `get` and `set` are often combined into a single operation, I can understand why you would feel that way.) If so, do you think we should also consider (in a separate proposal) eliminating `mutating get nonmutating set` configurations?
— You think that code which can throw should generally be a method, not a getter.
I've already mentioned my opinion on the appropriateness of this:
> My intuition is that, if you have a pair of things which act as getter and setter, Swift ought to permit you to treat them as a single member. If Swift erects a barrier preventing you from doing that, that is a limitation of the language and ought to be considered a negative.
I also have a couple of practical concerns.
I worry that, if Swift doesn't support throwing from getters, people will not respond by using methods; they will instead respond by handling errors in suboptimal ways, such as:
* Using `try!` or other trapping error handling mechanisms inside the getter, making errors impossible to handle gracefully.
* Returning optionals, which will throw away error information and permit the setter to accept meaningless `nil` values.
* Returning implicitly unwrapped optionals, which have the same disadvantages as optionals, in addition to undermining Swift's goal of forcing you to explicitly handle expected errors.
* Returning `Result`-style enums, which will make the property more difficult to use and permit the setter to accept meaningless error values.
* Abusing a future universal error mechanism, which would undermine Swift's goal of forcing you to explicitly handle anticipatable errors.
* Abusing exception mechanisms from other languages bridged into Swift, which would cause undefined behavior.
I also think that, in places where a protocol requirement might reasonably be fulfilled by either non-throwing storage or potentially-throwing computation, the absence of throwing getters might force you to favor one implementation or the other. For instance, consider the JSONRepresentable protocol I used in an earlier example:
protocol JSONRepresentable {
associatedtype JSONRepresentation: JSONValue
init(json: JSONRepresentation) throws
var json: JSONRepresentation { get throws }
}
Some types are best implemented by storing the data in one property as (unpacked) JSON; others are best implemented by storing the data in separate properties and generating the JSON on demand. If getters can't throw, then `json` must either be a non-throwing getter (disadvantaging types which generate the JSON) or a throwing method (disadvantaging types which store the JSON).
Finally, I think that even if properties might not have strong enough use cases, subscripts will. For instance:
enum XMLNode {
// ...cases omitted...
private var underlyingNode: NSXMLNode {
// Omitted because massive switch statements.
get { ... }
set { ... }
}
private var mutatingUnderlyingNode: NSXMLNode {
mutating get {
underlyingDocument = underlyingDocument.copy() as! NSXMLDocument
return underlyingDocument
}
}
subscript(xPath path: String) -> [XMLNode] {
get throws {
return try underlyingNode.nodesForXPath(path).map(XMLNode.init)
}
set throws {
let oldUnderlyingValue = try mutatingUnderlyingNode.nodesForXPath(path)
precondition(newValue.count == oldUnderlyingValue.count)
for (newNode, oldUnderlyingNode) in zip(newValue, oldUnderlyingValue) {
newNode.supplant(oldUnderlyingNode)
}
}
}
}
And if you think that methods should replace throwing property getters, then surely once inout-returning functions are introduced, their getters will need to throw. At that point you'll be in a situation where most getters in Swift can throw, but there's a specific exception for property getters, arising out of a linguistic opinion rather than a general rule. That's the sort of strange inconsistency Swift is otherwise good at avoiding. (For instance, I was very pleased indeed to discover the `while let` loop last year.)
Even if you and like-minded programmers don't like them, I can't see any practical reason to forbid throwing getters. They don't appear to undermine any of the error handling system's goals, introduce confusing or inconsistent behavior, or severely complicate the compiler's job. They merely stretch the role of a property somewhat. And it's a pretty mild kind of stretching; I think throwing getters are less strange, for instance, than the generic constants Chris Lattner and others are interested in introducing.
I do understand why you and other developers might not want to use throwing getters, but I think that's ultimately a matter of style, not of correctness. Throwing getters seem like a design choice that reasonable people might make.
Even if you would not use throwing property getters, do you agree that they're conceptually well-formed and others with differing opinions might want to use them?
--
Brent Royal-Gordon
Architechies
More information about the swift-evolution
mailing list