How to select different associated type based on type constraints?

extension CrudController: CrudControllerProtocol {
    public typealias ModelType = ModelT
}

extension CrudController where ModelT: Publicable {
    public typealias ReturnModelType = ModelT.PublicModel
}

extension CrudController {
    public typealias ReturnModelType = ModelT // Error: redeclaration of the above typealias
}

In the above snippet, the third typealias throws an error claiming that it's a redeclaration.

What I'm looking to do is

  1. Have CrudController conform to CrudControllerProtocol
  2. If ModelT (a generic type on CrudController ) conforms to Publicable
    Set ReturnModelType to ModelT.PublicModel,
    Else set ReturnModelType to ModelT

ModelType and ReturnModelType are both associatedtypes of CrudControllerProtocol .

I'm doing something similar elsewhere where I have different definitions for a function based on the type constraints and it's working fine, so I figure if it works for functions it ought to work for typealiases.

extension CrudSiblingsController: RouteCollection {}

public extension CrudSiblingsController where ThroughType.Right == ParentType,
ThroughType.Left == ChildType {
    public func boot(router: Router) throws {
        ...
    }
}

public extension CrudSiblingsController where ThroughType.Left == ParentType,
ThroughType.Right == ChildType {
    public func boot(router: Router) throws {
        ...
    }
}

public extension CrudSiblingsController {
    public func boot(router: Router) throws {
        ...
    }
}

The key difference of a function (func boot(router: Router)) is that only the implementation changes, that is, the behavior in a particular situation, not the signature, which makes it a bit harder to catch an ambiguity. Anyway, this is a buggy area right now, for instance, this shouldn't work:

class Foo<T> {
    func foo(_ arg: R) {}
}

extension Foo where T == Int {
    typealias R = Int
}

What you are trying to do shouldn't be a problem, but I'm not sure whether it's simply another bug or a limitation.

A known problem is how type aliases behave in protocol extensions. An analogous scenario with protocol extensions won't work either, but there's more to it. Currently, type aliases in protocol extensions do not act as default values for associated types. Doug stated that it is an accident of evolution, and there's currently a rough proposal up to change that.

protocol P {
    associatedtype Assoc
}
extension P {
    typealias Assoc = String
}

class Foo: P {
    typealias Assoc = Int // Type 'Foo' does not conform to protocol 'P'
}

If the issue you've encountered is also an evolution accident, it probably needs a separate proposal, so that the linked proposal can focus solely on changing the semantics of type aliases in protocol extensions. I can bring it up soon enough.

@Slava_Pestov, what do you think? Is this a bug or something that needs a proposal?

2 Likes

Hey @Slava_Pestov. I didn't want to bump this earlier because I know you were in the middle of a move, but I'm definitely still interested in your thoughts if you have any.

Ended up submitting this as a bug

https://bugs.swift.org/browse/SR-9196

I'm almost sure now this is a limitation, considering the type alias problem in protocol extensions and that type alias declarations in constrained extensions ignore the constraints (a known bug, or rather simply a tricky situation nobody has resolved):

class Foo<T> {
  var foo: Alias?
}
extension Foo where T: Sequence {
  typealias Alias = Int
}

let x: Int = Foo<Int>().foo

It seems like an intuitive feature, but there are quite some situations that are hard to reason about. Imagine you could similarly change Element in a constrained extension on Array. That would break type safety unless the extension is magically ignored by the compiler.

That's a really good example where things get muddy and confusing. I would definitely be thrown off if the construct in question was allowed and someone did something like

extension Array where Element == Int {
    typealias Element = Float
}

in a dependency I was trying to use. But that leaves me with a few questions as someone with no experience with language design.

  1. Does that actually break type safety? It seems to me like if Element is being used as return or parameter type within Array, for example in the case of subscript access, then Element is still deterministically definable, right?
[Int]()[0] // returns Float
[String]()[0] // returns String
  1. Should the extension Array where snippet above even be possible? Shouldn't the typealias spelled Element and the generic placeholder spelled Element clash? Similarly, the following snippet doesn't feel like it should compile, although it does.
struct FakeArray<Element> {
    typealias Element = String
    var element: Element? // Element is String now
}

Here we have a redefinition of Element within the initial definition of a struct that feels like it ought to clash, perhaps more so than the extension snippet above. Although something vaguely similar is possible within functions, so maybe that's by design.

func foo(a: Int) { // value rather than type being passed in
    let a = 10 // alias for that value immediately overwritten
    print(a) // and used for the rest of the function body in place of the passed in value
}

foo(a: 20) // prints 10
  1. Is the Struct/Generic placeholder example comparable to the initial Protocol/associatedtype example? There are some cases in Swift where associatedtypes are treated like generic placeholders, for example with where clause constraints, but there are other times where they are treated as different, for example when using a generic type constraint vs a protocol constraint with Self or an associatedtype
protocol FooProtocol {
    associatedtype T
}

struct Foo<T> {}

extension FooProtocol where T == Int {} // fine
extension Foo where T == Int {} // fine

let a: Foo<Int> // fine
let b: FooProtocol // error

Which case would this be?

Here's a functioning, but slightly less intuitive version of the snippet from the bug report. Shoutout to @tanner0101 for the help.

protocol Publicable {
    associatedtype PublicModel

    func publicized() -> PublicModel
}

protocol Returnable {
    associatedtype Return = Self
}

extension Returnable where Self: Publicable {
    typealias Return = Self.PublicModel
}

protocol CRUDController {
    associatedtype Model: Returnable

    func returnTheThing()
}

extension CRUDController {
    func returnTheThing() {
        print("not matching and not publicable")
    }
}


extension CRUDController where Model: Publicable, Model.Return == Model.PublicModel {
    func returnTheThing() {
        print("publicable")
    }
}

extension CRUDController where Model.Return == Model {
    func returnTheThing() {
        print("matching types and not publicable")
    }
}

extension String: Publicable, Returnable {
    struct PublicString {
        let inner: String

        init(str: String) {
            self.inner = "Public: \(str)"
        }
    }

    func publicized() -> PublicString {
        return PublicString(str: self)
    }
}

extension Int: Returnable {}

struct Controller<T: Returnable>: CRUDController {
    typealias Model = T
}

let controller = Controller<String>()
let intController = Controller<Int>()

controller.returnTheThing() // prints "pubclicable"
intController.returnTheThing() // prints "matching types and not publicable"
1 Like

There's something important I've overlooked in your example. One of your extensions is unconstrained, meaning a type alias is declared as part of the enclosing type's primary declaration. Naturally, a second declaration of a type alias with an equal identifier is then considered a redeclaration. Note this isn't an overload, as it would be with methods.

I was initially referring to a situation when all relevant type alias declarations occur in constrained extensions


To be honest, the example on Array wasn't very accurate. Element is a generic parameter, not a type alias, so we're dealing with shadowing (similar to your example in question 2) rather than a redeclaration. Be it a type alias, such an extension showcases exactly why it is considered a redeclaration.

Your Array extension doesn't make much sense, since the constraint itself implies that Element is predefined. Consider instead redeclaring SubSequence for a constrained Element. Allowing this would break type safety because any API expecting Slice would have to work with a different type. On a non-retroactive example any API with such a type collision will currently trigger type is ambiguous for type lookup in this context together with the redeclaration error. This mostly answers the second question.

It's long time to requalify this shadowing rule into a redeclaration error. I believe there is no specific use case that could advocate the opposite. It's mentioned in this pitch as part of a broader idea.

Associated types are just another way of spelling generic parameters on protocols. Everything you can do in concrete type extensions you can also do in protocol extensions, but protocols also allow to specify default implementations for requirements and make them depend on Self or associated types through constraints. Type aliases aren't the case, but they could be. However, for both concrete types and protocols, we still have to decide on how to address various ambiguous situations. With methods, this is pretty straightforward – we raise an error at call site. With type aliases, the error would have to be triggered at the implementation site as well.

// Module A
protocol P {
    associatedtype A
    associatedtype B
}

extension P where B: Sequence {
    typealias A = Int
}

protocol P1 {}

class Foo: P, P1 {
    typealias B = String // A inferred as Int
    
    func foo() -> A { return 0 } 
}

// Module B
extension P where Self: P1 {
    typealias A = String
}

// How should this ambiguity be diagnosed if the source of A isn't visible to the user?

We should strive to avoid code that retroactively breaks other code. One course of action is to be silent until the relevant type alias is actually used to then say type is ambiguous for type lookup in this context. The same technique is used when you try to retroactively shadow a generic parameter of an external type (relates to your example in question 2 and should really be a redeclaration error additionally).

extension Array {
  typealias Element = Bool

  func foo(_ arg: Element) {} // 'Element' is ambiguous for type lookup in this context
}