Extending `Any`

I keep butting up against Swift's refusal to let me extend Any (or AnyObject, similarly). e.g.:

extension Any { // ❌ Non-nominal type 'Any' cannot be extended
    var descriptionAndType: String {
        "\(String(describing: self)) [\(type(of: self))]"
    }
}

Is there any workaround?

(note that free-standing functions are not an acceptable workaround, because they are inferior to members in numerous ways, such as being incompatible with optional chaining and being [more] polluting of auto-complete suggestions)

I know that for some uses you can abuse operators for this, but it's challenging to make that (a) ergonomic, (b) work with optional chaining, and (c) not tank build times.

4 Likes

Workaround: make an empty protocol WadetregaskisExtensions, put your helpers in an extension, and add a conformance every time you want to use a new type with it. That’s the best you’ll get in today’s Swift.

I would expect an actual proposal here to depend on two things: an ideological war about whether this should be allowed and what the consequences will be, and a practical consideration about whether this will tank build times the way operators can. (Overloads are part of that, which would probably come up less here, but they aren’t all of it.)

8 Likes

Yeah, I forgot to mention that I'd seen that suggested on StackOverflow.

It doesn't apply here (and in most other use-cases I have for extending Any), because I'm encountering literally Any. I'm working with Apple's frameworks which are actually Objective-C and the bridging produces Any in many places. I genuinely have no idea what the type is because Apple decrees that it can be literally any type.

(sometimes the type is AnyObject, which is actually what it should be most of the time because it's an id in Objective-C (or maybe NSObject?), but for some reason the bridging to Swift usually uses Any - in any case, extending AnyObject isn't permitted by Swift either)

Ironically it's only because I'm being studious and carefully checking the types (as opposed to blindly type-casting them) that I need this descriptionAndType helper, because I have hundreds of places where I throw errors into which I want to include this information.

(I used to just use an Any associated value with the relevant error enum cases, which made this particular example far less important because the string formatting only had to happen in one or a couple of a places, like description / errorDescription, but Swift 6 doesn't allow that because Any as an associated value is technically not thread-safe)

Yeah, that's exactly what I'm afraid of too. I know extending Any / AnyObject has been discussed before, and I'm seen that the discussions were… not ideal.

I was just hoping (with this post & question) that there was some clever workaround I hadn't figured out yet.

2 Likes

FWIW SE-0116 explains this particular behavior: swift-evolution/proposals/0116-id-as-any.md at main · apple/swift-evolution · GitHub

1 Like

Or even just a concrete wrapper type, Wadetregaskis(myAnyValue).f().

1 Like

That's equivalent to a global function, though, with all the same downsides (namespace pollution etc).

Granted I already do stuff like that in places anyway, to use @unchecked Sendable, so it might make sense to amend those particular cases with this, at least.

If there was a way to call "foo.descriptionAndType" on anything, the amount of namespace pollution would be equally high compared to a global function, no?

You may consider using a custom (postfix) operator with Any argument, but I don't fancy those.

3 Likes

No, because that at least only pollutes after a ., whereas global functions pollute immediately. e.g. I might be typing a variable or type name, and Xcode's still going to try to show me global functions (sometimes it can exclude those based on context, but not always).

It would also pollute unqualified lookup whenever you're inside a type context (where self is available), so I think it ends up being nearly as broad. You'd just avoid the pollution in cases where you're already writing a top-level function.

6 Likes

On the flip side, unqualified lookup only considers modules imported by the current source file and their @_exported imports, whereas member lookup looks at extensions in all loaded modules. So a member of an extension of Any is not namespace at all, because two modules declaring a member with the same name will leave you with no way to distinguish the two.

7 Likes

Only if the extension is public, though…? Which it shouldn't be in any of the use-cases I can think of.