Reconsider the semantics of type aliases in protocol extensions

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.

I still can. What do you need to change? I guess you only need to reach the 'regular' trust level for that.

1 Like

That moment when you can't edit the title of your own topic but somebody else who isn't an admin can :sweat_smile:

"Reconsider the semantics of type aliases in protocol extensions", thank you.

P.S. I know you can start editing titles in general once you have the "regular" trust level, but I wasn't expecting the same rule to apply to your own topics.

Well you might remember this thread where I asked why I can do that.

You're welcome.