Reconsider the semantics of type aliases in protocol extensions

This pitch is the continuation of Source breaking ability to override associated type in protocol extension

The ability to declare type aliases that share names with associated types in protocols and protocol extensions has been a workaround for same-type constraints in protocol declarations, introduced in Swift 4 (SE-0142).

Here, the declaration of a typealias that shares its name with an associatedtype is tantamount to a same-type constraint that is syntactically and semantically inconsistent with the language. This feature violates the rule that declarations in a protocol extension should never conflict with its requirements (the typealias does not act as a default value) and introduces counterproductive situations that often have no practical value and are thus hard to assess and diagnose.

protocol P {
    associatedtype E
    func foo(_ arg: E)
}
protocol P1: P {...} 

class Foo: P {
    typealias E = String

    func foo(_ arg: E) {}
}

// This will break Foo and any other conformances to P and its descendants *: P 
extension P { typealias E = Bool } 

Swift 4.1 began preparations to eliminate the feature by adding warnings and disallowing such type aliases in protocol extensions by raising an ambiguity error:

// #1 
protocol P {associatedtype A}
protocol P1: P {typealias A = Int}
// warning: typealias overriding associated type 'R' from protocol 'P' is better expressed as
// same-type constraint on the protocol

// #2
protocol P {typealias A = Int}
protocol P1: P {associatedtype A}
// warning: associated type 'R' is redundant with type 'R' declared in inherited protocol 'P'

Now that we have powerful enough where clauses for protocol declarations, I propose to bring consistency to the grammar of using type aliases within protocols.

  • Permit type aliases to act as default values for associated types in protocol extensions or change the error message to propose the current syntax instead of simply producing %0 is ambiguous for type lookup in this context, which is actually a generic error.
  • Type aliases in protocols should be disallowed always, the same way we disallow any implementation details.
  • The above warnings should be errors in Swift 5 and in the case when such a warning is not produced, emit a generic typealias should not be declared within a protocol warning for Swift 4.2 and error for Swift 5.
4 Likes

Good idea

I want to encourage the community to discuss how to go about this.

Currently I don't see anything fundamentally wrong in having a typealias in a protocol extension with the same name as an associatedtype. This is the way we implement default values in Swift, even though there is an alternate syntax for associatedtype defaults. However, these kind of type aliases do not work as default values for associated types. I feel like the 'swifty' way of resolving the situation here is to either restore the default-specifying behavior for type aliases or simply change the error message to explain the right way of doing it with a fixit. This will prevent part of the source-breaking consequences this pitch implies.

That said, the only real issue we have to fix for Swift 5 is the ability to declare type aliases in protocols.

3 Likes

The SE PR – Reconsider semantics of type aliases in protocol extensions by AnthonyLatsis · Pull Request #857 · apple/swift-evolution · GitHub

and the PR for master – [GSB][SE][WIP] Reconsider typealias usage in protocols and prot ext by AnthonyLatsis · Pull Request #16966 · apple/swift · GitHub

Feedback appreciated!

This proposal is called

Reconsider how type aliases are used within protocols and their extensions

As demonstrated by the following example programs, the underlying issue is (afaict) not limited to the context of protocols, so there's a more general need to:

Reconsider how type aliases are used within types and their extensions


Question:

As you are aware of the more genaral issue, I assume that there are good reasons behind the decision to focus on the protocol-specific side of this problem first, but I'd still like to know what they are, so my question is:

Why focus on protocols first?

NOTE: I'm asking this because, without knowing anything about compiler hacking, I'm just thinking that what if:

  • Getting to the bottom of the more general issue will result in a better fix, one that might even turn out to be a simplification of the implementation.

whereas:

  • Looking at all (possibly) related issues one by one, isolated from each other in no particular order, will result in a more complex implementation (adding band-aids / special cases, and presumably as a consequence of this: more bugs).

?


Example programs:

If "a (value) type's binary layout should be solely determined by the primary definitions, and an error should occur if an extension is needed to complete the binary layout of a type, or would change the layout", then Swift accepts invalid, as it successfully compiles this program:

struct S {
    let v: A
}
extension S {
    typealias A = Int
}

With generic structs, the problem is more obvious:

struct S<T> {
    let a: (X, Y) // Swift happily accepts (X, Y) as being (Bool, String),
                  // even though T can't be both Int and Float.
}
extension S where T == Int {
    typealias X = Bool
}
extension S where T == Float {
    typealias Y = String
}

Some related bug reports

SR-5440 Typealias in constrained extension misinterprets the where clause

SR-3793 Protocol extension cannot implement associated type

SR-5392 Inconsistent "invalid redeclaration of typealias"

SR-7217 Protocol composition with conflicting typealiases does not diagnose

SR-7516 Compiler accepts ambiguous and/or invalid use of associated types

(I assume there are many more.)

Hi Jens,

With this proposal I am trying to fix some violations that are a result of how the language developed. Some are rudiments, others – intended decisions for convenience that are no longer relevant. In other words, I am trying to refactor an established grammar.

Everything you pointed out aren't really problems related to what this pitch tackles at the implementation level, so I doubt fixing them together will make a difference. Mostly, they are bugs or simply an early state of a not yet fully implemented feature. A good example are conditional conformances.

By the way, as long as the extension resides in the same module, this should be alright.
You can treat non-conditional extensions like these as part of the primary definition.

struct S {
    let v: A
    func roo() {foo()}
}
extension S {
    typealias A = Int
    func foo() {}
}

P.S.

Thanks for the bugs, I'll look if I can help with any of them.

1 Like

OK, as I have no idea about the implementation, I'll take your word for it (even though I'm quite surprised to learn that they are not related at the implementation level, and that fixing them together would not make a difference.)

Great, thanks!

I'm in favour of modifying (or at least documenting) how this part of the language works.

Type aliases in protocols should be always discouraged in Swift 4.2 and always disallowed in Swift 5, the same way we disallow any implementation details.

I'm not sure if you know this (the proposal doesn't mention it), but type-aliases inside protocols are not the same as associated types. They are just normal type-aliases which get inherited by conforming types. If we remove TAs in protocols, we remove this feature:

public protocol ABC {
 typealias ImportantType = Int
}
extension String: ABC {
  // since ImportantType is not an associated type, String does not get the ability to override it. 
  // This will cause the name String.ImportantType to be ambiguous.
  // typealias ImportantType = String 
}

let _: ABC.ImportantType = 99
let _: String.ImportantType = 42

I had some issues with this while thinking about nested types in protocols, and couldn't think of a good solution consistent with the rest of the language, or even any documentation on these existing rules (which as you noted, can be a bit brittle).

For example, imagine nesting the default Slice type inside Collection - even conformers which write their own SubSequence types would get this default Slice type inside of them too, even though they have a better implementation!

extension Collection {
  struct Slice<Base: Collection> { ... }
}
struct MyCustomCollection: Collection {
   struct CustomSlice: Collection { ... }
   typealias SubSequence = CustomSlice
}

let _: Collection.Slice<MyCustomCollection>  // Fine, you can do that if you want
let _: MyCustomCollection.Slice              // But this one shouldn't exist, IMO.

So I would really like if we could either:

  • Remove this typealias inheritance (likely too source-breaking), or
  • Add an explicit annotation that the type(alias) should be inherited, or
  • Think of some consistent rules which could apply to both types and type-aliases inside of protocols

Let's try to iterate through the bugs to hopefully bring some clarity.

Protocol composition with conflicting typealiases does not diagnose

This one's simply a diagnostic problem, since ranking shouldn't be involved in trying to give precedence to values of the same nature.

Compiler accepts ambiguous and/or invalid use of associated types
Typealias in constrained extension misinterprets the where clause
Inconsistent "invalid redeclaration of typealias"

Conditional conformances or conditional extensions and how type aliases are used within protocols are obviously quite different concepts. The only thing that changes (in the case with a conditional conformance) is the result of the conformance with a conflicting typealias been declared in a protocol extension (same-type constraint or default value). But that doesn't affect the issues whatsoever and... is wonderful since we can act without fear of breaking source.

Protocol extension cannot implement associated type

This one can be resolved, it seems to be working :tada:

1 Like

As input on how someone expects typealiases in protocols to work currently:

// some protocols
protocol BarP {}
protocol FooP {
  associatedtype Bar : BarP
}

protocol BazP {
  // `BazP` wants access to a `FooP` and a `BarP`
  associatedtype Foo: FooP
  // but `Foo` has a `BarP`, so use `Foo`'s
  typealias Bar = Foo.Bar
}

I suppose the above could also be an associatedtype with same type constraints...
But, would this work after these changes?

1 Like

That's a good example. @Karl's point is very similar. I might have been too harsh with type aliases within protocols.
What made me decide to disallow them is that a typealias is an implementation detail, not a requirement. We could do it this way:

protocol BazP {
  associatedtype Foo: FooP
}
extension BazP {typealias Bar = Foo.Bar}
// or
protocol BazP where Bar == Foo.Bar {
  associatedtype Foo: FooP
  associatedtype Bar
}

but now that I think of it, this might become inconvenient in certain cases.

Nevermind, I'm dumb. Sequence.Element is an associated type.

I agree. It might not be entirely consistent, but it is convenient as sugar for same-type constraints on associated types. I will amend the proposal to face conflicting type aliases. But I would like to still hear the opinion of @Douglas_Gregor, who actually reintroduced type aliases to protocols in SE-0092.
I am hesitating because this also removes the conflicting cases that arise because we have type aliases in protocols.

Anyways, these do not work currently

protocol A where T == Int {
  associatedtype T
}
protocol A {
  associatedtype T where T == Int
}

I'm not sure I understand the gist of your argument. There are currently bugs related to how typealias is implemented inside protocols. But instead of fixing the implementation, you're proposing to remove them from the language? On the rationale that implementations don't belong inside protocols?

That seems ill-advised given that both this feature, introduced in SE-0092, and enabling default implementations in protocols are important near-term parts of the same roadmap for completing generics.

1 Like

It's a bit different. To me, the fact that these bugs exist is a sign that the ability to declare type aliases in protocols conflicts with existing conventions and they do not belong to protocols. If you look into them, they are not simple bugs with a trivial fix. The fixes that come to mind rather imply workarounds and seem awkward. The bottom line is that a Swift protocol has always been a blueprint.

Just in case, I do not propose to remove them, I think it is reasonable for the language to be consistent about the established rules and treat type aliases just like any other requirement witness: allow them in protocol extensions as implementation details or default values for associated types and thus fix all the issues related to type aliases in protocols and protocol extensions. However, as you might have noticed, the above examples somewhat convinced me this (removing them from protocols) might be a counterproductive change. But that doesn't interfere with issues related to extensions. Concerning the manifesto, IMO allowing all kinds of implementation details in protocols isn't a single-valued question.

How can declaring a typealias in a protocol instead of an extension help in completing generics?

The removal decision apart for now, I will focus on changing the behavior of type aliases in protocol extensions so that they can be default values for associated types. This will relax the

protocol P {
  associatedtype A
  func foo() -> A // 'A' is ambiguous for type lookup in this context
}
extension P {typealias A = Int}

error with no breaking changes and bring consistency: also note that while this raises an error, removing foo will create a same-type constraint on A.

How does that sound?

cc @Douglas_Gregor

By the way, the example here from a 'bug' @Jens shared is another troublesome situation type aliases cause being allowed in protocols: [SR-7217] Protocol composition with conflicting typealiases does not diagnose · Issue #49765 · apple/swift · GitHub

For convenience:

struct Foo {}
struct Bar {}

protocol P1 { typealias T = Foo }
protocol P2 { typealias T = Bar }

typealias P3 = P1 & P2
print(P3.T.self) // => Bar()

typealias P4 = P2 & P1
print(P4.T.self) // => Bar()

You're kind of supposed to not be able to use T on a protocol metatype like any requirement, but you can because it's an implementation, not a requirement. How should this be fixed? Personally, I have no idea how to fix this without doing a workaround or introducing further contradiction.

Fwiw, the above example crashes the compiler when using a recent snapshot (2018-06-03):

Crash report
› /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2018-06-03-a.xctoolchain/usr/bin/swiftc --version
Apple Swift version 4.2-dev (LLVM 58850e66ae, Clang b58a7ad218, Swift feb385736b)
Target: x86_64-apple-darwin17.5.0
› /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2018-06-03-a.xctoolchain/usr/bin/swiftc -O test.swift
Assertion failed: (isa<X>(Val) && "cast<Ty>() argument of incompatible type!"), function cast, file /Users/buildnode/jenkins/workspace/oss-swift-package-osx/llvm/include/llvm/Support/Casting.h, line 255.
0  swift                    0x000000010dbd2b08 llvm::sys::PrintStackTrace(llvm::raw_ostream&) + 40
1  swift                    0x000000010dbd1d77 llvm::sys::RunSignalHandlers() + 39
2  swift                    0x000000010dbd3182 SignalHandler(int) + 258
3  libsystem_platform.dylib 0x00007fff79195f5a _sigtramp + 26
4  libsystem_platform.dylib 0x0f038e9c00000000 _sigtramp + 2263261376
5  libsystem_c.dylib        0x00007fff78f331ae abort + 127
6  libsystem_c.dylib        0x00007fff78efb1ac basename_r + 0
7  swift                    0x000000010ab739fa swift::Lowering::SILGenBuilder::createMetatype(swift::SILLocation, swift::SILType) + 554
8  swift                    0x000000010abb5b11 swift::ASTVisitor<(anonymous namespace)::RValueEmitter, swift::Lowering::RValue, void, void, void, void, void, swift::Lowering::SGFContext>::visit(swift::Expr*, swift::Lowering::SGFContext) + 8977
9  swift                    0x000000010abb4e11 swift::ASTVisitor<(anonymous namespace)::RValueEmitter, swift::Lowering::RValue, void, void, void, void, void, swift::Lowering::SGFContext>::visit(swift::Expr*, swift::Lowering::SGFContext) + 5649
10 swift                    0x000000010aba8da9 swift::Lowering::SILGenFunction::emitRValueAsSingleValue(swift::Expr*, swift::Lowering::SGFContext) + 57
11 swift                    0x000000010abfec3f swift::Lowering::SILGenFunction::emitRValueAsOrig(swift::Expr*, swift::Lowering::AbstractionPattern, swift::Lowering::TypeLowering const&, swift::Lowering::SGFContext) + 511
12 swift                    0x000000010abc3ea1 swift::Lowering::ManagedValue llvm::function_ref<swift::Lowering::ManagedValue (swift::Lowering::SGFContext)>::callback_fn<(anonymous namespace)::RValueEmitter::visitErasureExpr(swift::ErasureExpr*, swift::Lowering::SGFContext)::$_8>(long, swift::Lowering::SGFContext) + 113
13 swift                    0x000000010ab904c1 void llvm::function_ref<void (swift::SILValue)>::callback_fn<swift::Lowering::SILGenFunction::emitExistentialErasure(swift::SILLocation, swift::CanType, swift::Lowering::TypeLowering const&, swift::Lowering::TypeLowering const&, llvm::ArrayRef<swift::ProtocolConformanceRef>, swift::Lowering::SGFContext, llvm::function_ref<swift::Lowering::ManagedValue (swift::Lowering::SGFContext)>, bool)::$_5>(long, swift::SILValue) + 241
14 swift                    0x000000010ab7623f swift::Lowering::SILGenBuilder::bufferForExpr(swift::SILLocation, swift::SILType, swift::Lowering::TypeLowering const&, swift::Lowering::SGFContext, llvm::function_ref<void (swift::SILValue)>) + 143
15 swift                    0x000000010ab8babc swift::Lowering::SILGenFunction::emitExistentialErasure(swift::SILLocation, swift::CanType, swift::Lowering::TypeLowering const&, swift::Lowering::TypeLowering const&, llvm::ArrayRef<swift::ProtocolConformanceRef>, swift::Lowering::SGFContext, llvm::function_ref<swift::Lowering::ManagedValue (swift::Lowering::SGFContext)>, bool) + 3900
16 swift                    0x000000010abba88e swift::ASTVisitor<(anonymous namespace)::RValueEmitter, swift::Lowering::RValue, void, void, void, void, void, swift::Lowering::SGFContext>::visit(swift::Expr*, swift::Lowering::SGFContext) + 28814
17 swift                    0x000000010abb4e11 swift::ASTVisitor<(anonymous namespace)::RValueEmitter, swift::Lowering::RValue, void, void, void, void, void, swift::Lowering::SGFContext>::visit(swift::Expr*, swift::Lowering::SGFContext) + 5649
18 swift                    0x000000010aba301f swift::Lowering::SILGenFunction::emitExprInto(swift::Expr*, swift::Lowering::Initialization*, llvm::Optional<swift::SILLocation>) + 319
19 swift                    0x000000010ab29985 swift::Lowering::ArgumentSource::forwardInto(swift::Lowering::SILGenFunction&, swift::Lowering::Initialization*) && + 229
20 swift                    0x000000010ab2a862 swift::Lowering::ArgumentSource::forwardInto(swift::Lowering::SILGenFunction&, swift::Lowering::AbstractionPattern, swift::Lowering::Initialization*, swift::Lowering::TypeLowering const&) && + 706
21 swift                    0x000000010ab617e5 (anonymous namespace)::ArgEmitter::emit(swift::Lowering::ArgumentSource&&, swift::Lowering::AbstractionPattern) + 2517
22 swift                    0x000000010ab63cdd (anonymous namespace)::ArgEmitter::emitShuffle(swift::TupleShuffleExpr*, swift::Lowering::AbstractionPattern) + 4717
23 swift                    0x000000010ab627fe (anonymous namespace)::ArgEmitter::emitExpanded(swift::Lowering::ArgumentSource&&, swift::Lowering::AbstractionPattern) + 1470
24 swift                    0x000000010ab60ebe (anonymous namespace)::ArgEmitter::emit(swift::Lowering::ArgumentSource&&, swift::Lowering::AbstractionPattern) + 174
25 swift                    0x000000010ab5fef5 (anonymous namespace)::CallSite::emit(swift::Lowering::SILGenFunction&, swift::Lowering::AbstractionPattern, (anonymous namespace)::ParamLowering&, llvm::SmallVectorImpl<swift::Lowering::ManagedValue>&, llvm::SmallVectorImpl<(anonymous namespace)::DelayedArgument>&, llvm::Optional<swift::ForeignErrorConvention> const&, swift::ImportAsMemberStatus) && + 453
26 swift                    0x000000010ab5f82a (anonymous namespace)::CallEmission::emitArgumentsForNormalApply(swift::CanTypeWrapper<swift::FunctionType>&, swift::Lowering::AbstractionPattern&, swift::CanTypeWrapper<swift::SILFunctionType>, llvm::Optional<swift::ForeignErrorConvention> const&, swift::ImportAsMemberStatus, llvm::SmallVectorImpl<swift::Lowering::ManagedValue>&, llvm::Optional<swift::SILLocation>&, swift::CanTypeWrapper<swift::FunctionType>&) + 1914
27 swift                    0x000000010ab4d86d (anonymous namespace)::CallEmission::apply(swift::Lowering::SGFContext) + 3149
28 swift                    0x000000010ab4cb02 swift::Lowering::SILGenFunction::emitApplyExpr(swift::Expr*, swift::Lowering::SGFContext) + 1522
29 swift                    0x000000010abb3850 swift::ASTVisitor<(anonymous namespace)::RValueEmitter, swift::Lowering::RValue, void, void, void, void, void, swift::Lowering::SGFContext>::visit(swift::Expr*, swift::Lowering::SGFContext) + 80
30 swift                    0x000000010aba94c1 swift::Lowering::SILGenFunction::emitIgnoredExpr(swift::Expr*) + 1265
31 swift                    0x000000010ab43fa0 swift::Lowering::SILGenModule::visitTopLevelCodeDecl(swift::TopLevelCodeDecl*) + 352
32 swift                    0x000000010ab4472b swift::Lowering::SILGenModule::emitSourceFile(swift::SourceFile*, unsigned int) + 827
33 swift                    0x000000010ab45490 swift::SILModule::constructSIL(swift::ModuleDecl*, swift::SILOptions&, swift::FileUnit*, llvm::Optional<unsigned int>, bool) + 352
34 swift                    0x000000010ab45a9f swift::performSILGeneration(swift::FileUnit&, swift::SILOptions&, llvm::Optional<unsigned int>) + 95
35 swift                    0x000000010a4273b9 performCompile(swift::CompilerInstance&, swift::CompilerInvocation&, llvm::ArrayRef<char const*>, int&, swift::FrontendObserver*, swift::UnifiedStatsReporter*) + 8713
36 swift                    0x000000010a424161 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 3297
37 swift                    0x000000010a3e011c main + 2300
38 libdyld.dylib            0x00007fff78e87015 start + 1
Stack dump:
0.  Program arguments: /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2018-06-03-a.xctoolchain/usr/bin/swift -frontend -c -primary-file test.swift -target x86_64-apple-darwin17.5.0 -enable-objc-interop -O -color-diagnostics -module-name test -o /var/folders/50/br4kxvjd0t551h0fmtrzkwdw0000gn/T/test-b563d8.o 
<unknown>:0: error: unable to execute command: Abort trap: 6
<unknown>:0: error: compile command failed due to signal 6 (use -v to see invocation)
1 Like

I'd imagine it should be a compile-time error, because P3.T is ambiguous. (Isn't this what happens when, say, method names conflict?)

When it becomes possible to use existentials with self or associated type requirements, the way to avoid the error would naturally be to disambiguate by writing (P3 as P1).T or (P3 as P2).T.

Isn't this what happens when, say, method names conflict?

Well, yes, if you're not calling it on a protocol.

It's a bit strange though we can't call something with a default implementation on a static protocol type.