>> 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

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to