[swift-evolution] [Proposal Draft] automatic protocol forwarding
Matthew Johnson
matthew at anandabits.com
Tue Dec 29 20:05:33 CST 2015
> On Dec 29, 2015, at 6:10 PM, Brent Royal-Gordon <brent at architechies.com> wrote:
>
>>> * Does it have to be a protocol? Why not also allow the concrete type of the property you're forwarding to? Obviously you couldn't form a subtype relationship (unless you could...), but this might be useful to reduce boilerplate when you're proxying something.
>>
>> This is addressed in the alternatives considered section.
>
> Sorry, I missed that, probably because the sample code in that section didn't show such a forwarding.
>
>> The short answer is that there the direct interface of the concrete type does not contain sufficient information about potential Self parameters to do this well. This information does exist in the protocol declarations. Allowing this information to be specified in concrete interfaces would add enough complexity to the language that I don’t think it is worthwhile.
>
> That's a good point. You could perhaps add a way to tweak the forwarding of certain members, but that'd be a little tricky.
I gave this enough consideration to be leaning pretty strongly in the direction that protocols are the best way to do this.
Doing this properly for the concrete interface would require an `InvariantSelf` type that could be used in any method signature. It would also need to be actually used correctly in practice by types that were forwarded to. The distinction is pretty subtle when you’re dealing with a concrete interface and my instinct is that people would get it wrong a lot of the time.
Because protocols are inherently generic it is a little more straightforward to think about when you mean `Self` and when you mean a concrete type.
The good news is that you can declare a protocol containing the full interface of a concrete type if you really want or need to and use that for forwarding. The advantage of requiring a protocol here is that it requires you to consciously think about whether each parameter and return type is intended to be concrete or an abstract Self. It also allows you to properly forward an interface even if the original author did not consider these issues when implementing the type.
In the following example, should the other parameter and the return type be `Self ` or `Double`? It is not possible to know unless there is a protocol that declares foo.
extension Double {
func foo(other: Double) -> Double {
return self
}
}
>
> One of the things I'd like to see is the ability to proxy for an instance without the person writing the proxy knowing which instances it'll be used with. Think, for example, of the Cocoa animator proxy, or `NSUndoManager.prepareWithInvocationTarget(_:)`. It'd be nice if a Swift equivalent could return, say, `NSAnimatorProxy<View>` or `NSUndoManager.InvocationTarget<Target>`, which has all the methods of the generic type but records the calls for later use.
>
> Of course, what you really want is for only a particular subset of the methods to be available on the proxy (animated methods on `NSAnimatorProxy`, Void methods on `NSUndoManager.InvocationTarget`), and of course in these cases you're not calling directly through to the underlying methods. So I might just be barking up the wrong tree here.
I can see how it might be desirable to forward to a type you receive as a generic parameter. However, you would need to constrain that type to a protocol(s) in order to actually do anything useful with it. That same protocol(s) could also be used in the forwarding declaration.
If you want is to be able to forward a set of methods that is determined by the generic parameter, that just isn’t going to be possible. At least not without significant changes to other parts of the language providing capabilities that would allow you to implement something like that manually.
>
>>> * Why the method-based conversion syntax for return values, rather than something a little more like a property declaration?
>>>
>>> var number: Int
>>> forward IntegerType to number {
>>> static return(newValue: Int) {
>>> return NumberWrapper(newValue)
>>> }
>>> return(newValue: Int) {
>>> return NumberWrapper(newValue)
>>> }
>>> }
>>
>> This is actually a really good idea to consider! I didn’t consider something like this mostly because I didn’t think of it. I’m going to seriously consider adopting an approach along these lines.
>
> Great.
>
>> One possible advantage of the approach I used is that the initializer may already exist for other reasons and you would not need to do any extra work.
>
> True. But it may also exist and *not* do what you want in the forwarding case. It's easier to explicitly use the right initializer than it is to work around the forwarding system implicitly using the wrong one.
Right, I am generally leaning pretty strongly towards changing the proposal to use a solution similar to what you suggest.
>
>> A big advantage of this approach is that it would work even when you are forwarding different protocols to more than one member with the same type.
>
> But again, if that's the wrong behavior, there's no good way to fix it.
I was actually indicating an advantage of the approach you suggested because it provides a solution to that problem. :)
>
>>> * If you want to keep the method-based syntax, would it make sense to instead have an initializer for instance initializers too, and just have it take a second parameter with the instance?
>>>
>>> init(forwardedReturnValue: Int) {...}
>>> init(forwardedReturnValue: Int, from: NumberWrapper) {…}
>>
>> Part of the reason the instance method was used is because sometimes the right thing to do might be to mutate and then return self. Using an instance method gives you the flexibility to do that if necessary.
>
> In practice, I'm not sure that's actually the case very often. How frequently will the internal type return a changed value, but your identically-named cover method ought to mutate the property? That completely changes the semantics of the underlying call.
I would guess you are right about this being something that is never or almost never the right thing to do. Maybe a solution that actually prevents you from doing this would be a better one for that reason.
>
> I mean, what you're proposing would be something like this:
>
> class ListOfThings {
> private var actualList: [Thing]
>
> func filter(predicate: Thing -> Bool) -> ListOfThings {
> let returnValue = actualList.filter(predicate)
> actualList = returnValue
> return ListOfThings(returnValue)
> }
> }
>
> Is that a thing you actually expect people to do?
I hope not! :) I hadn’t looked too hard for an example where it would be the right thing to do and I think you are right that such an example would be pretty hard to come by. Thanks for pushing back on this one! :)
>
>>> * Does this mean that a `public forward` declaration would forward `internal` members through synthesized `public` interfaces, if the forwarder and forwardee happened to be in the same module?
>>>
>>>> All synthesized members recieve access control modifiers matching the access control modifier applied to the forward declaration.
>>
>> Yes, if the forwardee had internal visibility and the forwarder was public the forwarder could publicly forward the interface. This is intentional behavior. The forwardee may well be an internal implementation detail while the methods of the protocol are part of the public interface of the forwarder. It is possible to write code that does this manually today.
>
> I suppose that, if it's always a protocol you're forwarding to, you can assume that none of the protocol methods are internal-only implementation details. But I have to admit that I'm still concerned about this; it just seems like a recipe for accidentally exposing things you meant to keep private.
Think of a state or strategy pattern. You might have an internal protocol with several implementations representing the various states or strategies. You forward to a private member that is an existential of the protocol type. The synthesized forwarding methods represent the public interface. However, the protocol itself is not public as it is an implementation detail and the forwarder doesn’t even conform to the protocol. The protocol is strictly used as an implementation detail of the public interface of your type.
I honestly don’t understand the concern about this proposal accidentally exposing things you meant to keep private. You have full control over access control of everything, including the synthesized methods. The proposal doesn’t do anything you couldn’t already do manually and it makes it very easy to keep them private if desired. If somebody wanted to manually write a forwarder that declares forwarding methods public while forwarding to internal methods on the forwardee they can do that already today!
IMO the language needs to provide the tools to specify your intent clearly and easily and offer sensible defaults, but it can’t do more than that. I believe this proposal falls in line with that principle and also inline with how access control already works in the language today (the default for synthesized members matches the default for manual member declarations).
I am very concerned about not exposing details that shouldn’t be exposed and that is one of the big drawbacks of the approach to protocol forwarding that Kevin Ballard shared.
>
>>> * You don't explicitly mention this, but I assume mutating methods work and mutate `self`?
>>
>> Mutating methods are something I didn’t think about carefully yet. Thanks for pointing that out! But generally, yes a forwarding implementation of a mutating method would need to mutate the forwardee which is part of self, thus mutating self.
>
> Well, as long as they're thought about at some point!
Yes, I’m glad you brought them up!
>
> --
> Brent Royal-Gordon
> Architechies
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151229/58cac2a5/attachment.html>
More information about the swift-evolution
mailing list