> Do you have an imagined use for throwing getters?
In the proposal, I cite a pair of framework methods that I think would be
better modeled as a throwing subscript:
class NSURL {
func getResourceValue(_ value:
AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey key: String) throws
func setResourceValue(_ value: AnyObject?, forKey key: String)
throws
}
Here's another, much less matter-of-opinion usage:
// Numeric types, Bool, String, JSONValue?, [JSONValue], and [String:
JSONValue] conform.
protocol JSONValue { ... }
protocol JSONRepresentable {
associatedtype JSONRepresentation: JSONValue
init(json: JSONRepresentation) throws
var json: JSONRepresentation { get throws }
}
struct User: JSONRepresentable {
var json: [String: JSONValue]
var friends: [User] {
get throws {
guard let friendsJSON = json["friends"] as?
[User.JSONRepresentation] else {
throw UserError.InvalidFriends
}
return friendsJSON.map { try User(json: $0) }
}
set throws {
json["friends"] = newValue.map { try $0.json }
}
}
}
As long as we're doing computation in getters, it will make sense for that
computation to raise errors. I don't think we can get around the need for `get
throws`.
> Allowing the getter or setter to throw independently greatly complicates the
> abstract model for properties. While we don't have much in the way of
> abstraction tools over properties yet, it would be important to consider the
> impact this might have on "lens" functions that can perform mutable
> projections. Right now, you can more or less categorize mutable properties
> into two groups:
>
> - "reference-like", projecting mutable storage indirectly through a
> reference, which you could think of as having a type (Base) -> inout
> Property. This includes not only class properties but `nonmutating` struct
> properties like `UnsafeMutablePointer.memory`.
> - "value-like", projecting mutable storage that is part of a larger mutable
> value, which you could think of as having type (inout Base) -> inout
> Property. This includes most mutable struct properties that aren't explicitly
> `nonmutating`.
I think you may be overoptimistic here—I've tried to prototype lenses before
(using protocols and classes) and I've always quickly ended up in combinatorial
explosion territory even when merely modeling existing behavior. First of all,
mutability is uglier than you imply:
- There's actually a third setter category: read-only.
- The getter and setter can be *independently* mutating—Swift is happy to
accept `mutating get nonmutating set` (although I can't imagine why you would
need it).
So that's already six, not two, categories.
Another complication comes from the type of the property in the lens's view.
You need Any-typed lenses for KVC-style metaprogramming, but you also want
type-specialized lenses for greater safety where you have stronger type
guarantees. And yet their setters are different: Any setters need to be able to
signal that they couldn't downcast to the concrete type of the property you
were mutating. (This problem can actually go away if you have throwing setters,
though—an Any lens just has to make nonthrowing setters into throwing ones!)
(For added fun: you can't model the relationship between an Any lens and a
specialized lens purely in protocols, because that would require support for
higher-kinded types.)
So if you want to model the full richness of property semantics through their
lenses, the lens system will inevitably be complicated. If you're willing to
give up some fidelity when you convert to lenses, well, you can give up
fidelity on throwing semantics too, and have the lens throw if either accessor
throws.
> If properties can throw, we'd want to be able to represent that in the type
> system for projection functions. If there are four different ways a property
> can throw (no throws, only reads can throw, only writes can throw, or both
> reads and writes can throw), that gets really complicated.
Nevertheless, this might be a good enough reason to invoke the escape hatch I
left in the Alternatives Considered section:
Calling a setter often implicitly involves calling a getter, so it may
make sense to require the setter to be at least as throwing as the
getter. Absent feedback to this effect from implementors, however, my
instinct is to leave them independent…
The way I see it, we have three options for the throwing variants we might
allow:
- Independent: Any combination
- Dependent: Getter can't throw unless setter can (no "get throws set")
- Setter only: Getter can't throw (no "get throws set" or "get throws set
throws")
We also might want to look at that weird nonmutating case, so there are two
options there:
- Independent: Any combination
- Dependent: Getter can't mutate unless setter can (no "nonmutating get
mutating set")
(If we really wanted to, we could probably make the `mutating` property
setter-only; things like `lazy` would just have to use a reference-typed box.
That would be a pretty violent change to the language, though.)
That's a lot of possible semantics, so I wrote a script to help me understand
what each policy would do. Here's the abridged results:
20 variants for independent mutating, independent throws
16 variants for independent mutating, dependent throws
16 variants for dependent mutating, independent throws
13 variants for dependent mutating, dependent throws
12 variants for independent mutating, setter-only throws
10 variants for dependent mutating, setter-only throws
(Full results, plus script:
<https://gist.github.com/brentdax/97e3dfe7af208da51a1a>. By the way, without
any `throws` support at all, independent mutating requires 6 variants and
dependent mutating requires 5.)
My main conclusion is that limiting `throws` to setters only doesn't actually
gain us much over limiting both `throws` and `mutating` to the "sensible"
combinations.
And again, this only applies if we want lenses to model all property semantics
with complete fidelity. We could offer fewer lenses if we're willing to accept
clunkier interfaces. Heck, we could offer a single lens type if we're okay with
dynamic safety:
// Semantics would be equivalent to:
struct Lens<Instance, Value> {
// Can throw only errors thrown by the property.
func value(for instance: inout Instance) throws -> Value
// Can throw `SetterInaccessible` or errors thrown by the
property.
func updateValue<T>(for instance: inout Instance, mutator:
(inout Value) -> T) throws -> T
// Can throw `RequiresMutating` or errors thrown by the
property.
func value(for instance: Instance) throws -> Value
// Can throw `SetterInaccessible`, `RequiresMutating`, or
errors thrown by the property.
func updateValue<T>(for instance: Instance, mutator: (inout
Value) -> T) throws -> T
}
extension Lens {
// `updateValue` methods can also throw `InvalidValueType`.
init<ConcreteValue: Value>(downcasting: Lens<Instance,
ConcreteValue>)
}
enum LensError: Error {
case InvalidValueType (expectedType: Any.Type, foundType:
Any.Type)
case SetterInaccessible
case RequiresMutating
}
--
Brent Royal-Gordon
Architechies
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution