[swift-evolution] [Draft] Throwing Properties and Subscripts

Brent Royal-Gordon brent at architechies.com
Tue Mar 15 19:01:46 CDT 2016


>> 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`.
> 
> It's debatable whether this is a good use of property syntax. The standard library doesn't even use property syntax for things that might unconditionally fail due to programmer error.

Debatable, sure.

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. That doesn't man we are *obliged* to add features to support every weird setter variation—for instance, I doubt we're ever going to accommodate UIKit's love of `setFoo(_:animated:)` methods—but I think we ought to lean towards making properties and subscripts as powerful as methods when we can reasonably do so.

>> - There's actually a third setter category: read-only.
> 
> How is that different from a nonmutating setter? Did you mean a read-only property? A read-only property is just a regular function, Base -> Property.

Yes, I mean a read-only property (no setter). Currently these aren't exposed at all on the type, even though we could provide read-only access.

So I take it what you're proposing is that this:

	struct Foo {
		func method() { ... }
		mutating func mutatingMethod() { ... }
		
		var readOnlyProperty: Int { get { ... } }
		var readOnlyMutatingProperty: Int { mutating get { ... } }
		
		var readWriteProperty: Int { get { ... } set { ... } }
		var readWriteNonmutatingProperty: Int { get { ... } nonmutating set { ... } }
	}

Also has these members?

	extension Foo {
		// These argument lists might be combined in the future
		static func method(self: Foo) -> () -> Void
		static func mutatingMethod(self: inout Foo) -> () -> Void
		
		static func readOnlyProperty(self: Foo) -> Int
		static func readOnlyMutatingProperty(self: inout Foo) -> Int
		
		static func readWriteProperty(self: inout Foo) -> inout Int
		static func readWriteNonmutatingProperty(self: Foo) -> inout Int
	}

(Hmm. There might be room for a `reinout` or `inout(set)` along the lines of `rethrows`:

	static func readWriteProperty(self: reinout Foo) -> inout Int

That would mean the `self` parameter is inout only if the return value is treated as inout.)

>> - 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).
> 
> Fair point. From the point of view of the property abstraction, though, `mutating get nonmutating set` is erased to `mutating get mutating set`. That leaves three kinds of mutable property projection.

That can work. But like I said, you could do the same thing with `throws`.

> `mutating get` itself is sufficiently weird and limited in utility, its use cases (IMO) better handled by value types holding onto a class instance for their lazy- or cache-like storage, that it might be worth jettisoning as well.

That would interfere with the rather elegant mutating-get-to-copy-on-write pattern: <https://developer.apple.com/videos/play/wwdc2015/414/?time=2044>. I suppose a mutating method would work the same way as long as you were backing the instance with a reference type, though.

>> 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!)
> 
> This sounds like something generics would better model than Any polymorphism, to carry the type parameter through the context you need polymorphism.

Sorry, that probably wasn't as clear as it should be.

I would like to eventually have a way to dynamically look up and use lenses by property name. I think this could serve as a replacement for KVC. So, for instance, the `Foo` type I showed previously might have dictionaries on it equivalent to these:

	extension Foo {
		static var readableProperties: [String: (inout Foo) -> inout Any] = [
			"readWriteProperty": { $0.readWriteProperty },
			"readWriteNonmutatingProperty": { ... },
			"readOnlyProperty": { ... },
			"readOnlyMutatingProperty": { ... }
		]
		static var writableProperties: [String: (inout Foo) -> inout Any] = [
			"readWriteProperty": {
				get { return $0.readWriteProperty }
				set { $0.readWriteProperty = newValue as! Int }
			},
			"readWriteNonmutatingProperty": { ... }
		]
	}

Then you could dynamically look up and use a lens:

	for (propertyName, value) in configurationDictionary {
		let property = Foo.writableProperties[propertyName]!
		property(&foo) = value
	}

(These dictionaries would not be opted into with a protocol; they would be synthesized at a given use site, and would contain all properties visible at that site. That would allow you to pass a dictionary of lenses to external serialization code, essentially delegating your access to those properties.)

But note the forced cast in the `readWriteProperty` setter: if the Any assigned into it turns out to be of the wrong type, the program will crash. That's not good. So the setter has to be able to signal its failure, and the only way I can imagine to do that would be to have it throw an exception:

		static var writableProperties: [String: (inout Foo) throws -> inout Any] = [
			"readWriteProperty": {
				get { return $0.readWriteProperty }
				set {
					guard let newValue = newValue as? Int else {
						throw PropertyError.InvalidType
					}
					$0.readWriteProperty = newValue
				}
			},
			…
	
	for (propertyName, value) in configurationDictionary {
		let property = Foo.writableProperties[propertyName]!
		try property(&foo) = value
	}

Hence, non-throwing setters would have to become throwing setters when you erased the property's concrete type.

>> (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.
> 
> True, you could say that if either part of the access can throw, then the entire property access is abstractly considered `throws`, and that errors are checked after get, after set, and for an `inout` access, when materializeForSet is called before the formal inout access (to catch get errors), and also after the completion callback is invoked (to catch set errors). That means you have to `try` every access to an abstracted property, but that's not the end of the world.

Exactly.

(Although in theory, we could add a `throws(set)` syntax on inout-returning functions, indicating that the getter never throws but the setter does, much like the `reinout` I mentioned earlier.)

-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list