Reputation: 2428
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:
PlusSupport
, which defines plus(x, y)
) for generic type E
"+"
syntax on objects that can be converted to PlusSupport
and for that we need to provide implicit conversions 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 )implicit val customConversion = ...
) for some type which already has the default conversion from import defaultConversions._
(this custom conversion may be either user-written or provided by the third-party library com.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 from defaultConversions
will be used ; but we need to use exactly my custom conversion, so this is not an option
We can change import defaultConversions._
to import defaultConversions.{everything except conversion for A}
and then myCustomPlusForA
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 use implicit val myCustomPlusForA
without implicit
keyword (all compiles fine) and than to add implicit
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.
Upvotes: 3
Views: 455
Reputation: 2428
As @JesperNordenberg suggested in comment, one can use implicit prioritization. So, in order to make things work one just need to move methods from defaultConversions
to a companion object of PlusSupport
and delete defaultConversions
at all:
trait PlusSupport[E] {
def plus(a: E, b: E): E
}
// place default implicit conversions into companion object
private object PlusSupport {
// 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
}
def main(args: Array[String]): Unit = {
// no need to import from object PlusSupport
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 will be used
println((a1 + a2) == a1)
println((a1 + a2) == a2)
// default conversion for B will be used
println((b1 + b2) == b1)
println((b1 + b2) == b2)
}
Upvotes: 0