Reputation: 16976
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
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 ofBindable
, that is also a collection ofInt
.
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 ofInt
(and also conform it toBindable
)
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 Sequence
s:
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