I'm wondering whether there is an elegant way to have both the default implicit conversions and some user-defined custom conversion in the same scope. I have the following use-case:
- Suppose we have a trait which defines some binary operation (e.g.
PlusSupport
, which definesplus(x, y)
) for generic typeE
- We can add
"+"
syntax on objects that can be converted toPlusSupport
and for that we need to provide implicit conversions - There are a lot of default implicit conversions to
PlusSupport
for many different types and user of our library always import them as e.g.import defaultConversions._
(import all and don't think a lot ) - Now the user adds some custom implicit conversion (
implicit val customConversion = ...
) for some type which already has the default conversion fromimport defaultConversions._
(this custom conversion may be either user-written or provided by the third-party librarycom.3dparty.veryAdvancedConversions.AwesomePlus
); the user expects that his custom conversion will be used
Here is the code example:
trait A // some type A
trait B // some type B
// ... many other types goes here
// some binary operation
trait PlusSupport[E] {
def plus(a: E, b: E): E
}
object defaultConversions {
// default conversion of A to PlusSupport[A]
implicit def mkPlusSupportForA: A => PlusSupport[A] = _ => (a1: A, a2: A) => a1
// default conversion of B to PlusSupport[B]
implicit def mkPlusSupportForB: B => PlusSupport[B] = _ => (b1: B, b2: B) => b1
// ... many other conversions goes here
}
// + operator for elements with PlusSupport
class PlusOps[E](lhs: E)(plus: PlusSupport[E]) {
def +(rhs: E): E = plus.plus(lhs, rhs)
}
// adds "+" syntax
trait PlusSyntax {
implicit def plusOps[E](lhs: E)(implicit mkPlusSupport: E => PlusSupport[E]): PlusOps[E]
= new PlusOps[E](lhs)(mkPlusSupport(lhs))
}
object syntax extends PlusSyntax
def main(args: Array[String]): Unit = {
// import all default conversions for A, B, C, D etc. etc.
import defaultConversions._
import syntax._
// setup my custom conversion for A
implicit val myCustomPlusForA: A => PlusSupport[A] = _ => (a1: A, a2: A) => a2
val a1: A = new A {}
val a2: A = new A {}
val b1: B = new B {}
val b2: B = new B {}
// myCustomPlusForA should be used
println((a1 + a2) == a1)
println((a1 + a2) == a2)
// default conversion for B should be used
println((b1 + b2) == b1)
println((b1 + b2) == b2)
}
It doesn't compile with the following error:
Error:(52, 19) type mismatch;
found : A
required: String
println((a1 + a2) == a1)
The code can be corrected in 2 ways:
We can remove
implicit val myCustomPlusForA
-- everything will work fine and the default implicit conversion fromdefaultConversions
will be used ; but we need to use exactly my custom conversion, so this is not an optionWe can change
import defaultConversions._
toimport defaultConversions.{everything except conversion for A}
and thenmyCustomPlusForA
will be used ; but this is also a bad option since the user of the library would not take care about it (the user just want to import all "defaults" and add some "customization", e.g. he can useimplicit val myCustomPlusForA
withoutimplicit
keyword (all compiles fine) and than to addimplicit
just to test how things are changed with full customization)
So the question is how to fix the code so that both import defaultConversions._
and implicit val myCustomPlusForA
will be in the same scope and exactly myCustomPlusForA
will be used by the compiler? Which code pattern should be used to achieved the desired behaviour?
Update: the workaround that I have found so far is to use default value for implicit parameter and completely remove import defaultConversions._
(even make defaultConversions
private to avoid its usage by the users):
private def defaultMk[E](ev: E): E => PlusSupport[E] = ev match {
case _: A => mkPlusSupportForA.asInstanceOf[E => PlusSupport[E]]
case _: B => mkPlusSupportForB.asInstanceOf[E => PlusSupport[E]]
case _ => ???
}
trait PlusSyntax {
implicit def plusOps[E](lhs: E)(implicit mkPlusSupport: E => PlusSupport[E]
= defaultConversions.defaultMk(lhs)): PlusOps[E] = new PlusOps[E](lhs)(mkPlusSupport(lhs))
}
but it really looks strange to do the check at runtime while all information is available at the compile time and the compiler should just "substitute" the correct conversion.