penland365
penland365

Reputation: 555

Bring Scala Implicit into call site of library code

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

Answers (1)

Travis Brown
Travis Brown

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

Related Questions