[swift-dev] Pure Cocoa NSNumbers and AnyHashable

Philippe Hausler phausler at apple.com
Thu Nov 10 12:30:54 CST 2016


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
> 

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-dev/attachments/20161110/c49af9b4/attachment.html>


More information about the swift-dev mailing list