I'm wanting to create a dictionary where the keys are NSPoints, however I'm targeting older versions of macOS where NSPoint isn't hashable:
let pointMap = [NSPoint(): 0] // Conformance of 'CGPoint' to 'Hashable' is only available in macOS 15.0 or newer
Since a retroactive conformance extension would cause a conflict, I looked for alternative ways I could achieve what I need. Surprisingly, this works:
let pointMap = [NSPoint().hashValue: 0]
Since hashValue is a property of Hashable and therefore shouldn't be available on macOS < 15, I'm left very confused why this works where the former doesn't. Can anyone shed some light on this?
I'd also be interested to know if there's any way to create a retroactive conformance to Hashable but only for older systems.
hashValue is [edit: not!] injective, i.e. two different points may have the same hash value, so regardless of the backwards-deployment aspects here it's not doing what you want. The most efficient answer here is probably to make a struct that wraps an NSPoint but implements Hashable manually. The most convenient answer is probably to use the point's string representation instead as a key, using NSStringFromPoint. (No, there's not a good way to say "here's a conformance, but please only use it on older OSs".)
Thanks for the info @jrose. I was hoping to avoid making a wrapper but I do need it to be efficient so maybe that's the best option.
I'm still curious though, do you know why it lets me use the hashValue but not the point itself?
Availability of a conformance is distinct from the availability of the properties and methods that satisfy the protocol. If you think about it you can see this has to be true, because next year they could come out with a new protocol that references, say, needsDisplay, and have NSView adopt it, but of course NSView has had a needsDisplay forever.
EDIT: Of course the hashValue on NSPoint/CGPoint could have been marked with the same availability as the conformance. I don't know offhand why they chose not to do that. But it would have indeed been a choice.
Yeah, it just seems a bit bizarre that I can do this to make it magically work:
extension NSPoint: @retroactive Hashable {}
Not that I should do this, but if I'm not providing the implementation is there actually any problem here?
[edit] Maybe what I'm misunderstanding is why the original complaint that the conformance is only available in macOS 15.0 or newer is only a warning (it says "this is an error in the Swift 6 language mode"). If it's not available in older versions, how come it still compiles in Swift 5 mode?
I tested this on an older system and it runs fine with the above extension, but crashes without.
tangent: a pedantic digression into mathematical terminology, that i assume is somewhat appropriate in threads using the term 'injective'...
i presume this should read 'is not injective', since my understanding of the term is that it describes a function which maps distinct inputs to distinct outputs.
Of all the places to accidentally a word! Thank you, edited.
I…hm. The general behind-the-scenes problem with having two implementations for the same protocol is usually dynamic casts. Since NSPoint is an ObjC type, I’m not sure whether the usual rules to prefer the system implementation of a conformance will be applied correctly. Thus, you could have two different parts of the system that, yes, try to hash the point the same way, but don’t know they’re doing it the same way, and thus declare that two dictionaries of points don’t have the same type.
Actually building such an example in code would be a delicate tinkering job, so I’m not going to spend time on that. But it comes down to subtleties like that, where it partially works until it doesn’t. Though a current runtime engineer may be able to say more if they see this.
A lot of times this indicates “this was always wrong, but the compiler didn’t know to check for it before, and it’s possible you’re staying in the area where you’re not encountering consequences…so we won’t break your code right away, but we will tell you it’s a bad idea”.
When I extend a type retroactively to (like in this case) Hashable will my custom Hashable implementation be ignored when the type get's it's own conformance to Hashable?
C vs ObjC doesn’t matter here, but I should have been clearer. It’s an imported type, is the important part. (Although at least you don’t have to worry about a superclass becoming Hashable. I don’t think the language ever solved that.)
I believe the runtime will prefer a conformance from the “home” module of either the type or the protocol…but only for dynamic casts. Within your module, code will not be emitted to go check if there’s already a conformance. …if I recall correctly. And then the “imported” thing is a confounding factor because I don’t know if the overlay that adds CGPoint: Hashable is considered the “home” module for CGPoint, hence my reluctance to commit to any guidance beyond “the most correct thing to do is No Retroactive Conformances”.
Thanks again @jrose, I always appreciate getting a deeper understanding of things.
I'll just quietly continue to grizzle about why the NSPoint: Hashable implementation is available for older systems but the conformance isn't
extension NSPoint {
struct Hash: Hashable {
let point: NSPoint
}
var hashable: Hash { .init(point: self) }
}
let pointMap = [NSPoint().hashable: 0] // âś…
[edit] The above actually crashes on an older system, despite the lack of warnings or errors. The following works:
Please do file a bug about the lack of diagnostics for the synthesized implementation! But this does seem like a very tidy way to do it, the extension was a great idea.
This feels wrong though: what happens with a directory of two or more elements when the hash values happen to collide? Simulate that situation via hash(1, into: &hasher).
Hashable refines Equatable for a reason. When hash values inevetably collide, the Dictionary checks the individual values for equality to determine if they're actually duplicates.
I think perhaps @tera read the type defined in the extension as a wrapper for the hash value (since it's named NSPoint.Hash)—which would cause the problem described, just as @jrose pointed out earlier—whereas in actual fact the code above shows that the type is a Hashable-conforming wrapper for NSPoint.
Ah, yes, I got confused by the name and though this is still a variation of [NSPoint().hashValue: 0].
So is it not wise to go with retroactive Hashable conformance on NSPoint that would (hopefully ) get overridden in macOS 15, and the wrapper above NSPoint is indeed the best way to go?