Skip to content

[docs] Update AnyHashable documentation #29498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 12, 2020

Conversation

xwu
Copy link
Collaborator

@xwu xwu commented Jan 28, 2020

Certain AnyHashable edge cases were addressed in #17396, which defined custom boxes for some types to satisfy Equatable and Hashable semantic requirements; however, the corresponding documentation has not been updated.

As demonstrated on the Swift forums, this has been the cause of some confusion as to the intended behavior, which no longer aligns with the documentation. That the actual behavior is intended was confirmed by @jckarter in #21550:

AnyHashable should behave this way for any types that are transitively as?-castable, so you can get a String out as an NSString and v.v., an Array<T> as an NSArray, and so on. The standard library integer and floating-point types as well as CGFloat and Decimal all bridge to NSNumber, and NSNumber bridges back to any of those types if the value is exactly representable.

This is a stab at updating the documentation accordingly. I've tried to make it succinct but sufficiently understandable, but the result I've arrived at thus far doesn't exactly roll off the tongue.

Resolves SR-12091 and SR-12092.

@xwu
Copy link
Collaborator Author

xwu commented Jan 28, 2020

@swift-ci Please smoke test

@xwu
Copy link
Collaborator Author

xwu commented Jan 28, 2020

cc @jckarter @lorentey @natecook1000

Copy link
Member

@natecook1000 natecook1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, @xwu! One main note that has two applications inline. There’s also a style issue: we try to avoid abstract/single letter variable names in code samples. Can you use more descriptive identifiers?

/// If the wrapped value is of a type that can be bridged using `as` to a
/// different but compatible class defined in Foundation, or vice versa, then
/// the underlying hashable value to which operations are forwarded might not be
/// of the wrapped type itself but of a transitively bridged type:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to spell out exactly what’s happening here — something akin to “For example, the following three arrays can all bridge to NSArrays of NSNumber, so when boxed in AnyHashable, they compare and hash as if they are equivalent values.”

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to promise anything about the hash encoding used by AnyHashable. We want to reserve the right to change AnyHashable to add a type discriminator into its hash encoding, in case hash collisions between values of different types ever become a problem. (Adding a type discriminator would mean that converting to/from AnyHashable would always change hashes.)

On the other hand, if AnyHashable would guarantee that conversions from/to specific types were hash-preserving, we could use that to speed up certain casts between Set/Dictionary types. So there is a nontrivial trade-off between performance and safety here -- and this tradeoff ought to be carefully considered before we commit to any particular behavior.

I'd replace this paragraph with an explanation that makes fewer concrete promises.

AnyHashable treats types that support as/as?-conversions between them as equivalent, by internally transforming their values into a canonical representation. For instance, AnyHashable considers bridged counterparts of the same value (such as a String and an NSString, or an Int and an NSNumber) to be equal. Bridging sometimes changes the definition of equality, so that values that were originally distinct may be considered equal when converted to AnyHashable:

let string1: NSString = "café"
let string2: NSString = "cafe\u{301}" // U+301 COMBINING ACUTE ACCENT
print(string1 == string2) // prints "false"
print((string1 as AnyHashable) == (string2 as AnyHashable)) // prints "true"

Note that AnyHashable is not guaranteed to preserve the hash encodings of the original values (no matter their type), and the hash encodings it uses may change between standard library releases. Do not assume that AnyHashable generates hash values matching those produced by the original types, even if they appear interchangeable in a particular release.

Copy link
Collaborator Author

@xwu xwu Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent point @lorentey. I've updated the documentation along those lines. I also split the detailed discussion you mentioned about equality into the documentation for ==.

/// If the wrapped values are of a type that can be bridged using `as` to a
/// different but compatible class defined in Foundation, or vice versa, then
/// the underlying values compared might not be of the wrapped type itself but
/// of a transitively bridged type:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above here.

Copy link
Member

@lorentey lorentey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave the text a copy editing / technical review pass, although please take them with a ton of salt -- I'm not a native speaker. 😛

Comment on lines 112 to 115
/// For types that support conversions between each other using `as` or `as?`
/// (such as `Int` and `NSNumber`), `AnyHashable` treats their values
/// equivalently when type-erased by forwarding operations to canonical
/// representations of the wrapped values.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible edit:

/// Where `as` conversions are possible between two types (such as from `Int` to
/// `NSNumber` or between `String` and `NSString`), `AnyHashable` normalizes 
/// their values into some canonical representation, so that logically identical values 
/// compare equal no matter what their original type was.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this edit better. Some comments and a possible rewrite:

AnyHashable normalizes their values

Whose values? I think we mean the value that was wrapped in AnyHashable?

/// When `as` conversion is possible between two types (such as from `Int` to
/// `NSNumber` or between `String` and `NSString`), `AnyHashable` normalizes 
/// the wrapped value into a canonical representation, to ensure that instances of either
/// type that represent the same value  compare as equal.  For example, 
/// `AnyHashable(Int(5))` is equal to  `AnyHashable(NSNumber(5))`.

Copy link
Collaborator Author

@xwu xwu Feb 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wary of using the term "normalize" here, particularly in the context of strings, where it could be misinterpreted as implying Unicode normalization. More generally, the statement that AnyHashable normalizes the wrapped value means (to me, at least), at face value, that the underlying wrapped base value is permanently changed--which it is not. AnyHashable merely uses another ("canonical") value for the purposes of its comparison and hashing operations, but base is unchanged:

import Foundation

let x = "café" as NSString
let y = "cafe\u{301}" as NSString
x == y // false
AnyHashable(x) == AnyHashable(y) // true
AnyHashable(x).base as! NSString == AnyHashable(y).base as! NSString // false

I have adapted the wording accordingly so as to avoid giving this impression; I have incorporated the remainder of the suggested changes.

@lorentey lorentey requested a review from amartini51 January 30, 2020 02:09
@xwu xwu requested review from lorentey and natecook1000 February 6, 2020 01:44
@xwu
Copy link
Collaborator Author

xwu commented Feb 6, 2020

@swift-ci Please smoke test

Copy link
Member

@lorentey lorentey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks ready to merge!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants