brandonscript
brandonscript

Reputation: 72885

Storing objects conforming to a protocol with generics in a typed array

I've got a protocol:

protocol Adjustable: Equatable {
    associatedtype T
    var id: String { get set }
    var value: T { get set }
    init(id: String, value: T)
}

And a struct that conforms to it:

struct Adjustment: Adjustable {
    static func == (lhs: Adjustment, rhs: Adjustment) -> Bool {
        return lhs.id == rhs.id
    }

    typealias T = CGFloat
    var id: String
    var value: T
}

And I'm building a wrapper class that behaves like a Set to handle an ordered list of these properties:

struct AdjustmentSet {
    var adjustmentSet: [Adjustable] = []
    func contains<T: Adjustable>(_ item: T) -> Bool {
        return adjustmentSet.filter({ $0.id == item.id }).first != nil
    }
}

let brightness = Adjustment(id: "Brightness", value: 0)

let set = AdjustmentSet()
print(set.contains(brightness))

But that of course doesn't work, erroring with:

error: protocol 'Adjustable' can only be used as a generic constraint because it has Self or associated type requirements var adjustmentSet: [Adjustable] = []

Looking around, I thought at first this was because the protocol doesn't conform to Equatable, but then I added it, and it still doesn't work (or I did it wrong).

Moreover, I would like to be able to use a generic here, so that I can do something like:

struct Adjustment<T>: Adjustable {
    static func == (lhs: Adjustment, rhs: Adjustment) -> Bool {
        return lhs.id == rhs.id
    }

    var id: String
    var value: T
}

let brightness = Adjustment<CGFloat>(id: "Brightness", value: 0)

Or:

struct FloatAdjustment: Adjustable {
    static func == (lhs: Adjustment, rhs: Adjustment) -> Bool {
        return lhs.id == rhs.id
    }
    typealias T = CGFloat
    var id: String
    var value: T
}

let brightness = FloatAdjustment(id: "Brightness", value: 0)

And still be able to store an array of [Adjustable] types, so that eventually I can do:

var set = AdjustmentSet()
if set.contains(.brightness) {
    // Do something!
}

Or

var brightness = ...
brightness.value = 1.5
set.append(.brightness)

Upvotes: 3

Views: 124

Answers (3)

Soumya Mahunt
Soumya Mahunt

Reputation: 2762

With Swift 5.7 you will be able to this without any error from the compiler by prefixing your protocol with any, so your set becomes:

struct AdjustmentSet {
    var adjustmentSet: [any Adjustable] = []
    func contains(_ item: some Adjustable) -> Bool {
        return adjustmentSet.first { $0.id == item.id } != nil
    }
}

Note that all items in your adjustmentSet array will be allocated on heap since compile time swift can't determine the size of existential type Adjustable as types implementing it will have variable size.

Upvotes: 1

brandonscript
brandonscript

Reputation: 72885

Have made some great progress using Alexander's suggestion; I was able to use some nested class types to inherit the base type erasure class, and use a generic protocol that conforms to AnyHashable so I can use this with a set!

// Generic conforming protocol to AnyHashable
protocol AnyAdjustmentProtocol {
    func make() -> AnyHashable
}

protocol AdjustmentProtocol: AnyAdjustmentProtocol {
    associatedtype A
    func make() -> A
}

struct AdjustmentTypes {
    internal class BaseType<T>: Hashable {

        static func == (lhs: AdjustmentTypes.BaseType<T>, rhs: AdjustmentTypes.BaseType<T>) -> Bool {
            return lhs.name == rhs.name
        }

        typealias A = T

        var hashValue: Int { return name.hashValue }

        let name: String
        let defaultValue: T
        let min: T
        let max: T
        var value: T

        init(name: String, defaultValue: T, min: T, max: T) {
            self.name = name
            self.defaultValue = defaultValue
            self.min = min
            self.max = max
            self.value = defaultValue
        }
    }

    class FloatType: BaseType<CGFloat> { }

    class IntType: BaseType<Int> { }
}

struct AnyAdjustmentType<A>: AdjustmentProtocol, Hashable {
    static func == (lhs: AnyAdjustmentType<A>, rhs: AnyAdjustmentType<A>) -> Bool {
        return lhs.hashValue == rhs.hashValue
    }

    private let _make: () -> AnyHashable
    private let hashClosure:() -> Int

    var hashValue: Int {
        return hashClosure()
    }

    init<T: AdjustmentProtocol & Hashable>(_ adjustment: T) where T.A == A {
        _make = adjustment.make
        hashClosure = { return adjustment.hashValue }
    }
    func make() -> AnyHashable {
        return _make()
    }
}

struct Brightness: AdjustmentProtocol, Hashable {
    func make() -> AnyHashable {
        return AdjustmentTypes.FloatType(name: "Brightness", defaultValue: 0, min: 0, max: 1)
    }
}
struct WhiteBalance: AdjustmentProtocol, Hashable {
    func make() -> AnyHashable {
        return AdjustmentTypes.IntType(name: "White Balance", defaultValue: 4000, min: 3000, max: 7000)
    }
}

let brightness = Brightness().make()
let whiteBalance = WhiteBalance().make()

var orderedSet = Set<AnyHashable>()

orderedSet.insert(brightness)
print(type(of: orderedSet))
print(orderedSet.contains(brightness))

for obj in orderedSet {
    if let o = obj as? AdjustmentTypes.FloatType {
        print(o.value)
    }
    if let o = obj as? AdjustmentTypes.IntType {
        print(o.value)
    }
}

Prints:

Set<AnyHashable>
true
0.0

Special thanks to this article: https://medium.com/@chris_dus/type-erasure-in-swift-84480c807534 which had a simple and clean example on how to implement a generic type eraser.

Upvotes: 2

Alexander
Alexander

Reputation: 63271

You can't have an array of items of type Adjustable, because Adjustable isn't really a type. It's a blue print that describes a set of types, one per every possible value of T.

To get around this, you need to use a type eraser https://medium.com/dunnhumby-data-science-engineering/swift-associated-type-design-patterns-6c56c5b0a73a

Upvotes: 3

Related Questions