I have completed a first draft of a proposal to introduce automatic protocol 
forwarding.  I’m looking forward to feedback from everyone!
Automatic protocol forwarding

Proposal: SE-NNNN 
<https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-automatic-protocol-forwarding.md>
Author(s): Matthew Johnson <https://github.com/anandabits>
Status: Review
Review manager: TBD
Introduction

Automatic protocol forwarding introduces the ability to use delegation without 
the need write forwarding member implementations manually. 

A preliminary mailing list thread on this topic had the subject protocol based 
invocation forwarding 
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151207/000931.html>
Motivation

Delegation is a robust, composition oriented design technique that keeps 
interface and implementation inheritance separate. The primary drawback to this 
technique is that it requires a lot of manual boilerplate to forward 
implemenation to the implementing member. This proposal eliminates the need to 
write such boilerplate manually, thus making delegation-based designs much more 
convenient and attractive.

This proposal may also serve as the foundation for a future enhancement 
allowing a very concise “newtype” declaration.

Proposed solution

I propose introducing a forward declaration be allowed within a type 
declaration or type extension. The forward declaration will cause the compiler 
to synthesize implementations of the members required by the forwarded 
protocols. The synthesized implementations will simply forward the method call 
to the specified member.

The basic declaration looks like this:

forward Protocol, OtherProtocol to memberIdentifier
The first clause contains a list of protocols to forward. 

The second clause specifies the identifier of the property to which the 
protocol members will be forwarded. Any visible property that implements the 
members required by the protocol is eligible for forwarding. It does not matter 
whether it is stored, computed, lazy, etc.

It is also possible to include an access control declaration modifier to 
specify the visibility of the synthesized members.

Self parameters

When a protocol member includes a Self parameter forwarding implementations 
must accept the forwarding type but supply an argument of the forwardee type 
when making the forwarding call. The most straightforward way to do this is to 
simply use the same property getter that is used when forwarding. This is the 
proposed solution.

Self return types

When a protocol member includes a Self return type forwarding implementations 
must return the forwarding type. However, the forwardee implmentation will 
return a value of the forwardee type. This result must be used to produce a 
value of the forwarding type in some way.

The solution in this proposal is based on an ad-hoc overloading convention. A 
protocol-based solution would probably be desirable if it were possible, 
however it is not. This proposal supports forwarding to more than one member, 
possibly with different types. A protocol-based solution would require the 
forwarding type to conform to the “Self return value conversion” protocol once 
for each forwardee type.

Static members

When a forwardee value is returned from a static member an initializer will be 
used to produce a final return value. The initializer must be visible at the 
source location of the forward declaration and must look like this:

struct Forwarder {
    let forwardee: Forwardee
    forward P to forwardee
    init(_ forwardeeReturnValue: Forwardee) { //... }
}
Instance members

When a forwardee value is returned from an instance member an instance method 
will be used to transform the return value into a value of the correct type. An 
instance method is necessary in order to allow the forwarding type to access 
the state of the instance upon which the method was called when performing the 
transformation.

If the instance method is not implemented the initializer used for static 
members will be used instead.

The transformation has the form:

struct Forwarder {
    let forwardee: Forwardee
    forward P to forwardee
    func transformedForwardingReturnValue(forwardeeReturnValue: Forwardee) -> 
Forwarder { //... }
}
NOTE: This method should have a better name. Suggestions are appreciated!

Examples

Basic example

NOTE: Forwardee does not actually conform to P itself. Conformance is not 
required to synthesize the forwarding member implementations. It is only 
required that members necessary for forwarding exist. This is particularly 
important to the second example.

public protocol P {
    typealias TA
    var i: Int
    func foo() -> Bool
}

private struct Forwardee {
    typealias TA = String
    var i: Int = 42
    func foo() -> Bool { return true }
}

public struct Forwarder {
    private let forwardee: Forwardee
}

extension Forwarder: P {        
    // user declares
    public forward P to forwardee
    
    // compiler synthesizes
    // TA must be synthesized as it cannot be inferred for this protocol
    public typealias TA = String 
    public var i: Int {
        get { return forwardee.i }
        set { forwardee.i = newValue }
    }
    public func foo() -> Bool { 
        return forwardee.foo() 
    }
}
Existential forwardee

NOTE: Existentials of type P do not actually conform to P itself. Conformance 
is not required to synthesize the forwarding member implementations. It is only 
required that members necessary for forwarding exist.

public protocol P {
    func foo() -> Bool
}

struct S: P {
    private let p: P
    
    // user declares:
    forward P to p
    
    // compiler synthesizes:
    func foo() -> Bool {
        return p.foo()
    }
}
Self parameters

public protocol P {
    func foo(value: Self) -> Bool
}

extension Int: P {
    func foo(value: Int) -> Bool {
        return value != self
    }
}

struct S: P {
    private let i: Int
    
    // user declares:
    forward P to i
    
    // compiler synthesizes:
    func foo(value: S) -> Bool {
        return i.foo(value.i)
    }
}
Self return types

Using the instance method:

public protocol P {
    func foo() -> Self
}

extension Int: P {
    func foo() -> Int {
        return self + 1
    }
}

struct S: P {
    private let i: Int
    func transformedForwardingReturnValue(forwardeeReturnValue: Int) -> S {
        return S(i: forwardeeReturnValue)
    }
    
    // user declares:
    forward P to i
    
    // compiler synthesizes:
    func foo() -> S {
        return self.transformedForwardingReturnValue(i.foo())
    }
}
Using the initializer:

public protocol P {
    func foo() -> Self
}

extension Int: P {
    func foo() -> Int {
        return self + 1
    }
}

struct S: P {
    private let i: Int
    init(_ value: Int) {
        i = value
    }
    
    // user declares:
    forward P to i
    
    // compiler synthesizes:
    func foo() -> S {
        return S(i.foo())
    }
}
Forwarding multiple protocols

public protocol P {
    func foo() -> Bool
}
public protocol Q {
    func bar() -> Bool
}

extension Int: P, Q {
    func foo() -> Bool {
        return true
    }
    func bar() -> Bool {
        return false
    }
}

struct S: P, Q {
    private let i: Int
    
    // user declares:
    forward P, Q to i
    
    // compiler synthesizes:
    func foo() -> Bool {
        return i.foo()
    }
    func bar() -> Bool {
        return i.bar()
    }
}
Forwarding to multiple members

public protocol P {
    func foo() -> Bool
}
public protocol Q {
    func bar() -> Bool
}

extension Int: P {
    func foo() -> Bool {
        return true
    }
}
extension Double: Q {
    func bar() -> Bool {
        return false
    }
}

struct S: P, Q {
    private let i: Int
    private let d: Double
    
    // user declares:
    forward P to i
    forward Q to d
    
    // compiler synthesizes:
    func foo() -> Bool {
        return i.foo()
    }
    func bar() -> Bool {
        return d.bar()
    }
}
Non-final class

NOTE: C cannot declare conformance to the protocol due to the Self return value 
requirement. However, the compiler still synthesizes the forwarding methods and 
allows them to be used directly by users of C.

public protocol P {
    func foo() -> Self
}

extension Int: P {
    func foo() -> Int {
        return self + 1
    }
}

// C does not and cannot declare conformance to P
class C {
    private let i: Int
    init(_ value: Int) {
        i = value
    }
    
    // user declares:
    forward P to i
    
    // compiler synthesizes:
    func foo() -> C {
        return C(i.foo())
    }
}
Detailed design

TODO: grammar modification to add the forward declaration

Automatic forwarding only synthesizes member implementations. It does not 
automatically conform the forwarding type to the protocol(s) that are 
forwarded. If actual conformance is desired (as it usually will be) it must be 
explicitly stated.
The forwardee type need not actually conform to the protocol forwarded to it. 
It only needs to implement the members the forwarder must access in the 
synthesized forwarding methods. This is particularly important as long as 
protocol existentials do not conform to the protocol itself.
While it will not be possible to conform non-final classes to protocols 
containing a Self return type forwarding should still be allowed. The 
synthesized methods will have a return type of the non-final class which in 
which the forwarding declaration occured. The synthesized methods may still be 
useful in cases where actual protocol conformance is not necessary.
All synthesized members recieve access control modifiers matching the access 
control modifier applied to the forward declaration.
TODO: How should other annotations on the forwardee implementations of 
forwarded members (such as @warn_unused_result) be handled? 
It is possible that the member implementations synthesized by forwarding will 
conflict with existing members or with each other (when forwarding more than 
one protocol). All such conflicts, with one exception, should produce a 
compiler error at the site of the forwarding declaration which resulted in 
conflicting members.
One specific case that should not be considered a conflict is when forwarding 
more than one protocol with identical member declarations to the same member of 
the forwarding type. In this case the synthesized implementation required to 
forward all of the protocols is identical. The compiler should not synthesize 
multiple copies of the implementation and then report a redeclaration error.
It is likely that any attempt to forward different protocols with Self return 
types to more than one member of the same type will result in sensible 
behavior. This should probable be a compiler error. For example:
protocol P {
    func foo() -> Self
}
protocol Q {
    func bar() -> Self
}

struct Forwarder: P, Q {
    let d1: Double
    let d2: Double
    
    forward P to d1
    forward Q to d2

    func transformedForwardingReturnValue(_ forwardeeReturnValue: Double) -> 
Forwarder { 
        // What do we do here?  
        // We don't know if the return value resulted from forwarding foo to d1 
or bar to d2.
        // It is unlikely that the same behavior is correct in both cases.
    }
}
Impact on existing code

This is a strictly additive change. It has no impact on existing code.

Future enhancements

In the spirit of incremental change, this proposal focuses on core 
functionality. Several enhancements to the core functionality are possible and 
are likely to be explored in the future.

Partial forwarding synthesis

The current proposal makes automatic forwarding an “all or nothing” feature. In 
cases where you want to forward most of the implementation of a set of members 
but would need to “override” one or more specific members the current proposal 
will not help. You will still be required to forward the entire protocol 
manually. Attempting to implement some specific members manually will result in 
a redeclaration error.

This proposal does not allow partial forwarding synthesis in order to focus on 
the basic forwarding mechanism and allow us to gain some experience with that 
first, before considering the best way to make partial forwarding possible 
without introducing unintended potential for error. One example of a 
consideration that may apply is whether or not forwardee types should be able 
to mark members as “final for forwarding” in some way that prevents them from 
being “overriden” by a forwarder.

Newtype

While the current proposal provides the basic behavior desired for newtype, it 
is not as concise as it could be. Adding syntactic sugar to make this common 
case more concise would be straightforward:

// user declares
newtype Weight = Double forwarding P, Q

// compiler synthesizes
struct Weight: P, Q {
    var value: Double
    forward P, Q to value
    init(_ value: Double) { self.value = value }
}
However, there are additional nuances related to associated types 
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151228/004735.html>
 that should be considered and addressed by a newtype proposal.

Forwarding declaration in protocol extensions

It may be possible to allow the forward declaration in protocol extensions by 
forwarding to a required property of the protocol. This may have implementation 
complexities and other implications which would hold back the current proposal 
if it required the forward declaration to be allowed in protocol extensions. 

Alternatives considered

Specify forwarding as part of the member declaration

I originally thought it would make the most sense to specify forwarding 
alongside the forwardee member declaration. This proposal does not do so for 
the following reasons:

We must be able to specify access control for the forwarded members that are 
synthesized. Introducing a forwarding declaration is the most clear way to 
allow this.
It will sometimes be necessary to forward different protocols to the same 
forwardee with different access control levels. It would be very clunky to do 
this as part of the member declaration.
It should be possible to synthesize forwarding retroactively as part of an 
extension. This would not be possible if forwarding had to be specified in the 
original member declaration.
Require the forwardee to conform to the protocol(s) forwarded to it

There is not a compelling reason to require this. It is not necessary to 
synthesize and compile the forwarding methods and it would prevent the use of 
protocol existentials as the forwardee.

Automatically conform the forwarding type to the forwarded protocol(s)

It may seem reasonable to automatically synthesize conformance to the protocol 
in addition to the member implementations. This proposal does not do so for the 
following reasons:

Forwarding is considered an implementation detail that is not necessarily 
visible in the public interface of the type. The forwardee may be a private 
member of the type.
Type authors may wish to control where the actual conformance is declared, 
especially if protocol conformances are allowed to have access control in the 
future.
There may be use cases where it is desirable to have the forwarded members 
synthesized without actually conforming to the protocol. This is somewhat 
speculative, but there is not a compelling reason to disallow it.
Allow forwarding of all protocols conformed to by the forwardee without 
explicitly listing them

It may seem reasonable to have a * placeholder which will forward all visible 
protocol conformances of the forwardee type. This proposal does not include 
such a placeholder for the following reasons:

A placeholder like this could lead to unintended operations being synthesized 
if additional conformances are declared in the future. The new conformances 
could even lead to conflicts during synthesis which cause the code to fail to 
compile. The potential for such breakage is not acceptable.
A placeholder like this would not necessarily cause all desired forwarding 
methods to be synthesized. This would be the case when the members necessary to 
conform exist but actual conformance does not exist. This would be the case 
when the forwardee type is an existential. This could lead to programmer 
confusion.
An explicit list of protocols to forward is not unduely burdensome. It is 
straightforward to declare a new protocol that inherits from a group of 
protocols which are commonly forwarded together and use the new protocol in the 
forwarding declaration.
This is easily added as a future enhancement to the current proposal if we 
later decide it is necessary.
Allow forwarding of the entire interface of the forwardee type, not just 
specific protocols

It is impossible to synthesize forwarding of methods which contain the 
forwardee type as a parameter or return type that are not declared as part of a 
protocol interface in a correct and safe manner. This is because it may or may 
not be correct to promote the forwardee type in the signature to the forwarder. 

As an example, consider the following extension to Double. Imagine trying to 
synthesize a forwarding method in a Pixel type that forwards to Double. Should 
the return type be Pixel or Double? It is impossible to tell for sure.

extension Double {
    func foo() -> Double {
        return self
    }
}
When the method is declared in a protocol it becomes obvious what the signature 
of the forwarding method must be. If the protocol declares the return type as 
Self, the forwarding method must have a return type of Pixel. If the protocol 
declares the return type as Double the forwarding method will continue to have 
a return type of Double.

Allow forwarding to Optional members

It may seem like a good idea to allow synthesized forwarding to Optional 
members where a no-op results when the Optional is nil. There is no way to make 
this work in general as it would be impossible to forward any member requiring 
a return value. If use cases for forwarding to Optionalmembers emerege that are 
restricted to protocols with no members requiring return values the automatic 
protocol forwarding feature could be enhanced in the future to support these 
use cases.

Allow forwarding the same protocol(s) to more than one member

As with forwarding to Optional members, forwarding the same protocol to more 
than one member is not possible in general. However it is possible in cases 
where no protocol members have a return value. If compelling use cases emerge 
to motivate automatic forwarding of such protocols to more than one member an 
enhancement could be proposed in the future.

Provide a mechanism for forwardees to know about the forwarder

Some types may be designed to be used as components that are always forwarded 
to by other types. Such types may wish to be able to communicate with the 
forwarding type in some way. This can be accomplished manually. 

If general patterns emerge in practice it may be possible to add support for 
them to the language. However, it would be preliminary to consider support for 
such a feature until we have significant experience with the basic forwarding 
mechanism itself.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to