Protocol with clear Generic requirements should not be a Generic

As a programmed I want to use clear POP without generic associated type, cause it makes it unavailable to use on runtime.

Example:

protocol Acting: class, Identifiable {
    var id: String { get }
   var actionPoints: Int { get }
   var currentActionPoints: Int { get set }
   var isActingNow: Bool { set get }
}

class Actor: Acting {
....
}

class Actor2: Acting {
...
}

// I want to use Acting on runtime, cause it is a protocol with specified field type, not a generic now
var actors: [Acting] = [Actor(), Actor2(), ...]

Now the Acting protocol is marked a generic, cause it 'has Self requirements ...', but it is clearly stated, that id field from Identifieable generic having concrete type String.
And actually compiler mixes a paradigms here.

1 Like

:thinking: You still won’t be able to invoke any functions with Identifiable arguments using an Acting element. Might as well keep them (Acting and Identifiable) separated.

Why so?
Acting is conforms for generic - check
Acting is a pure protocol it not requires any of associated types, so should work on runtime - check


import SwiftUI

protocol Acting: class {
    var id: String { get }
    var actionPoints: Int { get }
    var currentActionPoints: Int { get set }
    var isActingNow: Bool { set get }
}

extension Acting: Identifiable {

}

class Actor: Acting {
    private(set) var id: String = "1"
    private(set) var actionPoints: Int = 1
    var currentActionPoints: Int = 1
    var isActingNow: Bool = false
}

class Actor2: Acting {
    private(set) var id: String = "2"
    private(set) var actionPoints: Int = 2
    var currentActionPoints: Int = 2
    var isActingNow: Bool = true
}

var actors: [Acting] = [Actor(), Actor2()]

This is also failed with

error: extension of protocol 'Acting' cannot have an inheritance clause
extension Acting: Identifiable {

That's because you can't retroactively conform a protocol to another protocol. I believe what @Lantua was suggesting was this:

import SwiftUI

protocol Acting: class {
    var id: String { get }
    var actionPoints: Int { get }
    var currentActionPoints: Int { get set }
    var isActingNow: Bool { set get }
}

class Actor: Acting, Identifiable {
    private(set) var id: String = "1"
    private(set) var actionPoints: Int = 1
    var currentActionPoints: Int = 1
    var isActingNow: Bool = false
}

class Actor2: Acting, Identifiable {
    private(set) var id: String = "2"
    private(set) var actionPoints: Int = 2
    var currentActionPoints: Int = 2
    var isActingNow: Bool = true
}

var actors: [Acting] = [Actor(), Actor2()]

Ideally, defining Acting as

protocol Acting: class, Identifiable where ID == String {
    var actionPoints: Int { get }
    var currentActionPoints: Int { get set }
    var isActingNow: Bool { set get }
}

would allow your original code to work, but this isn't supported yet. You can declare the protocol that way, but it still won't let you use existentials.

PS. class constraints have been deprecated in favour of AnyObject constraints. They really should add a warning for that.

You wouldn't be able to make use of Identifiable-accepting functions with an Acting existential, but they would still be usable from a generic context:

func identify<T: Identifiable>(_ thing: T) { ... }

func act<T: Acting>(_ actor: T) {
    identify(actor) // only works if `Acting: Identifiable`
    // ...
}

So separating Acting and Identifiable doesn't preserve all the functionality someone might want, unless they do the manual work of replacing all Acting constraints with Acting, Identifiable.

I do wonder if, compiler-internally, the Acting protocol really represents what it seems to, though. Are Acting.id and Identifiable.id linked in any meaningful way, beyond the fact that they happen to share a name? (I.e., does the compiler guarantee that they will be witnessed by the same implementation in a conforming type?) With @_implements, that might not be the case, if it's possible to have

@_implements(Acting.id)
var id1: String

@_implements(Identifiable.id)
var id2: Int

then Acting can't "refine away" the associated type requirement.

2 Likes

Fair point, but then you’d only be able to use it as a PAT when you want to use have it be Identifiable. So now we ends up with two separated scenario, Acting as a PAT, and Acting as a non-PAT, which is already more-or-less two separated protocols; non-PAT Acting, and PAT IdentifiableActing.

Problem is that we don’t have runtime for PAT (ok, we kinda do when we specialize a generic functions). There’s a question about scenario like this:

let a: Acting = ...

func foo<I: Identifiable>(_: I) { ... }

foo(a) // Should I be able to do this??

If the answer is no, then it’s not much different from having NonIdentifiableActing and IdentifiableActing mentioned above, and that’d be the concise way to express it.

If the answer is yes... Well, that’d mean Acting conforms to (not refine) Identifiable, which would be a big proposal on its own (and a few threads already pops up).

From the my logic point as a programmer, yes. But in current language version there is no a way to compile that code.

My general thesis:
If we apply all the generics attributes in protocol it should stop being generic.

And maybe it's a nice time to separate a generics and pure protocols.

generic Identifiable {
    associatedtype ID : Hashable // associatedtype can be used only in generics
    
    var id: Self.ID { get }
}

protocol Acting: Identifiable { // no associatedtype no problems

    // add the direct point to type with a @generic modifier,
    // or just let compiler decide himself
    @generic var id: String { get }
    
    var actionPoints: Int { get }
}

class Actor: Acting {
    var actionPoints = 5
}

class PowerfulActor: Acting {
    var actionPoints = 100
}

var actors: [Acting] = [Actor(), PowerfulActor()]

The problem is less about associated type, and more about protocol conforming to another protocol (or even itself).

protocol Fooable { }
extension Double: Fooable {}

func foo<T: Fooable>(_ t: T) { }

foo(3.0 as Double) // Ok
foo(3.0 as Fooable) // Error: Protocol type 'Fooable' can't conform to 'Fooable' because only concrete types can conform to protocols.

PS
There's a distinction between protocols with associated types, and (what people claim to be) generic protocols, but I get what you means.

2 Likes

Separating into two protocols still doesn't preserve all functionality. You wouldn't be able to pass an object from a generic IdentifiableActing context to an existential Acting context without a conditional cast, e.g.,

func foo(_ actor: Acting) { ... }
func bar<T: IdentifiableActing>(_ actor: T) {
    identify(actor)
    foo(actor) // error!
}

In general, I don't think it's an unreasonable idea that if the associatedtype requirements have been refined away, you can use the refined protocol as an existential while still taking advantage of protocol refinement. It's similar (though not identical) to the way you can subclass a specialized generic class to get rid of the generic parameter:

class Base<T> {}
class Sub: Base<Int> {}

func foo<T>(_ base: Base<T>) {}
func bar(_ sub: Sub) {
    foo(sub)
}
2 Likes

Agree. Conceptually, it's sound. Another analogy (again imperfect) would be how the compiler can allow certain uses of Self when a class is made final.

7 Likes

I'm thinking about something like this:

protocol Acting { }
protocol IdentifiableActing: Acting, Identifiable {}

func foo(_ actor: Acting) { }
func bar<T: IdentifiableActing>(_ actor: T) {
    foo(actor) // ok
}
1 Like

Gotcha, that makes more sense and I think covers most use cases, with the caveat that it requires more manual effort to use properly. It may become a maintainence challenge to ensure that you're always using the right version of (Identifiable)Acting, and if you're writing a library, it may be undesirable to expose two protocols that are quite similar. It also allows potentially undesirable conformances e.g., maybe I don't want to allow something to conform to Acting without also conforming to Identifiable.

Actually.
And workaround with base-classes inheritance its just an old crap c++ way.