Why can animatableData be two types at the same time?

struct SomeStruct
{
    var value: Int = 89
    func printValue() { print(value) }
}

struct BadAnimatable: Animatable
{
    var animatableData = SomeStruct()
}

From the code, animatableData is of SomeStruct type and does NOT conform to VectorArithmetic.

var badAnimatable = BadAnimatable()
badAnimatable.animatableData.scale(by: 2)
badAnimatable.animatableData.printValue()

The above code compiles, runs and prints out 89 without any error?!!
I used Jump to Definition from Xcode for scale(by:) method, Xcode directed me inside of EmptyAnimatableData which has the following signature:

@frozen public struct EmptyAnimatableData : VectorArithmetic { ... }

Using Jump to Definition for printText() method directed me inside of SomeStruct.
I am very confused right now because EmptyAnimatableData and SomeStruct are two different structs they shouldn't merge like protocols. Can anyone explain why this happens?
To help me understand, I tried to understand the usage of EmptyAnimatableData in Animatable for this extension:

extension Animatable where Self.AnimatableData == EmptyAnimatableData {

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

This just brings out the question of chicken and egg, which one comes first? In this case, which one becomes the EmptyAnimatableData first? Is it animatableData or AnimatableData? And when I extend a protocol by doing

protocol P
{
    associatedtype PType: AdditiveArithmetic
    var pVar: Self.PType { get set }
}

extension P where Self.PType == Int
{
    var pVar: Int  // No stored property allowed
}

swift compiler tells me that protocol should not have stored value.
That was my failed attempt at understanding the strange behavior of animatableData. I hope someone can explain what is going on here.

The whole code:

import SwiftUI

struct SomeStruct
{
    var value: Int = 89
    func printValue() { print(value) }
}

struct BadAnimatable: Animatable
{
    var animatableData = SomeStruct()
}

struct ContentView: View
{
    var body: some View
    {
        Text("Animatable Protocol")
        .onAppear
        {
            var badAnimatable = BadAnimatable()
            badAnimatable.animatableData.scale(by: 2)
            badAnimatable.animatableData.printValue()
        }
    }
}

#Preview {
    ContentView()
}

Correct, you can't use a stored property in a protocol extension like that. Neither does SwiftUI: what you're looking at isn't the full code of the extension (it's closed source!), just the module interface.

You can use a computed property like this:

extension P where Self.PType == Int {
    var pVar: Int { return 42 }
}

I expect this will unblock you from playing around to figure out what's going on.

1 Like

Thank you! I boiled the above down to the following:

protocol P
{
    associatedtype Number: Numeric
    var pVar: Self.Number { get }
}

extension P // OPTIONAL but kinda important: `where Self.Number == Int`
{
    var pVar: Int { 100 }
}

struct BadConform: P
{
    var pVar: String = "hello"
}

And it compiles!

I guess pVar got differentiated by the context they come along. So they are actually two different variables, kinda like pVar_Int and pVar_String. Since the default implementation is always going to linger around, using Self.AnimatableData == EmptyAnimatableData removes default implementation in the case when the conforming type successfully conforms to Animatable.
So the default implementation comes first and, at the end, the where clause comes in, does the check and removes implementation if necessary.

1 Like

That’s pretty much what happens, and when the compiler forms symbol names for the linker, we actually encode types in the string, almost like that: swift/docs/ABI/Mangling.rst at main · swiftlang/swift · GitHub

1 Like