swiftsetoverloadingextend

Overloading Set in Swift


Some background

I am very new to Swift and MacOS and trying to learn the Swift way to do things. This started because I realized that tuples are not hashable so I rolled my own:

struct XY: Hashable
{
    let x: Int
    let y: Int

    init(_ tuple: (Int, Int))
    {
        x = tuple.0
        y = tuple.1
    }
}

This is well and good as now I can do stuff like make a Set of XY's and manipulate tuples as a Set.

var xyset: Set<XY> = Set<XY>()
var xy1: XY = XY((3, 1))
var xy2: XY = XY((1, 1))
xyset.insert(xy1)
xyset.insert(xy2)
var found : Bool = xyset.contains(XY((1,1)))
print(found) //true

My issue

Having to XY everything is a bit redundant so I was looking to extend Set operations with my own functions so I can abstract some of that XY stuff out. I haven't been able to find a good way to do this. I think what I want to do is roughly speaking something like this:

struct XYSet: Set<XY>
{
    func insert(_ x: Int, _ y: Int)
    {
        this.insert(XY((x, y)))
    }
    
    func contains(_ x: Int, _ y: Int)
    {
        return this.contains(XY((x, y)))
    }
}

I would like to be able to rewrite the above prior code could to:

var xyset: XYSet = XYSet()
xyset.insert(3, 1)
xyset.insert(1, 1)
var found : Bool = xyset.contains(1, 1)
print(found) //true

Solution

  • You're on exactly the right road when you said "extend." You want to write an extension:

    extension Set<XY>
    {
        mutating func insert(_ x: Int, _ y: Int)
        {
            insert(XY((x, y)))
        }
    
        func contains(_ x: Int, _ y: Int) -> Bool
        {
            contains(XY((x, y)))
        }
    }
    
    var xyset: Set<XY> = Set()
    xyset.insert(3, 1)
    xyset.insert(1, 1)
    var found : Bool = xyset.contains(1, 1)
    print(found) //true
    

    That said, typically you wouldn't want to do this in Swift. Usually the thing you're holding isn't really "an ordered pair of Ints." It has some semantics like "a board location." And so you'd want to just create a struct like BoardLocation that expressed that thing. In Swift, types express what a thing means, not just what properties it happens to have. You don't want to assign a "board location" value to a "birthday" variable, just because both happen to be made of two integers.

    (In technical terms, Swift is "nominally typed." Two types with different names are distinct, even if they have the same structure. This is in contrast to a language like TypeScript which is "structurally typed." Two TypeScript types are "compatible" if their properties are compatible.)

    If you really mean exactly "an ordered pair that have no other meaning but that they are an ordered pair," then typically you'd create a generic Pair type:

    struct Pair<First, Second> {
        var first: First
        var second: Second
        init(_ first: First, _ second: Second) {
            self.first = first
            self.second = second
        }
    }
    
    extension Pair: Equatable where First: Equatable, Second: Equatable {}
    extension Pair: Hashable where First: Hashable, Second: Hashable {}
    
    // This is how you'd add your desired extension.
    // For syntactic reasons, you can't express the nested generic here like
    // "extension Set<Pair>", you have to add `where` clauses on the methods.
    // It's just a current language limitation.
    extension Set
    {
        mutating func insert<First, Second>(_ x: First, _ y: Second)
        where Element == Pair<First, Second>
        {
            insert(Pair(x, y))
        }
    
        func contains<First, Second>(_ x: First, _ y: Second) -> Bool
        where Element == Pair<First, Second>
        {
            contains(Pair(x, y))
        }
    }
    
    var xyset: Set<Pair<Int, Int>> = Set()
    xyset.insert(3, 1)
    xyset.insert(1, 1)
    let found = xyset.contains(1, 1)
    print(found) //true