KeyPath with generics and subclassing

What is the "fundamental discrepancy"?

I am not sure what you are trying to do here. self is an instance and Self is a type, and as is the type coercion operator, which is distinct from the operators is and as?/as!.

Could you elaborate more on this? The following are equivalent:

  • a is B is true,
  • a as! B doesn’t crash,
  • a as? B doesn’t return nil.

And none of them is checked at compile time (though analyzer does generate warning for impossible cases).

It’s no exception here.

I know, what I mean

is self is Self evaluate to false. So we need as! Self type coercion operator to force cast it.

1 Like

In what scenario does self is Self evaluate to false?

No, as is the type coercion operator, as! is a type casting operator. You would use self as! Self to cast self to Self when self is Self is true, not false.

Okay, I've reworked the example code to the simplest possible to illustrate the breakdown. Copy it and experiment running it.

The code compiles fine, the problem happens at runtime. Method setValue() succeeds while setValueFailing() doesn't because:

  • self is Self == true with Self seen as being Child (you can debug print)
  • but behind the scenes the instance is being handled as Base which is demonstrated by failing KeyPath matching

Which brings me to the point that self as! Self has a clear casting effect and changing the instance type, while self is Self says "all is good here" – but it isn't.

It's a bit like this:
self is Self is like confirming that 🍏is 🍏
self as! Self may work as 🍅—> 🍏 while self is Self next to it ensures us that it's 🍏is 🍏 ... but 🍅!= 🍏

Hence me suspecting it being a logical fallacy. The reason, as understand it, is perhaps more of a naming nature. A better way could be changing the self as! Self construct to let obj = instance(of: self) but Swift grammar bunches two different principles together thus introducing confusion. Or it is a subtle compiler bug.

Anyway, the proof is in the pudding. The code isn't working for if self is Self { ... — what can be done here?

import Foundation

class Base {
}

class Child: Base {
    var name: String = ""
    var number: Int = 0
}

extension Base {

    // Works well thanks to 'self as! Self'
    func setValue<K>(_ val: Any, at keyPath: K) {
        assert(self is Self) // runtime says 'yes'
        let obj = self as! Self // ✅ self is being force cast to `Child`
        assert(obj is Self) // obviously true
        setValue(obj: obj, keyPath: keyPath, value: val)
    }

    // Doesn't work even though 'self is Self' evaluates as true
    func setValueFailing<K>(_ val: Any, at keyPath: K) {
        if self is Self { // ❌ self is seen as `Child` but treated as `Base`
            print ("I think that am looking good to go since I am \(self) and \(Self.self)")
            setValue(obj: self, keyPath: keyPath, value: val)
        }
    }
    
    private func setValue<T: Base, K>(obj: T, keyPath: K, value v: Any?) {
        switch keyPath {
            case let p as ReferenceWritableKeyPath<T, String>:
                obj[keyPath: p] = v as? String ?? String(describing: v)
            case let p as ReferenceWritableKeyPath<T, Int>:
                obj[keyPath: p] = v as? Int ?? 0
            default:
                assertionFailure("Couldn't match KeyPath: \(keyPath) for type: \(obj.self)")
        }
    }
}

let child = Child()

// These calls work
child.setValue("Adam", at: \Child.name)
child.setValue(137, at: \Child.number)

// This one fails because given KeyPath doesn't exist in `Base`
// even though `self` is `Child`
child.setValueFailing("Adam", at: \Child.name)

Thanks for explanation for as and as!.
Why not just use self as Self when self is Self is true?
What's the difference between coercion and casting ?

In this thread topic, the key point is using as! Self trick to solve the problem, as Self compile failed.

If self is Self is always evaluated to true, why as Self failed?

In plain text, self is Self but self can't be as Self self must be as! Self to get really Self, ... looks so weird to me.

PS, may I treat coercion as as try as or safe as; and treat casting as! as force as or unsafe as?

They're similar elsewhere too:

  • self is Self is just returns Bool, it doesn't do anything to self,
  • optional != nil is just returns Bool, it doesn't do anything to optional,

In much the same way self is Self doesn't convert self to Self, a != nil doesn't convert a to non-optional.

let a: Optional = 3

if a != nil {
  // `a` is still `Int?`
}

if let obj = a {
  // `obj` is `Int`
  // `a` is still `Int?`
}

...

if self is Self {
  // `self` is still `Base`
}

if let obj = self as? Self {
  // `obj` is `Self`
  // `self` is still `Base`
}

The static type of self is Base, regardless of what you do. Even what we call casting is just creating new variable referring to the old self, but with the new type.

Though there are sentiments to do what Rust do:

if a != nil {
  // now `a` is `Int`
}

So it indeed is a sound suggestion, to say the least.

1 Like

Technically speaking, the compiler has enough information to do that. The language just doesn't choose to do add that exception.

  • We're inside Base extension, so self is a variable of type Base,
  • A variable of type Base generally can't be converted to Self, you need as? or as!.

Not sure what try as is, but as always succeed (it wouldn't even compile otherwise). You can indeed treat foo as! Bar much the same way as (foo as? Bar)!. If foo can't be converted to Bar at runtime, you'll force-unwrap a nil, crashing the program.

That said, unsafe unwrap is not as dangerous as it sounds. If you can prove that it never fails, it's a pretty handy tool. In this case, you know, from human ingenuity, that foo can always be converted to Bar. and so there's no point in handling failure case.

1 Like

Much clear for now. Because Self is Child at runtime.

The confusion point is as and as! operation. Or Coercion and Casting operator as @xwu mentioned upthread if self is Self is true all the time why as Self failed in this topic.

But after reading your this post, self and Self is Base-Child relationship we should use (base as? Child)! or base as! Child to make force unwrap conversion and get Child type reference, because base as Child failed due to base(self) is NOT Child(Self) for compiler at compile time.

My understanding is that, self is of type Base at compile time, but Self is of type Child at runtime since Self actually is type(of:self). So base to Child operation must need as! operator to help.

But what is the type of Self at compile time? Base or Child?
If Self is Base, self is Self should be true, self as Self should succeed.
If Self is Child, self is Self should be false, self as Self failed and need use self as! Self instead.

let self = self is Self ? self as Self : self as ! Self

At compile-time, Self does not have a concrete type. Self represents “the dynamic runtime-type of self.”

Specifically, Self is an instance of the type Base.Type. At compile-time, there is no way to know which instance will be passed in at runtime. It could be Base, it could be Child, if other subclasses exist it could be one of them, and if Base is open it could even be a subclass from another module that imported this one.

Hence the value of Self might be some type that doesn’t even exist when we compile this module. It could be a subclass written 10 years from now in an app that imports this module.

• • •

That’s why you can’t write “as Self”. The as operator works at compile-time, to let the compiler know which static type something should be treated as.

But Self is a purely dynamic, runtime-only concept.

• • •

Here’s a function that might help your understanding:

func staticType<T>(of value: T) -> Any.Type { T.self }

Observe:

staticType(of: Child())           // Child
staticType(of: Child() as Base)   // Base

Compared to:

type(of: Child())          // Child
type(of: Child() as Base)  // Child

Now you can look at the static and dynamic type of self within your methods on Base.

You will find that in an extension of Base, the static type of self is always Base, whereas the dynamic type of self is always the same as Self.

• • •

When you call a generic function by passing in a value, the compiler sets the generic parameter to match the static type of the value. In your non-working code, the static type of self is Base, so the generic parameter T becomes Base.

But the keypaths operate on Child, so they are not convertible to operating on T which is Base.

Conversely, in your working code, the dynamic run-time cast creates a new value with a static type of Self. The compiler does not know which type that will end up being at run-time, but it still treats it as a distinct type.

So then, passing the new value of static type Self into the generic function makes T equal to Self, and then at run-time when it turns out that Self is Child then the keypaths are indeed convertible to T. Because T is Self which dynamically is Child.

3 Likes

To add, there's a small reference here: Types: Self Type.

Thanks for your reply~

Two more questions.

The as operator works at compile-time,

  1. how about as? and as!? work at runtime?

  2. In this topic case, is self is Self always evaluated to true? Or when to false?

Yes.

Self is the dynamic type of self.

self is Self” always evaluates to true in every case. (Unless you do something truly crazy like redefine Self or self. Don’t do that.)

1 Like

It is not at all like that. The is operator in Swift asks only to compare the dynamic type of the instance on the left-hand side with the type given on the right-hand side.

In your example:

setValue(obj: self /* ... */)
// `self` has static type `Base` and dynamic type `Child`
let obj = self as! Self
setValue(obj: obj /* ... */)
// `obj` has static type `Child` and dynamic type `Child`

There is no bug and no two principles. I think you are still not grasping the distinction between the static and dynamic type.

By far the best answer :star:, thank you for actually taking time to work through the code and offering a practical approach!

This function of yours staticType(of:) truly works and offers the sorely missing bit of logic that would make the code understandable just by reading it. The code is tested and it works:

func staticType<T>(of value: T) -> Any.Type { T.self }
    
func setValue<K>(_ val: Any, at keyPath: K) {
    print("Static type of self:", staticType(of: self))      // Base
    print("Static type of Self:", staticType(of: Self.self)) // Child
    print("Dynamic type of self:", type(of: self))           // Child
    print("Dynamic type of Self:", type(of: Self.self))      // Child

    if self is Self && staticType(of: self) != staticType(of: Self.self) {
        setValue(obj: self as! Self, keyPath: keyPath, value: val)
    } else {
        setValue(obj: self, keyPath: keyPath, value: val)
    }
}

And this also illustrates the language problem I was referring to. It's not about not understanding static vs dynamic as some other posters suggested. I do understand that stuff. What I am pointing at is the fact that (in this case) Swift doesn't have an explicit way of making the code understandable by reading it.

Truly the construct of self as! Self is a bandaid that masks the reality by applying esoteric-looking mumbo-jumbo.

All it would take to fix that situation is having an instance(of:) method just like in Objective-C, so all that code and unnecessary intellectual struggle will turn into simple, expressive, understandable and explicit:

func setValue<K>(_ val: Any, at keyPath: K) {
    setValue(instance(of: self), keyPath: keyPath, value: val)
}

Simpler things work better because they require less explanation. As opposed to this thread essentially dealing with a non-issue due the obscure nature of the syntax. Hence my point about logical fallacy behind the self as! Self construct. It doesn't smell right, intuitively.

1 Like

self and Self will never have the same static type, the latter being a metatype. This line of code makes clear that you really are not correctly understanding what this code is doing, and I think this is the root of why you are finding this confusing. I urge you to review what @Nevin has explained, and to consult other sources, so that you find clarity.

There is no mumbo-jumbo here, and it is not an issue of syntax. If you are to understand how your code behaves in Swift with generics, dynamically dispatched methods, and runtime casts, you will need to master this distinction between static and dynamic types.

1 Like