Checking constraints on associated types within a protocol

I'm observing two different behaviors when it comes to the compiler's diagnostics when implementing a type that doesn't correctly adopt a protocol. In both cases, the protocol has an associatedtype with a constraint of another protocol.

What's the difference between these two situations? Why doesn't the first situation produce an error?

Case 1: Incorrectly adopting SwiftUI.Animatable because animatableData does not conform to VectorArithmetic. The following code builds without any warnings or errors:

struct MyAnimatable: Animatable {
    var animatableData: String {
        get { "TEST" }
        set { }
    }
}
Relevant interfaces from SwiftUI
public protocol VectorArithmetic : AdditiveArithmetic {

    /// Multiplies each component of this value by the given value.
    mutating func scale(by rhs: Double)

    /// Returns the dot-product of this vector arithmetic instance with itself.
    var magnitudeSquared: Double { get }
}

public protocol Animatable {

    /// The type defining the data to animate.
    associatedtype AnimatableData : VectorArithmetic

    /// The data to animate.
    var animatableData: Self.AnimatableData { get set }
}

Case 2: Making my one "fake" set of types to mimic the same constraints. There's an error diagnostic produced on the MyAnimatable symbol (shown below):

// NOTE: Simplifying this not to inherit from AdditiveArithmetic intentionally
protocol VectorArithmetic {
    mutating func scale(by rhs: Double)
    var magnitudeSquared: Double { get }
}

protocol Animatable {
    associatedtype AnimatableData: VectorArithmetic
    var animatableData: AnimatableData { get set }
}

struct MyAnimatable: Animatable {
    var animatableData: String {
        get { "TEST" }
        set { }
    }
}
type 'MyAnimatable' does not conform to protocol 'Animatable' (sourcekitd)
SwiftExperiments.swift(11, 20): unable to infer associated type 'AnimatableData' for protocol 'Animatable'
SwiftExperiments.swift(16, 9): candidate would match and infer 'AnimatableData' = 'String' if 'String' conformed to 'VectorArithmetic'

For context, I ran into this as I was implementing a SwiftUI animatable view modifier and chose a value of type SwiftUI.Angle for animatableData, which did not work because it doesn't conform to VectorArithmetic, but I got no errors.

Thanks

Possibly they’ve added a default implementation in an extension. Add this to your case 2 and the errors go away

extension Double: VectorArithmetic {}

extension Animatable {
    var animatableData: Double {
        get { 1.0 }
        set { }
    }
}

Thanks for the suggestion to look into extensions of Animatable. I found a couple (shared below) but none that provide a default implementation for the animatableData property requirement. I'd like to assume that when such an extension is not in the SwiftUI module interface, then it's not the reason. But I'm not sure of that. I guess an extension could be defined in any other source.

Is there any way to definitively check? Is there some way to inspect or query the MyAnimatable and Animatable types for this information?

extension Animatable where Self : VectorArithmetic {

    /// The data to animate.
    public var animatableData: Self
}

extension Animatable where Self.AnimatableData == EmptyAnimatableData {

    /// The data to animate.
    public var animatableData: EmptyAnimatableData
}
1 Like

I think the second extension is adding the default implementation, or at least is part of it, because this code prints EmptyAnimatableData

struct S: Animatable {}

func test() {
    let s = S()
    let ad = s.animatableData
    print(type(of: ad)) //EmptyAnimatableData
}

I’m not an expert and can’t say exactly how the default implementation is implemented except to say there is definitely a default implementation with type EmptyAnimatableData.

I tried out a slight variation that more closely mimics Case 1 above.

struct MyAnimatable: Animatable {
    var animatableData: String {
        get { "TEST" }
        set { }
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
        .onAppear {
            let ma = MyAnimatable()
            let ad = ma.animatableData
            print(type(of: ad))
        }
    }
}

The output was String when running in the iOS Simulator.

This leads me to believe that despite a default implementation of the var animatableData protocol requirement is provided, the implementing struct MyAnimatable does not use that when it provides its own implementation. This matches my understanding of how protocol extensions with default implementations are supposed to work.

Therefore, I'd still expect a compiler error because String does not conform to VectorArithmetic. Any other ideas?

Given that this may have more to do with SwiftUI specifics than a language concern (not actually sure yet), I've posted a question on the Apple Developer Forums too.

1 Like

There is an error in your reasoning.

As you say, String does not conform to VectorArithmetic. Therefore, your property var animatableData: String does not fulfill the protocol requirement.

You are correct that default implementations are overridden when you provide your own implementation of the requirement: but here you did not provide an implementation that fulfills the requirement, so the default implementation is not overridden.

In a context where the the value ma is of concrete type MyAnimatable, your property shadows but does not override the required property, which is very much still there and fulfilled by the default implementation. You can prove this to yourself by passing your value to a generic function:

let ma = MyAnimatable()
print(type(of: ma.animatableData)) // This will print `String`

func f<T: Animatable>(_ t: T) {
  print(type(of: t.animatableData))
}
f(ma) // This will print `EmptyAnimatableData`
4 Likes

Wow, thanks @xwu, your explanation and example are quite clear.

The fact that a type can incorrectly implement a protocol requirement, but the error can be hidden in this shadows but does not override the default implementation way feels like an unfortunate design flaw. I can't think of a reason why this behavior would be desirable. In class inheritance, the compiler enforces using the override keyword to make the outcome clear. I'd prefer a similar syntactical change be required to gain the shadowing behavior and the default should be a compiler error.

What's the best way to voice this kind of feedback for consideration?

You could create a new issue here

1 Like