Reputation: 555
I'm trying to find a workaround for bringing a user defined implicit into the call site of some library code. I have something like this
//library code
trait Fruit { def foo: Unit }
class Apple extends Fruit {
def foo: Unit = {
// a bunch of imperative code
}
}
class Pear extends Fruit // etc
I would like to augment that to something like this
// library code
trait Fruit { def foo[+A <: Fruit](implicit fi: FruitImplicit[A]): Unit }
class Apple extends Fruit {
def foo[Apple](implicit fi: FruitImplicit[Apple]): Unit = {
// a bunch of imperative code
fi.aReallyImportantMethod
}
}
class Pear extends Fruit // etc
trait FruitImplicits[+A <: Fruit]{
def mark: Unit = Unit // default implementation that normally would be used
}
// user code
implicit val appleImplicit: FruitImplicit[Apple] = new FruitImplicit[Apple] {
def mark: Unit = println("YOLO")
}
My thought on trying to bring this into scope would be something like this:
// library code
class Apple extends Fruit {
object FruitOps extends LowPriorityFruitImplicits with FruitImplicits[A] {}
import FruitOps._
def foo[Apple](implicit fi: FruitImplicit[Apple]): Unit = {
// a bunch of imperative code
fi.aReallyImportantMethod
}
}
I know I'm missing something ( this doesn't compile obviously ). Is there a way around this? I keep thinking that there is a way to turn FruitImplicits
into FruitIMplicits[Apple]
statically which would bring in the implicit for that specific type. However, that would require creating a type that itself is its subtype.
Or maybe I'm crazy and this is a terrible idea. Any thoughts?
Upvotes: 1
Views: 87
Reputation: 139038
I think this is a terrible idea, of course, but I also think that what you're trying to do is possible (if I understand correctly).
One issue is that the Apple
type parameter for your foo
in Apple
is shadowing the type Apple
. If you want to refer to the subtype in Fruit
you'll need to use something like F-bounded polymorphism:
trait Fruit[F <: Fruit[F]] {
def foo()(implicit fi: FruitImplicit[F]): Unit
}
(I've added an extra set of parentheses in the interest of following best practices for side-effecting code, but they're not necessary.)
Your FruitImplicit
would then look like this:
trait FruitImplicit[A <: Fruit[A]] {
def mark(): Unit
}
object FruitImplicit {
implicit def defaultAction[F <: Fruit[F]]: FruitImplicit[F] =
new FruitImplicit[F] {
def mark(): Unit = println("default")
}
}
Here we supply a default action that will be used if the user doesn't provide their own implicit for the type we're calling mark
in.
Now your implementation would look like this:
class Apple extends Fruit[Apple] {
def foo()(implicit fi: FruitImplicit[Apple]): Unit = {
println("This is an apple")
fi.mark()
}
}
class Pear extends Fruit[Pear] {
def foo()(implicit fi: FruitImplicit[Pear]): Unit = {
println("This is a pear")
fi.mark()
}
}
And then:
scala> val apple = new Apple
apple: Apple = Apple@7bb35c46
scala> val pear = new Pear
pear: Pear = Pear@777bd4a2
scala> apple.foo()
This is an apple
default
scala> pear.foo()
This is a pear
default
But if the user provides an implicit…
scala> implicit val appleAction: FruitImplicit[Apple] =
| new FruitImplicit[Apple] {
| def mark(): Unit = println("YOLO")
| }
appleAction: FruitImplicit[Apple] = $anon$1@146578a5
scala> apple.foo()
This is an apple
YOLO
scala> pear.foo()
This is a pear
default
…it'll be used instead of the default for that type.
Upvotes: 1