sbooth
sbooth

Reputation: 16976

Add protocol conformance for Collection of a specific type

I'd like to add protocol conformance to Collection when Element is a specific type.

As an example of what I'm trying to do, consider the (contrived) code below for binding numeric values to placeholders in an expression.

typealias Placeholder = String

enum Number {
    case integer(Int64)
    case float(Double)
}

protocol Bindable {
    func bind(to expression: Expression, for placeholder: Placeholder)
}

class Expression {
    var values: [Placeholder: Number] = [:]
}

extension Int: Bindable {
    func bind(to expression: Expression, for placeholder: Placeholder) {
        expression.values[placeholder] = .integer(Int64(self))
    }
}

extension Float: Bindable {
    func bind(to expression: Expression, for placeholder: Placeholder) {
        expression.values[placeholder] = .float(Double(self))
    }
}

class Builder {
    let expression = Expression()
    func set<T: Bindable>(_ value: T, for placeholder: Placeholder) {
        value.bind(to: expression, for: placeholder)
    }
}

With this code it's possible to set Int and Float values:

let builder = Builder()
builder.set(Int(10), for: "Int")
builder.set(Float(10), for: "Float")

Now I'd like to be able to use the existing set(_:for:) to bind the sum of an array when Element is Int:

extension Bindable where Self: Collection, Element == Int {
    func bind(to expression: Expression, for placeholder: Placeholder) {
        expression.values[placeholder] = .integer(Int64(self.reduce(0, +)))
    }
}

// Error: Instance method 'set(_:for:)' requires that '[Int]' conform to 'Bindable'
builder.set([1,2], for: "sum")

I incorrectly assumed that the extension would make [Int] conform to Bindable.

I can work around the problem by adding an additional set(_:for:) function to Builder:

class Builder {
    func set<T: Collection>(_ value: T, for placeholder: Placeholder) where T.Element == Int {
        value.bind(to: expression, for: placeholder)
    }
}

extension Collection where Element == Int {
    func bind(to expression: Expression, for placeholder: Placeholder) {
        expression.values[placeholder] = .integer(Int64(self.reduce(0, +)))
    }
}

builder.set([1,2], for: "sum")

But I'd like to understand why the first method doesn't work.

Upvotes: 0

Views: 74

Answers (1)

Sweeper
Sweeper

Reputation: 271775

You thought that this makes all [Int] conform to Bindable:

extension Bindable where Self: Collection, Element == Int {

What the above actually says is:

Add this bind method to all conformers of Bindable, that is also a collection of Int.

You are extending the wrong thing. You should be writing an extension on [Int], not conformers of Bindable. You want to say:

Add this bind method to all collection of Int (and also conform it to Bindable)

extension Collection : Bindable where Element == Int {
    func bind(to expression: Expression, for placeholder: Placeholder) {
        expression.values[placeholder] = .integer(Int64(self.reduce(0, +)))
    }
}

The above code doesn't actually work... because unfortunately, you can't make a protocol conform to another protocol via an extension, so the next alternative is to do it for arrays:

extension Array : Bindable where Element == Int {
    func bind(to expression: Expression, for placeholder: Placeholder) {
        expression.values[placeholder] = .integer(Int64(self.reduce(0, +)))
    }
}

If you really want set to accept a generic collection/sequence, you could add another overload for set that accepts Sequences:

func set<S, T>(_ values: S,
               for placeholder: Placeholder,
               withIdentity identity: T,
               andReductionFunction reductionFunction: (T, S.Element) -> T)
    where S: Sequence, S.Element: Bindable, T: Bindable {
    values.reduce(identity, reductionFunction).bind(to: expression, for: placeholder)
}

Usage:

builder.set([1,2], for: "sum", withIdentity: 0, andReductionFunction: +)

Upvotes: 2

Related Questions