Generics with specialization

Hi,

Overview

  • I have a generic struct called Race, which takes in S as the generic parameter
  • Depending on the type of S I want to be able to have a different implementation of isAvailable
  • I have made my attempt, by switching over the type of status

Question

  • Just wondering if there is a better way to implement isAvailable using generic specialization or other means or is my code the best possible way?

Code

protocol Raceable {
    associatedtype S: Status
    var status: S { get }
    var isAvailable: Bool { get }
}

struct Race<S: Status>: Raceable {
    let status: S
    
    // Is there a better way to implement this?
    var isAvailable: Bool {
        switch status {
        case is StatusA:
            false
        case is StatusB:
            true
        default:
            false
        }
    }
}

protocol Status: Codable, Equatable {}

enum StatusA: Status {
    case yetToStart
    case inProgress
    case completed
}

enum StatusB: Status {
    case idle
    case inProgress
    case aborted
    case completed
}

There's more flexibility with methods. Properties require constraints on the extensions.

struct Race<S: Status>: Raceable {
  let status: S
  var isAvailable: Bool { false }
}

extension Race<StatusA> {
  var isAvailable: Bool { false }
}

extension Race<StatusB> {
  var isAvailable: Bool { true }
}

Thanks @Danny but I had tried it but it is not what I intended.

The problem with that approach is if you use via protocol you will get the default implementation instead of the specialization (it may not be a bug but it is not what I intended).

Example (from your code)

let p1: any Raceable = Race(status: StatusB.idle)
print("p1.isAvailable: \(p1.isAvailable)")

Output

p1.isAvailable: false

This compiles, but I ain't sure it does what you want.

...
extension Race where S == StatusA {
    var isAvailable: Bool {
        false
    }
}

extension Race where S == StatusB {
    var isAvailable: Bool {
        true
    }
}

Thanks @ibex10 , the outcome is still the same when accessed via a protocol (default implementation would be picked instead of the specialized one).

let p1: any Raceable = Race(status: StatusB.idle)
print("p1.isAvailable: \(p1.isAvailable)")

Ideally I would want p1.isAvailable to be true but it takes the default implementation.

Reason for this is being explained in Calling a protocol with associatedType's method, doesn't always pick the same specialized generic implementation - #2 by xwu

However I can't seem to find a better implementation than to the one I posted originally to reliably use protocol and still match my expectation.

May be it is a design issue on my part, any alternate design are welcome as well.

1 Like

You could add an isAvailable requirement to Raceable, which would then allow you to dynamically dispatch to the implementation of the generic type at runtime. Here I think you’d could make it static since it only depends on the type and not the value.

Thanks @j-f1, that is what I thought too, but may be I am not understanding it fully.

In my example isAvailable is already part of the requirement and yet default implementation is taken. Could you try with the sample code I am getting the default implementation of false when accessed through a protocol.

I couldn't use static because in the real world problem I still would need to examine the instance's status value to determine value isAvailable, this example was a simplified version of the real world problem.

Your solution to switch over the type is one way of doing this.

You could alternatively make a property named something like isRaceAvailable a (if you want, static) requirement of Status and call that from a (if you want, default) implementation of Race.isAvailable.

2 Likes

As soon as you erase the Status, you lose your specializations. I recommend figuring out how to erase the need to use any Raceable. You surely won't be able to get it as simple as this, but you may have some option of otherwise passing the Status through to functions and types.

protocol Raceable<S> {
  associatedtype S: Status
  var status: S { get }
}

struct Race<S: Status>: Raceable {
  let status: S
}

extension Raceable<StatusA> {
  var isAvailable: Bool { false }
}

extension Raceable<StatusB> {
  var isAvailable: Bool { true }
}
let p1: some Raceable<StatusB> = Race(status: StatusB.completed)
#expect(p1.isAvailable)

Thanks a lot @xwu, I think adding it as a static requirement to Status is a nice alternative.

One more question I remember another post where you explained what was happening, I couldn't seem to get the link.

Just want to confirm my understanding:

  • When accessing through a protocol, the protocol's default implementation is used because the implementation to use is determined at compile time (static dispatch) .. and not dynamic dispatch?

No, protocol requirements are dynamically dispatched. The protocol’s default implementation is used because it is the only implementation that witnesses the requirement. A type only conforms to a protocol in exactly one way, and you have not given Race a non-default implementation.

1 Like

Thanks a lot @xwu, I am slowing understanding ... please bear with me

protocol requirements are dynamically dispatched, however A type only conforms to a protocol in exactly one way

I suppose only because of protocol using dynamic dispatch polymorphism works we get the inherited class's implementation invoked (not referring to my example, but inheritance in general)

The part I completely missed is A type only conforms to a protocol in exactly one way. So to the compiler that is the rule, so no 2 options.

Just curious wouldn't this detail trip a lot of developers or are there scenarios where such behavior is actually useful (specializations used when using concrete types and protocol implementation used when using protocols.)