I am trying to implement a basic protocol extension like so:
protocol Value {
func get() -> Float
mutating func set(to:Float)
}
extension Value {
static func min(of a:Value, and b:Value) -> Float {
if a < b { //Expression type 'Bool' is ambiguous without more context
return a.get()
}else{
return b.get()
}
}
static func < (a:Value, b:Value) -> Bool {
return a.get() < b.get()
}
}
At the if
clause the compiler says:Expression type 'Bool' is ambiguous without more context
. Why doesn't this work?
As touched on in this Q&A, there's a difference between operator overloads implemented as static
members and operator overloads implemented as top-level functions. static
members take an additional (implicit) self
parameter, which the compiler needs to be able to infer.
So how is the value of self
inferred? Well, it has to be done from either the operands or return type of the overload. For a protocol extension, this means one of those types needs to be Self
. Bear in mind that you can't directly call an operator on a type (i.e you can't say (Self.<)(a, b)
).
Consider the following example:
protocol Value {
func get() -> Float
}
extension Value {
static func < (a: Value, b: Value) -> Bool {
print("Being called on conforming type: \(self)")
return a.get() < b.get()
}
}
struct S : Value {
func get() -> Float { return 0 }
}
let value: Value = S()
print(value < value) // Ambiguous reference to member '<'
What's the value of self
in the call to <
? The compiler can't infer it (really I think it should error directly on the overload as it's un-callable). Bear in mind that self
at static scope in a protocol extension must be a concrete conforming type; it can't just be Value.self
(as static methods in protocol extensions are only available to call on concrete conforming types, not on the protocol type itself).
We can fix both the above example, and your example by defining the overload as a top-level function instead:
protocol Value {
func get() -> Float
}
func < (a: Value, b: Value) -> Bool {
return a.get() < b.get()
}
struct S : Value {
func get() -> Float { return 0 }
}
let value: Value = S()
print(value < value) // false
This works because now we don't need to infer a value for self
.
We could have also given the compiler a way to infer the value of self
, by making one or both of the parameters take Self
:
protocol Value {
func get() -> Float
}
extension Value {
static func < (a: Self, b: Self) -> Bool {
print("Being called on conforming type: \(self)")
return a.get() < b.get()
}
}
struct S : Value {
func get() -> Float { return 0 }
}
let s = S()
print(s < s)
// Being called on conforming type: S
// false
The compiler can now infer self
from the static type of operands. However, as said above, this needs to be a concrete type, so you can't deal with heterogenous Value
operands (you could work with one operand taking a Value
; but not both as then there'd be no way to infer self
).
Although note that if you're providing a default implementation of <
, you should probably also provide a default implementation of ==
. Unless you have a good reason not to, I would also advise you make these overloads take homogenous concrete operands (i.e parameters of type Self
), such that they can provide a default implementation for Comparable
.
Also rather than having get()
and set(to:)
requirements, I would advise a settable property requirement instead:
// Not deriving from Comparable could be useful if you need to use the protocol as
// an actual type; however note that you won't be able to access Comparable stuff,
// such as the auto >, <=, >= overloads from a protocol extension.
protocol Value {
var floatValue: Double { get set }
}
extension Value {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.floatValue == rhs.floatValue
}
static func < (lhs: Self, rhs: Self) -> Bool {
return lhs.floatValue < rhs.floatValue
}
}
Finally, if Comparable
conformance is essential for conformance to Value
, you should make it derive from Comparable
:
protocol Value : Comparable {
var floatValue: Double { get set }
}
You shouldn't need a min(of:and:)
function in either case, as when the conforming type conforms to Comparable
, it can use the top-level min(_:_:)
function.