In Swift, how to cast to protocol with associated type?
Asked Answered
D

4

29

In the following code, I want to test if x is a SpecialController. If it is, I want to get the currentValue as a SpecialValue. How do you do this? If not with a cast, then some other technique.

The last line there won't compile. There error is: Protocol "SpecialController" can only be used as a generic constraint because it has Self or associated type requirements.

protocol SpecialController {
    associatedtype SpecialValueType : SpecialValue
    var currentValue: SpecialValueType? { get }
}
...
var x: AnyObject = ...
if let sc = x as? SpecialController {  // does not compile
Durante answered 2/11, 2016 at 19:12 Comment(2)
swift 5.7: if let sc = x as? any SpecialControllerHellish
if let sc = x as? ObjectName & ProtocolNameHalicarnassus
D
29

Unfortunately, Swift doesn't currently support the use of protocols with associated types as actual types. This however is technically possible for the compiler to do; and it may well be implemented in a future version of the language.

A simple solution in your case is to define a 'shadow protocol' that SpecialController derives from, and allows you to access currentValue through a protocol requirement that type erases it:

// This assumes SpecialValue doesn't have associated types – if it does, you can
// repeat the same logic by adding TypeErasedSpecialValue, and then using that.
protocol SpecialValue {
  // ...
}

protocol TypeErasedSpecialController {
  var typeErasedCurrentValue: SpecialValue? { get }
}

protocol SpecialController : TypeErasedSpecialController {
  associatedtype SpecialValueType : SpecialValue
  var currentValue: SpecialValueType? { get }
}

extension SpecialController {
  var typeErasedCurrentValue: SpecialValue? { return currentValue }
}

extension String : SpecialValue {}

struct S : SpecialController {
  var currentValue: String?
}

var x: Any = S(currentValue: "Hello World!")
if let sc = x as? TypeErasedSpecialController {
  print(sc.typeErasedCurrentValue as Any) // Optional("Hello World!")
}
Delacruz answered 10/11, 2017 at 21:6 Comment(0)
S
1

[Edited to fix: : SpecialValue, not = SpecialValue]

This is not possible. SpecialValueController is an "incomplete type" conceptually so the compiler cannot know. SpecialValueType, although it is constrained by SpecialValue, it is not known until it is determined by any adopting class. So it is a really placeholder with inadequate information. as?-ness cannot be checked.

You could have a base class that adopts SpecialController with a concrete type for SpecialValueController, and have multiple child classes that inherit from the adopting class, if you're still seeking a degree of polymorphism.

Somnolent answered 2/11, 2016 at 19:18 Comment(2)
The fact that currentValue is a SpecialValue is not something that can be changed by any adopting class. You are guaranteed that the currentValue is a SpecialValue or a subtype of it.Durante
@RobN If you don't take my word for it, see this very similar issue where a 320k rep guy explains the same: #39629858Somnolent
L
1

It is now possible to do so with recent versions of Swift (tested with 5.7)

Example:

protocol SpecialValue {}

protocol SpecialController {
    associatedtype SpecialValueType: SpecialValue
    var currentValue: SpecialValueType? { get }
}
extension String: SpecialValue {}

struct S: SpecialController {
  var currentValue: String?
}
var x: Any = S(currentValue: "Hello World!")

if let x = x as? any SpecialController {
    print(x.currentValue)
}
print(x is any SpecialController)
print(type(of: x) is any SpecialController.Type)

Output:

Optional("Hello World!")
true
true
Limburg answered 14/12, 2023 at 17:9 Comment(0)
P
0

This doesn't work because SpecialController isn't a single type. You can think of associated types as a kind of generics. A SpecialController with its SpecialValueType being an Int is a completely different type from a SpecialController with its SpecialValueType being an String, just like how Optional<Int> is a completely different type from Optional<String>.

Because of this, it doesn't make any sense to cast to SpecialValueType, because that would gloss over the associated type, and allow you to use (for example) a SpecialController with its SpecialValueType being an Int where a SpecialController with its SpecialValueType being a String is expected.

As compiler suggests, the only way SpecialController can be used is as a generic constraint. You can have a function that's generic over T, with the constraint that T must be a SpecialController. The domain of T now spans all the various concrete types of SpecialController, such as one with an Int associated type, and one with a String. For each possible associated type, there's a distinct SpecialController, and by extension, a distinct T.

To draw out the Optional<T> analogy further. Imagine if what you're trying to do was possible. It would be much like this:

func funcThatExpectsIntOptional(_: Int?) {}

let x: Optional<String> = "An optional string"
// Without its generic type parameter, this is an incomplete type. suppose this were valid
let y = x as! Optional
funcThatExpectsIntOptional(y) // boom.
Protamine answered 2/11, 2016 at 19:18 Comment(16)
My associated type has a constraint on it. I know that whatever currentValue is, it must be a SpecialValue or a subtype of it. So with your Optional analogy, what I'm really trying to do is... given an Optional<X> where I know X : SpecialValue just get the value out of it.Durante
That's not a constraint. It's a default.Somnolent
@Somnolent Notice that I use a colon, not an equals sign. If you adopt SpecialController, then currentValue's type must be SpecialValue or a subtype.Durante
Not a problem in Obj-C however. :)Concordia
@Concordia Obj-C's weak typing is far far away from where this is at :pProtamine
Ah I see. Misread that. However, we're still both right. :-) The point remains that a protocol with an associated type is considered incomplete.Somnolent
@RobN That constraint doesn't address the issue. It limits the domain of possible concrete types SpecialController can be, but SpecialController still represents a set of multiple separate types, one per every SpecialValue type.Protamine
I'm don't mean to argue with either of you about what Swift thinks. I'm just explaining that what I'm saying is logically sane, so it seems their could be a way to say it the language. That's why I can do it in, for example, Java, with runtime type-checked casts.Durante
@AlexanderMomchliov Of course. But note that as? is being used.. this is a check that can fail at runtime. Anyway, no need to dwell on that in this thread.Concordia
@RobN It's actually not sane, and Java should not allow it. The only reason it does is because its generics system is a bodged compile-time abstraction over the Object type. Once your generalized code compiles, it's all Object under the hood, with unrestricted ability to do run time casts and such. You can achieve the same in Swift with Any... but you shouldn't.Protamine
@RobN Would you agree that Array is an incomplete type?Protamine
Sure, "incomplete", or I've heard other terms like "higher kinded" or "existential". Either way, does it not make logical sense to ask "Is x an Array?" without specifying the type parameter to Array? That question has a clear true/false answer for all objects/values, right?Durante
@RobN Yes, it's kind of like a higher kinder a type, a type constructor that takes a type parameter (the element type) and uses it to construct your new array type. The question isn't quite valid, because Array on its own is not a type. Array of something is a type, and perhaps you don't care for what that "something" is, so it can be "anything". You can perform this check in swift: let isArray = ([1, 2, 3] as Any) is Array<Any>Protamine
@RobN So if you agree that Array is an incomplete type, what about SpecialController? If I tell you I have an instance of SpecialController, have I provided sufficient type information to explain all aspects of the behaviour of SpecialController?Protamine
No, but you provided enough that we know it has a property called currentValue, whose value is of type SpecialValue, or a subtype. I should be able to get at that value, somehow.Durante
Let us continue this discussion in chat.Protamine

© 2022 - 2024 — McMap. All rights reserved.