Type conversion with generics / protocol composition

I must be missing something fundamental, but why does the following not work?

protocol Clock {}
struct SystemClock: Clock, Equatable {}
struct Timer<C: Clock & Equatable>: Equatable {}
    let clock: C
    init() {
        self.clock = SystemClock()
    }
}

(It will say "cannot assign value of type 'SystemClock' to type 'C'")

Is there any other way I can make my Timer struct be equatable, at least when the clock is equatable?

I was able to get the following to compile fine.

protocol Clock {}

struct SystemClock: Clock, Equatable {}

struct Timer<C: Clock & Equatable>: Equatable {
  let clock: C
}

extension Timer where C == SystemClock {
  init() {
    self.clock = SystemClock()
  }
}

let t = Timer<SystemClock>()

print(t)

The feature you'd want to make what you originally wrote be correct would be a default generic type. As others noted below, even a default wouldn't work in that context, since there's no guarantee C will be SystemClock .

2 Likes

The issue you are having in your code:

protocol Clock {}

struct SystemClock: Clock, Equatable {}

struct Timer<C: Clock & Equatable>: Equatable {
    let clock: C
    init() {
        self.clock = SystemClock()
    }
}

is essentially the same as the issue in this code:

struct S<T: BinaryInteger> {
    let value: T
    init() {
        self.value = UInt8(123) // ERROR: Cannot assign value of type 'UInt8' to type 'T'
    }
}

or even simpler, in this code:

func foo<T: Comparable>() -> T {
    return 123 // ERROR: Cannot convert return expression of type 'Int' to return type 'T'
}

The fundamental thing that you are missing is that the type parameter T (your C) can be any type that conforms to Comparable (your Clock), and you cannot write your generic code as if that type parameter is exactly Int (or SystemClock).

SystemClock conforms to Clock, UInt8 conforms to BinaryInteger, Int conforms to Comparable, but Int is not convertible to any type T conforming to Comparable.

5 Likes

I don't see why a default generic type would help. What @yxckjhasdkjh wrote originally doesn't work because, as the diagnostic says, SystemClock isn't C. This is because the user can choose any type for C that conforms to Clock & Equatable, whereas SystemClock is one specific type that does so. Even if you could assign a default choice for C, it doesn't change the fact that the user can choose a type that isn't SystemClock.

1 Like

Yeah, that makes sense.

However, in an initializer I am explicitly controlling the kind of concrete type that I'm creating. So, in theory, the compiler would know enough to infer that Timer() always returns a Timer<SystemClock> (whereas I might have more specific additional initializers).

@nuclearace's approach works, but I wouldn't have discovered it by myself.

You are not in control of the concrete type, because it is specified by the user. If I write let t: Timer<SomeOtherClock> = .init(), you need to return me a Timer<SomeOtherClock>.

4 Likes

No you are not, think about the following case:

struct MyOtherNonSystemClock: Clock {}
let timer = Timer<MyOtherNonSystemClock>() // Hey, initializer!
2 Likes