[swift-dev] Pure Cocoa NSNumbers and AnyHashable

Philippe Hausler phausler at apple.com
Thu Nov 10 12:44:21 CST 2016


Just realized some of that code won't work correctly for certain hashes - it needs to all be UInt bit patterns since Cocoa uses unsigned values for hashing and not signed Int.

Sent from my iPhone

> On Nov 10, 2016, at 10:30 AM, Philippe Hausler via swift-dev <swift-dev at swift.org> wrote:
> 
> So I think there are a few rough edges here not just the hashing or equality. I think the issue comes down to the subclass of NSNumber that is being used - it is defeating not only hashing but also performance and allocation optimizations in Foundation.
> 
> So what would we have to do to get rid of the “type preserving” NSNumber subclass?
> 
> Also the other side of things is that NSNumber is a different concept than Int or Float or Double etc. if it was written in Swift would roughly look like this:
> 
> enum Numeric : Hashable {
>     case bool(Bool)
>     case char(Int8)
>     case unsignedChar(UInt8)
>     case short(Int16)
>     case unsignedShort(UInt16)
>     case int(Int32)
>     case unsignedInt(UInt32)
>     case long(Int)
>     case unsignedLong(UInt)
>     case float(Float)
>     case double(Double)
>     case longLong(Int64)
>     case unsignedLongLong(UInt64)
>     
>     var longValue: Int {
>         switch self {
>         case .bool(let value):
>             return value ? 1 : 0
>         case .char(let value):
>             return Int(value)
>         case .unsignedChar(let value):
>             return Int(value)
>         case .short(let value):
>             return Int(value)
>         case .unsignedShort(let value):
>             return Int(value)
>         case .int(let value):
>             return Int(value)
>         case .unsignedInt(let value):
>             return Int(value)
>         case .long(let value):
>             return value
>         case .unsignedLong(let value):
>             return Int(value)
>         case .float(let value):
>             return Int(value)
>         case .double(let value):
>             return Int(value)
>         case .longLong(let value):
>             return Int(value)
>         case .unsignedLongLong(let value):
>             return Int(value)
>         }
>     }
>     
>     var unsignedLongValue: UInt {
>         switch self {
>         case .bool(let value):
>             return value ? 1 : 0
>         case .char(let value):
>             return UInt(value)
>         case .unsignedChar(let value):
>             return UInt(value)
>         case .short(let value):
>             return UInt(value)
>         case .unsignedShort(let value):
>             return UInt(value)
>         case .int(let value):
>             return UInt(value)
>         case .unsignedInt(let value):
>             return UInt(value)
>         case .long(let value):
>             return UInt(value)
>         case .unsignedLong(let value):
>             return value
>         case .float(let value):
>             return UInt(value)
>         case .double(let value):
>             return UInt(value)
>         case .longLong(let value):
>             return UInt(value)
>         case .unsignedLongLong(let value):
>             return UInt(value)
>         }
>     }
>     
>     var integerValue: Int {
>         return longValue
>     }
>     
>     var unsignedIntegerValue: UInt {
>         return unsignedLongValue
>     }
>     
>     var doubleValue: Double {
>         switch self {
>         case .bool(let value):
>             return value ? 1.0 : 0.0
>         case .char(let value):
>             return Double(value)
>         case .unsignedChar(let value):
>             return Double(value)
>         case .short(let value):
>             return Double(value)
>         case .unsignedShort(let value):
>             return Double(value)
>         case .int(let value):
>             return Double(value)
>         case .unsignedInt(let value):
>             return Double(value)
>         case .long(let value):
>             return Double(value)
>         case .unsignedLong(let value):
>             return Double(value)
>         case .float(let value):
>             return Double(value)
>         case .double(let value):
>             return value
>         case .longLong(let value):
>             return Double(value)
>         case .unsignedLongLong(let value):
>             return Double(value)
>         }
>     }
>     
>     var longLongValue: Int64 {
>         switch self {
>         case .bool(let value):
>             return value ? 1 : 0
>         case .char(let value):
>             return Int64(value)
>         case .unsignedChar(let value):
>             return Int64(value)
>         case .short(let value):
>             return Int64(value)
>         case .unsignedShort(let value):
>             return Int64(value)
>         case .int(let value):
>             return Int64(value)
>         case .unsignedInt(let value):
>             return Int64(value)
>         case .long(let value):
>             return Int64(value)
>         case .unsignedLong(let value):
>             return Int64(value)
>         case .float(let value):
>             return Int64(value)
>         case .double(let value):
>             return Int64(value)
>         case .longLong(let value):
>             return Int64(value)
>         case .unsignedLongLong(let value):
>             return Int64(value)
>         }
>     }
>     
>     private static let HASHFACTOR = 2654435761
>     
>     private static func hashInt(_ i: Int) -> Int {
>         return (i > 0 ? (i) : (-i)) * HASHFACTOR
>     }
>     
>     private static func hashDouble(_ d_: Double) -> Int {
>         var d = d_
>         if d < 0 { d = -d }
>         let dInt = floor(d + 0.5)
>         let integralHash = HASHFACTOR * Int(fmod(dInt, Double(UInt.max)))
>         return (integralHash + Int(((d - dInt) * Double(UInt.max))))
>     }
>     
>     var hashValue: Int {
>         switch self {
>         case .long:
>             fallthrough
>         case .int:
>             fallthrough
>         case .char:
>             fallthrough
>         case .bool:
>             let i = integerValue
>             return Numeric.hashInt(i)
>         case .unsignedLong:
>             fallthrough
>         case .unsignedInt:
>             fallthrough
>         case .unsignedChar:
>             let i = unsignedIntegerValue
>             return i > UInt(Int.max) ? Numeric.hashDouble(Double(i)) : Numeric.hashInt(Int(i))
>         case .longLong(let value):
>             return Numeric.hashDouble(Double(value))
>         case .unsignedLongLong(let value):
>             return Numeric.hashDouble(Double(value))
>         default:
>             return Numeric.hashDouble(doubleValue)
>         }
>     }
>     
>     private static func compareDoubles(_ d1: Double, _ d2: Double) -> ComparisonResult {
>         if d1.isNaN || d2.isNaN {
>             if d1.isNaN {
>                 if d2.isNaN {
>                     return .orderedSame
>                 }
>                 return copysign(1.0, d1) < 0.0 ? .orderedDescending : .orderedAscending
>             }
>             return copysign(1.0, d1) < 0 ? .orderedAscending : .orderedDescending
>         }
>         if d1 < d2 { return .orderedAscending }
>         if d2 < d1 { return .orderedDescending }
>         return .orderedSame
>     }
>     
>     static func ==(_ lhs: Numeric, _ rhs: Numeric) -> Bool {
>         switch (lhs, rhs) {
>         case (.double, .double):
>             fallthrough
>         case (.double, .float):
>             fallthrough
>         case (.float, .double):
>             fallthrough
>         case (.float, .float):
>             return compareDoubles(lhs.doubleValue, rhs.doubleValue) == .orderedSame
>         case (.unsignedLongLong(let lhsValue), .unsignedLongLong(let rhsValue)):
>             return lhsValue == rhsValue
>         case (.unsignedLongLong(let lhsValue), _):
>             if lhsValue >= UInt64(Int64.max) { return false }
>             break
>         case (_, .unsignedLongLong(let rhsValue)):
>             if rhsValue >= UInt64(Int64.max) { return false }
>             break
>         default:
>             break
>         }
>         return lhs.longLongValue == rhs.longLongValue
>     }
> }
> 
>> On Nov 10, 2016, at 9:48 AM, Joe Groff <jgroff at apple.com> wrote:
>> 
>> A quick ping. I'd like some feedback about how to address this problem.
>> 
>> -Joe
>> 
>>> On Nov 2, 2016, at 10:26 AM, Joe Groff via swift-dev <swift-dev at swift.org> wrote:
>>> 
>>> There's a hole in our AnyHashable implementation when it comes to what I'll call "pure" NSNumbers coming from Cocoa, which were instantiated using -[NSNumber numberWith*:] factories or @(n) syntax in Objective-C. While we maintain type specificity when Swift number types are bridged through NSNumber, NSNumbers constructed in ObjC do not necessarily remember the type they were constructed with or expect to be strictly used as only that type, so we resign to being "fuzzy" and let them bridge back to any Swift type. We however fail to bring this fuzziness to AnyHashable. When we construct an AnyHashable, we'll bring bridged NSNumbers back to their original Swift types, but we leave a pure NSNumber as an NSNumber, so it doesn't hash or equate with numeric values in Swift:
>>> 
>>> // ObjC
>>> @import Foundation;
>>> 
>>> NSDictionary *foo() {
>>>  return @{@(1): @"one"};
>>> }
>>> 
>>> // Swift
>>> let theFoo /*: [AnyHashable: Any]*/ = foo()
>>> theFoo[1] // returns nil, ought to find the value "one"
>>> 
>>> One way to address this would be to make Swift's number types use the same hashing as NSNumber does. We could go so far as to switch the "custom AnyHashable" polarity around and coerce the Swift number types into NSNumbers when we put them inside AnyHashable, which would give us consistent hashing and fuzzy equality, but would come at a performance cost when converting a number to AnyHashable. We would also lose type specificity in equality for Swift values, since NSNumber's -isEqual: only compares numeric value, unless we special-cased NSNumber in AnyHashable's implementation.
>>> 
>>> If we didn't want to adopt NSNumber's hashing for Swift's types, but we were willing to say that all of Swift's number types produce the same hashValue for the same numeric value (so 12.hashValue == 12.0.hashValue == (12 as UInt8).hashValue, etc.), we could also go the other direction, and customize a pure NSNumber's AnyHashable implementation to use Swift's number hashing. We would still need special handling for equality of a pure NSNumber with Swift numbers, but maybe that's inevitable.
>>> 
>>> -Joe
>>> _______________________________________________
>>> swift-dev mailing list
>>> swift-dev at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-dev
>> 
> 
> _______________________________________________
> swift-dev mailing list
> swift-dev at swift.org
> https://lists.swift.org/mailman/listinfo/swift-dev
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-dev/attachments/20161110/555711a4/attachment.html>


More information about the swift-dev mailing list