Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot assign class instance to its protocol type?

Please see self containing example below. Compiler reports an error on the last line (marked by COMPILE ERROR) where I am assigning an instance of SimpleTrain to a protocol type that it (in my best judgement) conforms to. How can I make it compile? What am I doing wrong? Or is this compiler issue?

protocol Train {
    typealias CarriageType

    func addCarriage(carriage: CarriageType)
    func shortTrain<ShortType: Train where ShortType.CarriageType == CarriageType>() -> ShortType
}

class SimpleTrain<T> : Train {
    typealias CarriageType = T
    private var carriages: [T] = [T]()

    func addCarriage(carriage: T) {
       carriages.append(carriage)
    }

    func shortTrain<ShortType: Train where ShortType.CarriageType == CarriageType>() -> ShortType {
        let short = SimpleTrain<T>()
        short.addCarriage(carriages[0])
        return short //COMPILE ERROR: SimpleTrain<T> is not convertible to 'ShortType'
    }
}

EDIT: Even when I explicitly downcast the shortTrain's return type above (so that last line of code snippet above reads return short as ShortType) as suggested by Antonio there is still compilation error when calling function shortTrain:

let s = SimpleTrain<String>()
s.addCarriage("Carriage 1")
s.addCarriage("Carriage 2")

let a = s.shortTrain() //ERROR: Cannot convert the expression's type '()' to type 'Train'
let b = s.shortTrain<SimpleTrain<String>>() //ERROR: cannot explicitly specialize a generic function
like image 990
Rasto Avatar asked Oct 08 '14 22:10

Rasto


2 Answers

An explicit downcast of either the variable:

let short = SimpleTrain<T>() as ShortType

or the return value:

return short as ShortType

solves the problem.

Update

When I was answering to the question, I wondered myself how the Train protocol can be used as a return type, being it typealiased.

Look at this code:

protocol ProtocolWithNoAlias {        
}

protocol ProtocolWithAlias {
    typealias AliasType
}

var x: ProtocolWithNoAlias?
var y: ProtocolWithAlias?

The last line is reported as a compilation error:

Protocol 'ProtocolWithAlias' can only be used as a generic constraint because it has Self os associated type requirements

which means you cannot use ProtocolWithAlias as a concrete type, which in turn means you cannot declare variables having ProtocolWithAlias type, and consequently it doesn't make sense to define function returning it.

I cannot find any mention about that in the official documentation, but I am sure I read somewhere, I just don't remember where.

Conclusion: I interpret the error you're having troubles with:

SimpleTrain<T> is not convertible to 'ShortType'

as a direct consequence of the protocol being typealiased.

Note that this is my personal opinion, since I am unable at this moment to prove it besides that code snippet testing protocol with and without aliases.

like image 80
Antonio Avatar answered Nov 10 '22 01:11

Antonio


First, you want to read the canonical thread on this from devforums. You particularly want to skip around to read jckarter's comments.

Now to the edited question:

let a = s.shortTrain() //ERROR: Cannot convert the expression's type '()' to type 'Train'

This is because you haven't given the compiler enough information to determine the type of a. Think through what it sees:

func shortTrain<ShortType: Train where ShortType.CarriageType == CarriageType>() -> ShortType {
let a = s.shortTrain()

The compiler needs to figure out the type of a at compile time, and it can't handle abstract types. It needs a fully specified type for ShortType (everything nailed down; all the generics specified, all the type aliases resolved). It looks around, and it sees some constraints on ShortType, but it doesn't see anything that actually gives a type. All it has is a, which isn't giving it any hints.

So unfortunately, that leaves you having to tell it explicitly what you want to happen.

let a: SimpleTrain<String> = s.shortTrain()

That's probably the opposite of what you were going for, but it's about all you can do in Swift right now. The Swift team has indicated (many times) that they are well aware of these issues with associated types (and several other related weaknesses in the type system). They're specifically aware of the Scala type system which can handle these things and has a lot in common with the current Swift type system (though in my experience, getting complex path-dependent associated types to work in Scala can also lead to hair tearing).

That said, it's not exactly obvious from your example what you planned to do with this functionality. Would some Trains return a different type as their shortTrain()?

I find that these problems often blow up in the general case, but tend to be quite solvable in the specific cases for the app in front of you. It's hard to build really arbitrary types in Swift in code that could solve every problem, but it often works out when you focus on the types you'll really need. For example, if shortTrain() returned Self, this obviously gets simpler. If the caller knows the desired resulting type, then an init(shorten:) can probably handle it. A protocol method like shortCarriages() -> [CarriageType] may provide a good bridge. Stay flexible on your design, and one of them will almost certainly work out.

like image 30
Rob Napier Avatar answered Nov 10 '22 01:11

Rob Napier