In Swift can I use a tuple as the key in a dictionary?

DictionaryTuplesSwift

Dictionary Problem Overview


I'm wondering if I can somehow use an x, y pair as the key to my dictionary

let activeSquares = Dictionary <(x: Int, y: Int), SKShapeNode>()

But I get the error:

Cannot convert the expression's type '<<error type>>' to type '$T1'

and the error:

Type '(x: Int, y: Int)?' does not conform to protocol 'Hashable'

So.. how can we make it conform?

Dictionary Solutions


Solution 1 - Dictionary

The definition for Dictionary is struct Dictionary<KeyType : Hashable, ValueType> : ..., i.e. the type of the key must conform to the protocol Hashable. But the language guide tells us that protocols can be adopted by classes, structs and enums, i.e. not by tuples. Therefore, tuples cannot be used as Dictionary keys.

A workaround would be defining a hashable struct type containing two Ints (or whatever you want to put in your tuple).

Solution 2 - Dictionary

As mentioned in the answer above, it is not possible. But you can wrap tuple into generic structure with Hashable protocol as a workaround:

struct Two<T:Hashable,U:Hashable> : Hashable {
  let values : (T, U)

  var hashValue : Int {
      get {
          let (a,b) = values
          return a.hashValue &* 31 &+ b.hashValue
      }
  }
}

// comparison function for conforming to Equatable protocol
func ==<T:Hashable,U:Hashable>(lhs: Two<T,U>, rhs: Two<T,U>) -> Bool {
  return lhs.values == rhs.values
}

// usage:
let pair = Two(values:("C","D"))
var pairMap = Dictionary<Two<String,String>,String>()
pairMap[pair] = "A"

Solution 3 - Dictionary

Unfortunately, as of Swift 4.2 the standard library still doesn't provide conditional conformance to Hashable for tuples and this is not considered valid code by the compiler:

extension (T1, T2): Hashable where T1: Hashable, T2: Hashable {
  // potential generic `Hashable` implementation here..
}

In addition, structs, classes and enums having tuples as their fields won't get Hashable automatically synthesized.

While other answers suggested using arrays instead of tuples, this would cause inefficiencies. A tuple is a very simple structure that can be easily optimized due to the fact that the number and types of elements is known at compile-time. An Array instance almost always preallocates more contiguous memory to accommodate for potential elements to be added. Besides, using Array type forces you to either make item types the same or to use type erasure. That is, if you don't care about inefficiency (Int, Int) could be stored in [Int], but (String, Int) would need something like [Any].

The workaround that I found relies on the fact that Hashable does synthesize automatically for fields stored separately, so this code works even without manually adding Hashable and Equatable implementations like in Marek Gregor's answer:

struct Pair<T: Hashable, U: Hashable>: Hashable {
  let first: T
  let second: U
}

Solution 4 - Dictionary

I created this code in an app:

struct Point2D: Hashable{
    var x : CGFloat = 0.0
    var y : CGFloat = 0.0

    var hashValue: Int {
        return "(\(x),\(y))".hashValue
    }

    static func == (lhs: Point2D, rhs: Point2D) -> Bool {
        return lhs.x == rhs.x && lhs.y == rhs.y
    }
}

struct Point3D: Hashable{
    var x : CGFloat = 0.0
    var y : CGFloat = 0.0
    var z : CGFloat = 0.0

    var hashValue: Int {
        return "(\(x),\(y),\(z))".hashValue
    }

    static func == (lhs: Point3D, rhs: Point3D) -> Bool {
        return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z
    }

}

var map : [Point2D : Point3D] = [:]
map.updateValue(Point3D(x: 10.0, y: 20.0,z:0), forKey: Point2D(x: 10.0, 
y: 20.0))
let p = map[Point2D(x: 10.0, y: 20.0)]!

Solution 5 - Dictionary

If you don't mind a bit of inefficiency, you can easily convert your tuple to a string and then use that for the dictionary key...

var dict = Dictionary<String, SKShapeNode>() 

let tup = (3,4)
let key:String = "\(tup)"
dict[key] = ...

Solution 6 - Dictionary

No need special code or magic numbers to implement Hashable

Hashable in Swift 4.2:

struct PairKey: Hashable {

    let first: UInt
    let second: UInt

    func hash(into hasher: inout Hasher) {
        hasher.combine(self.first)
        hasher.combine(self.second)
    }

    static func ==(lhs: PairKey, rhs: PairKey) -> Bool {
        return lhs.first == rhs.first && lhs.second == rhs.second
    }
}

More info: https://nshipster.com/hashable/

Solution 7 - Dictionary

I suggest to implement structure and use solution similar to boost::hash_combine.

Here is what I use:

struct Point2: Hashable {
    
    var x:Double
    var y:Double
    
    public var hashValue: Int {
        var seed = UInt(0)
        hash_combine(seed: &seed, value: UInt(bitPattern: x.hashValue))
        hash_combine(seed: &seed, value: UInt(bitPattern: y.hashValue))
        return Int(bitPattern: seed)
    }
    
    static func ==(lhs: Point2, rhs: Point2) -> Bool {
        return lhs.x == rhs.x && lhs.y == rhs.y
    }
}

func hash_combine(seed: inout UInt, value: UInt) {
    let tmp = value &+ 0x9e3779b97f4a7c15 &+ (seed << 6) &+ (seed >> 2)
    seed ^= tmp
}

It's much faster then using string for hash value.

If you want to know more about magic number.

Solution 8 - Dictionary

Add extension file to project (View on gist.github.com):

extension Dictionary where Key == Int64, Value == SKNode {
    func int64key(_ key: (Int32, Int32)) -> Int64 {
        return (Int64(key.0) << 32) | Int64(key.1)
    }
    
    subscript(_ key: (Int32, Int32)) -> SKNode? {
        get {
            return self[int64key(key)]
        }
        set(newValue) {
            self[int64key(key)] = newValue
        }
    }
}

Declaration:

var dictionary: [Int64 : SKNode] = [:]

Use:

var dictionary: [Int64 : SKNode] = [:]
dictionary[(0,1)] = SKNode()
dictionary[(1,0)] = SKNode()

Solution 9 - Dictionary

You can't yet in Swift 5.3.2, But you can use an Array instead of tuple:

var dictionary: Dictionary<[Int], Any> = [:]

And usage is simple:

dictionary[[1,2]] = "hi"
dictionary[[2,2]] = "bye"

Also it supports any dimentions:

dictionary[[1,2,3,4,5,6]] = "Interstellar"

Solution 10 - Dictionary

Or just use Arrays instead. I was trying to do the following code:

let parsed:Dictionary<(Duration, Duration), [ValveSpan]> = Dictionary(grouping: cut) { span in (span.begin, span.end) }

Which led me to this post. After reading through these and being disappointed (because if they can synthesize Equatable and Hashable by just adopting the protocol without doing anything, they should be able to do it for tuples, no?), I suddenly realized, just use Arrays then. No clue how efficient it is, but this change works just fine:

let parsed:Dictionary<[Duration], [ValveSpan]> = Dictionary(grouping: cut) { span in [span.begin, span.end] }

My more general question becomes "so why aren't tuples first class structs like arrays are then? Python pulled it off (duck and run)."

Solution 11 - Dictionary

struct Pair<T:Hashable> : Hashable {
    let values : (T, T)
    
    init(_ a: T, _ b: T) {
        values = (a, b)
    }
    
    static func == (lhs: Pair<T>, rhs: Pair<T>) -> Bool {
        return lhs.values == rhs.values
    }
    
    func hash(into hasher: inout Hasher) {
        let (a, b) = values
        hasher.combine(a)
        hasher.combine(b)
    }
}

let myPair = Pair(3, 4)

let myPairs: Set<Pair<Int>> = set()
myPairs.update(myPair)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJuJoDiView Question on Stackoverflow
Solution 1 - DictionaryLukasView Answer on Stackoverflow
Solution 2 - DictionaryMarek GregorView Answer on Stackoverflow
Solution 3 - DictionaryMax DesiatovView Answer on Stackoverflow
Solution 4 - DictionaryChris FelixView Answer on Stackoverflow
Solution 5 - DictionaryPeter WagnerView Answer on Stackoverflow
Solution 6 - DictionarynorbDEVView Answer on Stackoverflow
Solution 7 - DictionaryVolodymyr B.View Answer on Stackoverflow
Solution 8 - Dictionary7footsView Answer on Stackoverflow
Solution 9 - DictionaryMojtaba HosseiniView Answer on Stackoverflow
Solution 10 - DictionaryTravis GriggsView Answer on Stackoverflow
Solution 11 - Dictionarydavid_adlerView Answer on Stackoverflow