Reconsider the semantics of type aliases in protocol extensions

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.

The problem is actually more widespread than typealias, consider this example (e.g. [SR-5181] Protocol extension with constrained associated type doesn't work when using custom operator · Issue #47757 · apple/swift · GitHub):

protocol P {
  associatedtype T
  func foo()
  static func +(_: Self, _: Self) -> Self
}

extension P {
   func foo() { print("foo") }

   static func +(lhs: Self, rhs: Self) -> Self {
     print("+")
     return lhs
   }
}

extension P where Self.T == Int {
   func foo() { print("foo where T == Int") }

   static func +(lhs: Self, rhs: Self) -> Self {
     print("+ where T == Int")
     return rhs
   }
}

func bar<T: P>(_ t: T) where T.T == Int {
  t.foo()
  _ = t + t
}

class A<T> : P {}
class B<T> : P {
   func foo() { print("concrete foo") }
}

extension B {
   static func +(lhs: B<T>, rhs: B<T>) -> B<T> {
     print("concrete +")
     return rhs
  }
}

let a = A<Int>()

a.foo()   // prints "foo where T == Int"
_ = a + a // prints "+"

bar(a) // prints "foo"
       // prints "+"

let b = B<Int>()
bar(b) // prints "concrete foo"
       // prints "+"

_ = b + b // prints "concrete +"

It seems like the more general solution/proposal, to make this situation consistent, could be to make declarations in extensions be invisible for lookup from other contexts, which effectively makes method declarations "default" to protocol declaration if any.

So from type-checker perspective, it would only see either protocol or concrete type declaration in cases above and then dispatch to "default"ed implementations according to how types have been initialized e.g. for a + a it would be constrained extension, for t.foo() either constrained extension or overload on concrete type, so we get:

let a = A<Int>()

a.foo()   // prints "foo where T == Int"
_ = a + a // prints "+ where T == Int"

bar(a) // prints "foo where T == Int"
           // prints "+ where T == Int"

let b = B<Int>()
bar(b) // prints "concrete foo"
       // prints "concrete +"

_ = b + b // prints "concrete +"

WDYT?

I agree this situation should be diagnosed as ambiguous, but in fact protocol typealiases can be used on the protocol metatype as long as their underlying type does not mention Self. This is intended behavior and there is code and tests to support this.

Thanks for the report. It was a bug in Sema where we forgot to wrap the type in a metatype, which confused SILGen. I have a fix: Sema: Fix ConstraintSystem::getTypeOfMemberReference() for protocol typealiases by slavapestov · Pull Request #18465 · apple/swift · GitHub

Note that this bug would have occurred with typealiases in protocol extensions too. It's not specific to typealiases in protocols.

2 Likes

I disagree. I think we can fix these bugs, and treating typealiases in protocols from typealiases in extensions would actually be a step backwards.

I don't think removing them from protocols will actually simplify anything in the implementation because we still have to support typealiases in protocol extensions.

In fact I think most of the problems you identified stem from the fact that when performing a name lookup on a concrete type, we always pick a typealias in an unconstrained protocol extension over the associated type with the same name. This sounds like an undesirable behavior. They're not, fact being treated as same-type constraints -- there's a warning to that effect that was put in for migrating old code, but its just an isolated warning. The behavior you're observing is that when a type conforms to a protocol with an associated type, we consider typealiases with the same name as potential witnesses for the associated type. IMO, we should change the conformance lookup code to only consider typealiases as witnesses if they are defined in constrained extensions.

1 Like

Can you explain what doesn't work? The following example does compile:

protocol A1 where T == Int {
  associatedtype T
}

protocol A2 {
  associatedtype T where T == Int
}

struct S1 : A1 {
  typealias T = Int
}

struct S2 : A2 {
  typealias T = Int
}

Note that you have to restate T1 and T2 in S1 and S2. This is because the associated types do not have defaults, just because they're subject to a same-type constraint. We do check the same type constraint (doing typealias T = String in S1 or S2 produces an error as expected).

It would be nice to infer witnesses for associated types that are subject to same-type constraint because it makes a lot of intuitive sense; but that is a separate missing feature (in fact you filed the closely related bug [SR-7336] Protocol with same-type constraint on associated type "can only be used as generic constraint" · Issue #49884 · apple/swift · GitHub :-) )

2 Likes

Type aliases in protocols can do one thing associated types with defaults cannot -- be generic. This is totally valid code:

protocol P {
  associatedtype Element
  typealias Mapping<Key> = Dictionary<Key, Element>
}
5 Likes

@Slava_Pestov I changed my mind quite a bit about this matter thanks to the feedback, and there's a lot of refactoring to be done. In short,
I tend to agree we have to be able to solve the bugs and troublesome cases that are yet to be discussed without forcing people to declare type aliases only in protocol extensions.

I if remember correctly I was talking about the compiler allowing to override those associated types.


// Case #1
protocol A where T == Int {
  associatedtype T
}
// Case #2
protocol A {
  associatedtype T where T == Int
}

Actually, I think we shouldn't encourage these ways of imposing same-type constraints on associated types. I remember Case #1 being discussed in the proposal over here, and it was supposed to be an error. It might have been overlooked, which in my opinion is good, but I still think this specific case with a same-type constraint must be an error similar to what we get when func foo<T>(_ arg: T) where T == Int {}, with a fixit that turns it into a typealias. The same goes for Case #2.

I would prefer a type alias to act as a default value for an associated type if it's declared in a protocol extension.

That sounds a lot more useful than just unconditionally acting as an implementation of the associated type requirement. @Douglas_Gregor what do you think?

2 Likes

Yes, I think that's a more useful formulation.

Doug

1 Like

It seems I can no longer edit the thread title. The gist of this pitch has reshaped quite a bit since it was first created and has become much more solid; it's time to continue the discussion in relation to the refactored up-to-date proposal: Reconsider the semantics of type aliases in protocol extensions. Feedback appreciated, here's the pull request once more, in case the review process is of any interest.