[swift-evolution] [Proposal Draft] automatic protocol forwarding

Matthew Johnson matthew at anandabits.com
Tue Dec 29 17:30:40 CST 2015


> On Dec 29, 2015, at 4:29 PM, Kevin Ballard <kevin at sb.org> wrote:
> 
> On Tue, Dec 29, 2015, at 01:24 PM, Matthew Johnson wrote:
>> Hi Kevin,
>>  
>> Thanks for taking time to look at the proposal.
>>  
>> The technique you show here is not bad, but it has several deficiencies IMO which are addressed by the solution in the proposal.
>>  
>> 1. Forwarding should be an implementation detail, not exposed as it is with this method.
>> 2. As such, the getter for the forwardee is visible.  The forwardee is an implementation detail that should not be visible in most cases.
>  
> I was tempted to call it `_forwardedSequence` instead, but I chose not to do that because the _prefix convention right now means "stdlib things that we don't want to expose but have to do so because of implementation details”.

Sure, but naming conventions are not access control.

>  
>> 3. There is no way to specify access control for the synthesized methods and if there were it would be coupled to the access control of the conformance.
>  
> I'm not sure what you mean by this. Access control for these methods should work identically to access control for the original protocol. The forwarder protocol would simply be declared with the same access control.

There are 3 things at play here:

1. The protocol that is forwarded
2. The forwarding methods implementing the protocol requirements
3. Conformance to the protocol

When you write code manually it is possible to specify distinct access control for 1 and 2, and as Joe noted it would be nice to be able to specify access control for 3 as well.  If this becomes possible all 3 could have distinct access levels.  

For example, a public protocol, internal implementing methods, but private conformance.  Or public implementing methods, an internal protocol, and private conformance.  Or a public protocol, and internal implementing methods and conformance.  Or public implementing methods, but an internal protocol and internal conformance.  

Some of these combinations might be unusual, but there may be use cases for them.  Ideally a forwarding mechanism would allow granular control, just as is possible with manual forwarding implementations.

>  
>> 4. You can't forward to a type that doesn't conform to the protocol.  This is particularly important for existential forwardees (at least until they conform to their protocol).
>  
> It seems weird to me that your forwarding proposal allows you to forward to a member that doesn't conform to the protocol. I suppose it makes some sense if you only forward some methods and implement the others yourself, but I'm not convinced there's actually a good reason to support this. What circumstances are you thinking of where you'd actually find yourself forwarding to a member that doesn't conform to the protocol (that isn't an existential)? The only case that really comes to mind is when the member is of a generic type that can't conform to the protocol (e.g. Array<Int> doesn't conform to Equatable), but the solution there is to allow for conditional protocol conformance (which we already know is something we want in the language).

The proposal requires the forwardee to be *conformable* but not necessarily conforming.  The difference is admittedly subtle, but there may be good reasons.  It may be that it isn’t desriable for the forwardee to actually conform for one reason or another, maybe because you don’t want it to be possible to cast values of the forwardee type to the existential type of the protocol.  

The reason the proposal doesn’t require actual conformance is simply because it isn’t necessary to synthesize the forwarding members and there are potential use cases that could take advantage of the additional flexibility.  And again, because actual conformance would not be required to write the forwarding methods manually.

Protocols are an essential part of the forwarding mechanism because they are the only place in the language to differentiate between Self and a parameter or return value that should be always be of a specific type.  However, actual conformance to the protocol by either the forwarder or the forwardee is not necessary to implement forwarding.  Rather than arbitrarily require that it seems best to allow users control over whether, how, and where actual conformance is declared by both the forwarder and the forwardee.

The proposal specifically does not support “partial forwarding”.  That was noted as a possible future enhancement.  If such an enhancement were ever brought forward I would not support dropping the requirement for the forwardee to be “theoretically conformable” to the protocol.  Partial forwarding would only exist to enable forwarders to “override” specific member implementations.

>  
> Forwarding to existentials is a valid concern, but I'm not actually sure why Swift doesn't make existential protocol values conform to the protocol anyway. That limitation would make sense if you could create existential protocol values from protocols with Self/associated types where the existential simply omits the relevant members (because then it obviously doesn't conform to the protocol), but Swift doesn't actually let you create existentials in that situation anyway.

Yes, I agree that existential should conform to the corresponding protocol and I hope it is a temporary limitation.  It has been mentioned that there are some implementation complexities and possible performance issues to work through. 

In any case, until this happens existential are a strong use case for taking advantage of forwarding to "theoretically conformable but not actually conforming” types.  There is no reason not to support this use case and no reason not to enable any other use cases that might take advantage of that flexibility.


>  
>> 5. A type that can't conform to the protocol can't forward an implementation of the members of the protocol.  Specifically, non-final classes cannot automatically forward a group of methods to an implementer.
>  
> It sounds like you're talking here about the ability to forward members that aren't actually associated with a protocol, right? I don't see why you can't just declare a protocol in that case to represent the members that you want to be able to forward.

No, I’m talking about the case where you might have a protocol with a Self return type.  non-final classes cannot conform to such a protocol. But it is possible to synthesize forwarding methods corresponding to a protocol where the return type of those methods is Forwarder.  You can’t declare conformance and don’t actually conform, but the forwarding methods could be synthesized without trouble and would work just fine in cases where a return type of Forwarder (rather than a covariant return type) is acceptable.  

>  
>> 6. This solution does not lay the foundation for a newtype feature. It would be possible to have a specialized newtype feature, but why not build it on top of a more general forwarding feature?
>  
> Barring a detailed newtype proposal, it's hard to see how forwarding would actually interact with it. Are you thinking that a newtype would have some member that provides the underlying value (e.g. a `var rawValue`)? Given such a member, a generalized forwarding mechanism would then interact with it. But if e.g. newtype works by letting you cast (with `as`) to the underlying type, then your generalized forwarding mechanism can't actually work unless it has two different modes (one of which uses a member and the other uses a cast), which means you're specializing for newtype anyway.
>  
> Besides, when using a newtype mechanism like that, since the newtype has the same in-memory representation as the original type, you don't actually want to generate new methods at all for forwarding, instead you just want to re-use the exact same methods as the original type (otherwise you're just bloating your code with a bunch of stubs that do nothing more than bitcast the value and call the original method).

I roughly sketched what a newtype mechanism built on top of protocol forwarding might look like in a simple case in the future enhancements section of the proposal.  This is not a detailed proposal by any means, but it does suggest a possible direction.  The suggested direction is pretty similar to what Joe Groff mentioned in this post: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151207/001137.html <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151207/001137.html>.

>  
> -Kevin Ballard
>  
>> You may be right that many protocols are not amenable to forwarding.  The motivation for this proposal is that it enables delegation-based designs to be implemented succinctly.  In that use case the protocols will be designed alongside concrete implementations and types that forward to them.  A secondary motivation for this proposal is to lay a foundation for a newtype feature.  In that case the protocols to be forwarded would be specifically designed to represent the portion of the interface of the wrapped type which should be visible to users of the newtype.
>>  
>> I hope these points might be enough to convince you that it is worth a closer look.
>>  
>> Matthew
>>  
>>> On Dec 29, 2015, at 2:06 PM, Kevin Ballard via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>  
>>> I briefly skimmed your proposal, so I apologize if you already addressed this, but it occurs to me that we could support automatic protocol forwarding today on a per-protocol basis simply by declaring a separate protocol that provides default implementations doing the forwarding. Handling of Self return types can then be done by adding a required initializer (or just not implementing that method, so the concrete type is forced to deal with it even though everything else is forwarded).
>>>  
>>> For example, if I want to automatically forward SequenceType to a member, I can do something like
>>>  
>>> protocol SequenceTypeForwarder : SequenceType {
>>> typealias ForwardedSequenceType : SequenceType
>>>  
>>> var forwardedSequence : ForwardedSequenceType { get }
>>> }
>>>  
>>> extensionSequenceTypeForwarder {
>>> func generate() -> ForwardedSequenceType.Generator {
>>> return forwardedSequence.generate()
>>>     }
>>>  
>>> func underestimateCount() -> Int {
>>> return forwardedSequence.underestimateCount()
>>>     }
>>>  
>>> func map<T>(@noescape transform: (ForwardedSequenceType.Generator.Element) throws -> T) rethrows -> [T] {
>>> return try forwardedSequence.map(transform)
>>>     }
>>>  
>>> func filter(@noescape includeElement: (ForwardedSequenceType.Generator.Element) throws -> Bool) rethrows -> [ForwardedSequenceType.Generator.Element] {
>>> return try forwardedSequence.filter(includeElement)
>>>     }
>>>  
>>> func forEach(@noescape body: (ForwardedSequenceType.Generator.Element) throws -> Void) rethrows {
>>> return try forwardedSequence.forEach(body)
>>>     }
>>>  
>>> func dropFirst(n: Int) -> ForwardedSequenceType.SubSequence {
>>> return forwardedSequence.dropFirst(n)
>>>     }
>>>  
>>> func dropLast(n: Int) -> ForwardedSequenceType.SubSequence {
>>> return forwardedSequence.dropLast(n)
>>>     }
>>>  
>>> func prefix(maxLength: Int) -> ForwardedSequenceType.SubSequence {
>>> return forwardedSequence.prefix(maxLength)
>>>     }
>>>  
>>> func suffix(maxLength: Int) -> ForwardedSequenceType.SubSequence {
>>> return forwardedSequence.suffix(maxLength)
>>>     }
>>>  
>>> func split(maxSplit: Int, allowEmptySlices: Bool, @noescape isSeparator: (ForwardedSequenceType.Generator.Element) throws -> Bool) rethrows -> [ForwardedSequenceType.SubSequence] {
>>> return try forwardedSequence.split(maxSplit, allowEmptySlices: allowEmptySlices, isSeparator: isSeparator)
>>>     }
>>> }
>>>  
>>> With this protocol declared, I can then say something like
>>>  
>>> struct Foo {
>>> var ary: [Int]
>>> }
>>>  
>>> extensionFoo : SequenceTypeForwarder {
>>> var forwardedSequence: [Int] { return ary }
>>> }
>>>  
>>> and my struct Foo now automatically implements SequenceType by forwarding to its variable `ary`.
>>>  
>>> The downside to this is it needs to be manually declared for each protocol. But I wager that most protocols actually aren't really amenable to forwarding anyway.
>>>  
>>> -Kevin Ballard
>>> 
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151229/604d650c/attachment.html>


More information about the swift-evolution mailing list