Overriding specific methods when adopting protocols with extension


(Howard Lovatt) #1

Here is a proposal I put forwards last month on the same topic.

···

============================================================

# Proposal: Split extension into implementing methods and adding methods
and protocols retrospectively

## Revision history

Version | Date | Comment |
---------|--------------|--------------|
Draft 1 | 11 April 2017 | Initial version |
Draft 2 | 13 April 2017 | Added support for post-hoc conformance to a

protocol - replaced static final extensions with final extensions |

Draft 3 | 17 April 2017 | Added justification section |
Draft 4 | 2 May 2017 | Allow final extensions to be public and allow

ad-hoc code reuse |

## Introduction

Currently extension methods are confusing because they have different
dispatch rules for the same calling syntax. EG:

    public protocol P {
        func mP() -> String
     }
    extension P {
        func mP() -> String { return "P.mP" }
        func mE() -> String { return "P.mE" }
    }
    struct S: P {
        func mP() -> String { return "S.mP" }
        func mE() -> String { return "S.mE" }
    }
    let s = S()
    s.mP() // S.mP as expected
    s.mE() // S.mE as expected
    let p: P = s // Note: s now typed as P
    p.mP() // S.mP as expected
    p.mE() // P.mE unexpected!

The situation with classes is even more confusing:

    class C: P { /*gets the protocol extensions*/ }
    let pC: P = C()
    pC.mP() // P.mP as expected!
    pC.mE() // P.mE as expected!
    class D: C {
        /*override not allowed!*/ func mP() -> String { return "D.mP" }
        /*override not allowed!*/ func mE() -> String { return "D.mE" }
    }
    let pD: P = D()
    pD.mP() // P.mP unexpected!
    pD.mE() // P.mE unexpected!

This proposal cures the above two problem by separating extension methods
into two seperate use cases: implementations for methods and adding methods
and protocols retrospectively. The proposal still retains retroactively
adding protocol conformance and ad-hoc code reuse, however these are made
easy to understand and safe.

## Implementing methods in same file as type declaration

If the extension is in the **same** file as the type declaration then its
implemented methods are dispatched using a Vtable for protocols and classes
and statically for structs and enums. EG:

File P.swift

    protocol P {
        // func m() not declared in type since it is added by the extension,
under this proposal it is an error to include a declaration in a type
**and** in an extension
    }
    extension P {
        func m() { print("P.m") } // m is added to the protocol declaration
    }

Same or another file

    struct S: P {
        override func m() { print("S.m") } // Note override required
because m already has an implementation from the extension
    }
    let p: P = S() // Note typed as P
    p.m() // Now prints S.m as expected

Extensions in the same file as the declaration can have any access, can be
final, and can have where clauses and provide inheritable implementations.
Ad-hoc code reuse is supported, in particular if a class/enum/strict
already had a method, m say, and a protocol, P say, required an m then an
extension that added P would not need to provide m (i.e. as at present).

In a protocol at present you can declare a method that is then implemented
in an extension without the use of the override keyword. This situation
only applies to protocols, for structs/enumerated/classes you cannot
declare in type and implement in an extension at all. This proposal unifies
the behaviour of protocol/struct/enum/class with extensions and also
prevents the error of a minor typo between the protocol and extension adding
two methods instead of generating an error, by requiring either:

  1. The method is only declared in the protocol and not in any extensions and
is therefore abstract
  2. The method is only in one extension and not in the protocol

A method can be abstract in one protocol and implemented in a second
protocol that extends the first.

The implementation needed to achieve this proposal for a protocol is that a
value instance typed as a protocol is copied onto the heap, a pointer to
its Vtable added, and its address passed/copied (i.e. it becomes a class
instance). No change is needed for a class instance typed as a protocol,
which unlike at present can now be passed/copied as a protocol directly.
Think of a protocol as like an abstract class; cannot be instantiated like
an abstract class and which possibly has abstract methods, but in different
in that it cannot have fields but can be multiply inherited.

Static and final methods implemented in extensions are not part of the
Vtable and are statically dispatched, i.e. no change from current Swift for
static but final now has the expected meaning for a protocol. Dispatching
for structs and classes unchanged.

## Retrospectively adding protocols and methods

A new type of extension is proposed, a `final extension`, which can be
either in or outside the file in which the protocol/struct/enum/class
declaration is in:

File P.swift

    protocol P {}
    extension P {
        func m() { print("P.m") } // m is added to the protocol declaration
    }

Same or another file

    struct S: P {} // Inherits m from the extension

In file P2.swift

    protocol P2 {
        func m2()
        func m() // Note same signature as P.m which S already implements
    }

In same or another file

    final extension S: P2 { // Note extension marked final
        // m cannot be provided because S already has a final m (the
inherited method must be final)
        func m2() { print("SP2.m2") } // Implicitly final, completes
implementation of P2
        func mE() { print("SP2.mE") } // Implicitly final, not an existing
method
    }

Which are called as any other method would be called:

    let s = S() // or S() as P2 or s: P2
    s.m() // Prints S.m
    s.m2() // Prints SP2.m2
    s.mE() // Prints SP2.mE

Notes:

  1. A method added by a `final extension`, e.g. `mE`, is implicitly final
(as the name would suggest).

  2. If the `final extension` adds a method, e.g. `mE`, that method cannot
already exist. IE a `final extension` cannot override an existing method or
implement a protocol declared method that lacks an implementation (unless
it also adds the protocol). This is retroactively adding a method. Also see
next point.

  3. If the `final extension` adds a protocol, e.g. `P2`, then it must
implement all the methods in that protocol that are not implemented, e.g.
`m2`. This is retroactively adding protocol conformance. Also see next
point.

  4. If the `final extension` adds a protocol, e.g. `P2`, then it inherits
all the methods in that protocol that are implemented, e.g. `m`. These
inherited methods must be final. This is ad-hoc code reuse of final methods
when retroactively adding protocol conformance.

Final-extensions can have `where` clauses.

The implementation for a `final extension` is always static dispatching.
That is why all methods involved in a `final extension` are final. The
compiler always knows that the method can be called statically and there is
no need for a Vtable entry for any of the methods, it is as though the
methods were declared static but with the more convenient syntax of a
normal method.

## Justification

The aim of Swift is nothing more than dominating the world. Using the
current, April 2017, https://www.tiobe.com/tiobe-index/
index of job adverts for programmers the languages that are in demand are:
Java 15.568%, C 6.966%, C++ 4.554%, C# 3.579%, Python 3.457%, PHP 3.376%,
Visual Basic .NET 3.251%, JavaScript 2.851%, Delphi/Object Pascal 2.816%,
Perl 2.413%, Ruby 2.310%, and Swift 2.287%. So Swift at 12th is doing very
well for a new language and is already above Objective-C at 14th. However
there is obviously a long way to go and the purpose of this proposal is to
help with this climb.

A characteristic of many of the languages above Swift in the Tiobe Index is
that they have major third party libraries; for some languages they are
almost defined by their third part libraries, e.g. Ruby for Rails. A major
part of this proposal is to make extensions safe when using multiple
libraries from different venders. In particular, the two forms of extensions in
this proposal can safely be exported.

As part of Swift's goal of world domination is that it is meant to be easy
to learn by a process of "successive disclosure". The current inconsistent
behaviour of protocols and extensions hinders this process and is a common
gotcha for newbies. This proposal eliminates that problem also.

Extensions are not new in languages, they are part of the .NET languages
for example. Since .NET popularised extensions they have been discussed by
other language communities, particularly Java and Scala, and in the
academic community (normally termed the Expression Problem) however they
have not proved popular because of the problems they cause. Nearly all
languages have a strong bias towards keeping the language small and simple
and trade of the advantages of a feature against the disadvantages. The
feature only makes it into the language if it offers many advantages, has
few disadvantages, and is not heavily overlapping with other features. It
is this keeping it small and simple test that extensions have failed in
other languages, in particular their behaviour is hard to predict in a
large code base with multiple third party libraries.

However, extensions are popular in Swift and this proposals makes a few
changes to them to make their behaviour predictable both in terms of third
party libraries and in terms of method dispatch when the variable is typed
as a protocol. Thereby still providing extensions including retroactive
conformance and ad-hoc code reuse, but without the problems.

## Possible future work (not part of this proposal)

This proposal will naturally allow bodies to be added to protocols directly
rather than via an extension, since under the proposal the extensionadds
the declaration to the type so it is a small step to allow the protocol
methods to have an implementation.

In an opposite sense to the above adding bodies to protocols, extensions could
be allowed to add method declarations without bodies to protocols.

The two above future work proposals, if both added, would add symmetry to
where declarations and bodies may appear for protocols.

## In summary

The proposal formalises the split use of extensions into their two uses:
implementing methods and retroactively adding protocols and methods (in
both cases including ad-hoc code reuse). The purpose of this split is to
eliminate the problems associated with exceptions that have been well
documented both with respect to Swift and other languages. Syntax is added
that clarifies their two use cases (implementing methods and retroactively
adding):

  1. The former are termed extensions and must be in the same file as the
type is declared, but can have non-final or final methods.
  2. The latter are termed final-extensions and can be in any file, however
final-extensions only have final methods.

Note the distinction between an extension in the same file and in a
separate file is consistent with the philosophy that there is special
status to the same file as proposed for private in <http://github.com/apple/
swift-evolution/blob/master/proposals/0169-improve-
interaction-between-private-declarations-and-extensions.md

.

============================================================


(Björn Forster) #2

As the point Allowing subclasses to override requirements satisfied by
defaults (*)in the generics manifesto is potentially source breaking in the
way the software behaves, is it still on the table for Swift 4?
Maybe someone from the core team can comment on this? :slight_smile:

···

On Wed, May 10, 2017 at 11:00 AM, Howard Lovatt via swift-evolution < swift-evolution@swift.org> wrote:

Here is a proposal I put forwards last month on the same topic.

============================================================

# Proposal: Split extension into implementing methods and adding methods
and protocols retrospectively

## Revision history

> Version | Date | Comment |
>---------|--------------|--------------|
> Draft 1 | 11 April 2017 | Initial version |
> Draft 2 | 13 April 2017 | Added support for post-hoc conformance to a
protocol - replaced static final extensions with final extensions |
> Draft 3 | 17 April 2017 | Added justification section |
> Draft 4 | 2 May 2017 | Allow final extensions to be public and allow
ad-hoc code reuse |

## Introduction

Currently extension methods are confusing because they have different
dispatch rules for the same calling syntax. EG:

    public protocol P {
        func mP() -> String
     }
    extension P {
        func mP() -> String { return "P.mP" }
        func mE() -> String { return "P.mE" }
    }
    struct S: P {
        func mP() -> String { return "S.mP" }
        func mE() -> String { return "S.mE" }
    }
    let s = S()
    s.mP() // S.mP as expected
    s.mE() // S.mE as expected
    let p: P = s // Note: s now typed as P
    p.mP() // S.mP as expected
    p.mE() // P.mE unexpected!

The situation with classes is even more confusing:

    class C: P { /*gets the protocol extensions*/ }
    let pC: P = C()
    pC.mP() // P.mP as expected!
    pC.mE() // P.mE as expected!
    class D: C {
        /*override not allowed!*/ func mP() -> String { return "D.mP" }
        /*override not allowed!*/ func mE() -> String { return "D.mE" }
    }
    let pD: P = D()
    pD.mP() // P.mP unexpected!
    pD.mE() // P.mE unexpected!

This proposal cures the above two problem by separating extension methods
into two seperate use cases: implementations for methods and adding
methods and protocols retrospectively. The proposal still retains
retroactively adding protocol conformance and ad-hoc code reuse, however
these are made easy to understand and safe.

## Implementing methods in same file as type declaration

If the extension is in the **same** file as the type declaration then its
implemented methods are dispatched using a Vtable for protocols and classes
and statically for structs and enums. EG:

File P.swift

    protocol P {
        // func m() not declared in type since it is added by the
extension, under this proposal it is an error to include a declaration in
a type **and** in an extension
    }
    extension P {
        func m() { print("P.m") } // m is added to the protocol declaration
    }

Same or another file

    struct S: P {
        override func m() { print("S.m") } // Note override required
because m already has an implementation from the extension
    }
    let p: P = S() // Note typed as P
    p.m() // Now prints S.m as expected

Extensions in the same file as the declaration can have any access, can
be final, and can have where clauses and provide inheritable
implementations. Ad-hoc code reuse is supported, in particular if a
class/enum/strict already had a method, m say, and a protocol, P say,
required an m then an extension that added P would not need to provide m
(i.e. as at present).

In a protocol at present you can declare a method that is then implemented
in an extension without the use of the override keyword. This situation
only applies to protocols, for structs/enumerated/classes you cannot
declare in type and implement in an extension at all. This proposal
unifies the behaviour of protocol/struct/enum/class with extensions and
also prevents the error of a minor typo between the protocol and extension adding
two methods instead of generating an error, by requiring either:

  1. The method is only declared in the protocol and not in any extensions and
is therefore abstract
  2. The method is only in one extension and not in the protocol

A method can be abstract in one protocol and implemented in a second
protocol that extends the first.

The implementation needed to achieve this proposal for a protocol is that
a value instance typed as a protocol is copied onto the heap, a pointer to
its Vtable added, and its address passed/copied (i.e. it becomes a class
instance). No change is needed for a class instance typed as a protocol,
which unlike at present can now be passed/copied as a protocol directly.
Think of a protocol as like an abstract class; cannot be instantiated like
an abstract class and which possibly has abstract methods, but in different
in that it cannot have fields but can be multiply inherited.

Static and final methods implemented in extensions are not part of the
Vtable and are statically dispatched, i.e. no change from current Swift for
static but final now has the expected meaning for a protocol. Dispatching
for structs and classes unchanged.

## Retrospectively adding protocols and methods

A new type of extension is proposed, a `final extension`, which can be
either in or outside the file in which the protocol/struct/enum/class
declaration is in:

File P.swift

    protocol P {}
    extension P {
        func m() { print("P.m") } // m is added to the protocol declaration
    }

Same or another file

    struct S: P {} // Inherits m from the extension

In file P2.swift

    protocol P2 {
        func m2()
        func m() // Note same signature as P.m which S already implements
    }

In same or another file

    final extension S: P2 { // Note extension marked final
        // m cannot be provided because S already has a final m (the
inherited method must be final)
        func m2() { print("SP2.m2") } // Implicitly final, completes
implementation of P2
        func mE() { print("SP2.mE") } // Implicitly final, not an existing
method
    }

Which are called as any other method would be called:

    let s = S() // or S() as P2 or s: P2
    s.m() // Prints S.m
    s.m2() // Prints SP2.m2
    s.mE() // Prints SP2.mE

Notes:

  1. A method added by a `final extension`, e.g. `mE`, is implicitly
final (as the name would suggest).

  2. If the `final extension` adds a method, e.g. `mE`, that method
cannot already exist. IE a `final extension` cannot override an existing
method or implement a protocol declared method that lacks an implementation
(unless it also adds the protocol). This is retroactively adding a method.
Also see next point.

  3. If the `final extension` adds a protocol, e.g. `P2`, then it must
implement all the methods in that protocol that are not implemented, e.g.
`m2`. This is retroactively adding protocol conformance. Also see next
point.

  4. If the `final extension` adds a protocol, e.g. `P2`, then it
inherits all the methods in that protocol that are implemented, e.g. `m`.
These inherited methods must be final. This is ad-hoc code reuse of final
methods when retroactively adding protocol conformance.

Final-extensions can have `where` clauses.

The implementation for a `final extension` is always static dispatching.
That is why all methods involved in a `final extension` are final. The
compiler always knows that the method can be called statically and there is
no need for a Vtable entry for any of the methods, it is as though the
methods were declared static but with the more convenient syntax of a
normal method.

## Justification

The aim of Swift is nothing more than dominating the world. Using the
current, April 2017, https://www.tiobe.com/tiobe-index/
index of job adverts for programmers the languages that are in demand
are: Java 15.568%, C 6.966%, C++ 4.554%, C# 3.579%, Python 3.457%, PHP
3.376%, Visual Basic .NET 3.251%, JavaScript 2.851%, Delphi/Object Pascal
2.816%, Perl 2.413%, Ruby 2.310%, and Swift 2.287%. So Swift at 12th is
doing very well for a new language and is already above Objective-C at
14th. However there is obviously a long way to go and the purpose of this
proposal is to help with this climb.

A characteristic of many of the languages above Swift in the Tiobe Index
is that they have major third party libraries; for some languages they are
almost defined by their third part libraries, e.g. Ruby for Rails. A major
part of this proposal is to make extensions safe when using multiple
libraries from different venders. In particular, the two forms of
extensions in this proposal can safely be exported.

As part of Swift's goal of world domination is that it is meant to be easy
to learn by a process of "successive disclosure". The current inconsistent
behaviour of protocols and extensions hinders this process and is a
common gotcha for newbies. This proposal eliminates that problem also.

Extensions are not new in languages, they are part of the .NET languages
for example. Since .NET popularised extensions they have been discussed
by other language communities, particularly Java and Scala, and in the
academic community (normally termed the Expression Problem) however they
have not proved popular because of the problems they cause. Nearly all
languages have a strong bias towards keeping the language small and simple
and trade of the advantages of a feature against the disadvantages. The
feature only makes it into the language if it offers many advantages, has
few disadvantages, and is not heavily overlapping with other features. It
is this keeping it small and simple test that extensions have failed in
other languages, in particular their behaviour is hard to predict in a
large code base with multiple third party libraries.

However, extensions are popular in Swift and this proposals makes a few
changes to them to make their behaviour predictable both in terms of third
party libraries and in terms of method dispatch when the variable is typed
as a protocol. Thereby still providing extensions including retroactive
conformance and ad-hoc code reuse, but without the problems.

## Possible future work (not part of this proposal)

This proposal will naturally allow bodies to be added to protocols
directly rather than via an extension, since under the proposal the
extensionadds the declaration to the type so it is a small step to allow
the protocol methods to have an implementation.

In an opposite sense to the above adding bodies to protocols, extensions could
be allowed to add method declarations without bodies to protocols.

The two above future work proposals, if both added, would add symmetry to
where declarations and bodies may appear for protocols.

## In summary

The proposal formalises the split use of extensions into their two uses:
implementing methods and retroactively adding protocols and methods (in
both cases including ad-hoc code reuse). The purpose of this split is to
eliminate the problems associated with exceptions that have been well
documented both with respect to Swift and other languages. Syntax is added
that clarifies their two use cases (implementing methods and retroactively
adding):

  1. The former are termed extensions and must be in the same file as the
type is declared, but can have non-final or final methods.
  2. The latter are termed final-extensions and can be in any file,
however final-extensions only have final methods.

Note the distinction between an extension in the same file and in a
separate file is consistent with the philosophy that there is special
status to the same file as proposed for private in <
http://github.com/apple/swift-evolution/blob/master/proposa
ls/0169-improve-interaction-between-private-declarations-and-extensions.md
>.

============================================================

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