Skip to content

Instantly share code, notes, and snippets.

@karwa
Last active October 23, 2019 02:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karwa/4c6bff75f8fa84b16df2c8caae97d622 to your computer and use it in GitHub Desktop.
Save karwa/4c6bff75f8fa84b16df2c8caae97d622 to your computer and use it in GitHub Desktop.

Ease restrictions on protocol nesting

During the review process, add the following fields as needed:

Introduction

Allow protocols to be nested in other types, and for other types (including other protocols) to be nested inside protocols, subject to a few constraints.

Swift-evolution thread: Discussion thread topic for that proposal

Motivation

Nesting types inside other types allows us to scope their usage and provide a cleaner interface. Protocols are an important part of Swift, and many popular patterns (for example, the delegate pattern) define protocols which are intended to be used in the context of other types. It would be nice to apply type-nesting here: MyClass.Delegate reads better than MyClassDelegate, and literally brings structure to large frameworks.

Similarly, we have examples in the standard library where supporting types are defined with the intention that they be used in the context of some protocol - FloatingPointClassification, FloatingPointSign, and FloatingPointRoundingRule are enums which are used by various members of the FloatingPoint protocol. It would also be nice to apply type-nesting here, with the enums belonging to the protocol itself - e.g. FloatingPoint.Sign.

Proposed solution

There are two important restrictions to this proposal:

  • Nested protocols may not capture generic type parameters from their contexts
  • Nested types (including protocols) may not capture associated types from their contexts

These restrictions are due to currently-limited support for existential types. There are many interesting ideas to overcome both, but after discussions on the mailing lists, we should be able to handle most of the common cases with these limitations and it keeps things reasonable to implement.

The first part is to allow protocols to be nested inside of structural types (for example, in the delegate pattern):

class AView : MYView {

    protocol Delegate : class {
        func somethingHappened()
    }
    weak var delegate : Delegate?
    
    func doSomething() {
        //...
        delegate?.somethingHappened()
    }
}

class AController : MYViewController, AView.Delegate {
    
    func somethingHappened() {
        // Respond to callback
    }
}

Similarly, we will allow structural types to be nested inside of protocols (such as the standard library's FloatingPoint* enums):

protocol FloatingPoint {
    
    enum Sign {
        case plus
        case minus
    }
    
    var sign: Sign { get }
}

struct Float : FloatingPoint {

    var sign: FloatingPoint.Sign { /* return the sign */ }
}

And the same for protocols inside of protocols:

protocol TextStream {
    protocol Transformer {
        func transform(_: Character) -> Character
    }
    
    var transformers : [Transformer] { get set }
    func getNextCharacter() -> Character
}

struct WeLoveUmlauts : TextStream.Transformer {
    func transform(_ char: Character) -> Character {
        switch char {
            case "a".characters.first!: return "ä"
            case "e".characters.first!: return "ë"
            //...etc
            default: return char
        }
    }
}

In all of the examples, any of the structual types may have generic types, and any of the protocols may have associated types. So long as the restrictions mentioned earlier are observed, i.e. that no types are captured between a protocol and its outer or inner types.

Source compatibility

This change is additive, although there are a couple of places in the standard library where we might consider reorganising things after this change. Those changes are not a part of this proposal.

Effect on ABI stability

Would change the standard library ABI if it chose to adopt the feature.

Effect on API resilience

Nesting changes the name (both in source and symbolic) of the relevant types. Has the same effect as other type renamings/nesting and un-nesting.

Alternatives considered

The alternative is to namespace your types manually with a prefix, similar to what the standard library, Apple SDK overlays, and existing Swift programs already do. However, nested types and cleaner namespaces are one of the little things that developers - espcially coming from Objective-C - have always been really excited about. From time to time somebody pops up on the mailing list to ask why we don't have it yet for protocols, and changes proposed here usually are met with broad support.

@slavapestov
Copy link

slavapestov commented Oct 24, 2016

I think the terminology and distinctions used here are not quite correct. I suggest replacing "simple protocols" and mention of generic types with the following:

  • Defining associated types on nested protocols should be allowed. The tricky case is when the inner protocol references associated types of the outer protocol.
  • When nesting a type inside a protocol, it should not matter if the nested type is generic or not. The key is whether or not the nested type refers to outer associated types. If it does, then you need to provide the Self substitution when referencing the nested type from outside the protocol's scope.
  • Similarly, nesting a protocol inside a generic type is OK, as long as the generic parameters are "promoted" to associated types of the inner protocol.

As far as ABI resilience goes, the key concern is whether adding members to nested types in protocols changes the ABI. Normally, adding a new protocol requirement does not change ABI as long as the requirement has a default implementation provided by an unconstrained protocol extension. However, if capturing an outer associated type is implemented by adding a hidden Self generic parameter to the nested type, then it will be possible to change ABI, by adding a new requirement that references Self in the case where no existing requirement references Self.

@slavapestov
Copy link

Finally, regarding source stability, what we want to do is ensure that if existing stdlib protocols become nested types, we can compile old code by defining type aliases to map the old names to the new names. Please think this through and see if there are any issues (there shouldn't be).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment