1

I'm trying to implement a generic flyweight pattern in Swift. In my code, I'm using a dictionary of weak references to weak references. The base protocol (Node) just has a position. Changing the position creates a new Node.

The implementations I've seen online don't try to clean things up when there are no longer any references to a given flyweight.

Classes conforming to Node can opt-in to flyweighting using Factory, which maintains a dictionary of existing objects.

  • When a value is deallocated, the hash function changes (see Weak<T>.hash(into:)). Will that break things?
  • Would it be better for the values to be unowned, and somehow removed from the factory in deinit? Could I do that in a generic way?
  • The identity operator === can't be used on protocol types. Can I get around that somehow in my test?
  • Is there a simpler way to do this?

Full playground code

import Foundation

protocol Node {
    // Immutable: changing the position returns a new node
    func position(_ p:CGPoint) -> Node
    func printAddress()
}

protocol HashableNode : class, Hashable, Node { }

struct Weak<T : HashableNode> : Hashable {
    static func == (lhs: Weak<T>, rhs: Weak<T>) -> Bool {
        return lhs.value == rhs.value
    }

    weak var value : T?

    func hash(into hasher: inout Hasher) {
        value?.hash(into: &hasher)
    }
}

class Factory<T:HashableNode> {
    var values = [Weak<T> : Weak<T>]()

    func make(_ proto: T) -> T {
        let w = Weak<T>(value: proto)
        if let v = values[w] {
            if let r = v.value {
                return r
            }
        }
        values[w] = w
        return proto
    }
}

class TestNode : HashableNode {

    deinit {
        print("TestNode deinit")
    }

    // Can I define equality and hash automatically?
    static func == (lhs: TestNode, rhs: TestNode) -> Bool {
        return lhs.p == rhs.p
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(p.x)
        hasher.combine(p.y)
    }


    let p:CGPoint

    init(p: CGPoint) {
        print("TestNode init")
        self.p = p
    }

    func position(_ p: CGPoint) -> Node {
        return testNodeFactory.make(TestNode(p: p))
    }

    func printAddress() {
        print(Unmanaged.passUnretained(self).toOpaque())
    }

}

let testNodeFactory = Factory<TestNode>()

func runTest() {

    let n0 = testNodeFactory.make(TestNode(p: CGPoint.zero))
    let n1 = testNodeFactory.make(TestNode(p: CGPoint.zero))

    assert(n0 === n1)

    n0.printAddress()
    n1.printAddress()

    let n2 = n0.position(CGPoint(x: 1, y: 1))

    n2.printAddress()

    // Doesn't compile:
    // Binary operator '!==' cannot be applied to operands of type 'Node' and 'TestNode'
    // assert(n2 !== n0)

}

runTest()
print("done!")

Taylor
  • 5,871
  • 2
  • 30
  • 64

1 Answers1

2

This line:

let n2 = n0.position(CGPoint(x: 1, y: 1))

The position method being called here is returning a Node, not a TestNode as you’ve explicitly created for n1 and n2. The compiler doesn’t know how to compare those two types.

You could either implement Comparable for TestNode<->Node or type cast n2 as a TestNode

Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
Jacob J
  • 61
  • 4