Search code examples
scalaimplicitoverriding

How to override an implicit value, that is imported?


I have tried solutions, described in How to override an implicit value?, but it does not help. Here is a code example.

A definition of TestImplicit abstraction with 2 different implementations (analogue of ExecutionContextExecutor):

trait TestImplicit {
  def f(s:String):Unit
}
object TestImplicitImpl1 extends TestImplicit {
  override def f(s: String): Unit = println(s"1: $s")
}
object TestImplicitImpl2 extends TestImplicit {
  override def f(s: String): Unit = println(s"2: $s")
}

And in the ImplDefinition object a q variable is defined to be used implicitly via import (analogue of ExecutionContext.Implicits.global):

object ImplDefinition {
  implicit val q:TestImplicit = TestImplicitImpl1
}

Client that defines a method, accepting TestImplicit implicitly (analogue of scala.concurrent.Future):

trait TestImplicitClient {
  def fu(implicit ti:TestImplicit):Unit
}
object TestImplicitClient extends TestImplicitClient {

  override def fu(implicit ti: TestImplicit): Unit = {
    println("client")
    ti.f("param")
  }
}

The next step, a client of client, that chooses which implementation of TestImplicit should be used, the decision is done via import (analogue of API that uses Future):

object ClientOfClient {

  import somepackage.ImplDefinition.q

  def t():Unit =
    TestImplicitClient.fu
}

Now in test, I want to use this ClientOfClient.t(), but I need to override implicit, and use TestImplicitImpl2 instead. The main idea behind - implicits should be defined/overridable by the client of API, but not by API itself:

import somepackage.{ClientOfClient, TestImplicit, TestImplicitImpl2}
import org.junit.Test

class ImplTest {
  // trying to hide it via import, does not help
  import somepackage.ImplDefinition.{q => _,_}

  @Test def test(): Unit ={
    //trying to hide it via downgrading to non-implicit, does not work either
    val q = somepackage.ImplDefinition.q
    implicit val ti = TestImplicitImpl2
    ClientOfClient.t()
  }
}

Each time I run test, I get in the output:

client
1: param

But not expected:

client
2: param

How can I fix it? I need a way to allow clients to override implicits and stay with as simple API as possible. Which means, I do not want to add additional implicit parameter into ClientOfClient.t() method.


Solution

  • As soon as you have a singleton object ClientOfClient with a hard-coded constant TestImplicitImpl1 everywhere, there is essentially nothing you can do. But there are several work-arounds.


    1. Use default implicit parameters

    object ClientOfClient {
      def t()(implicit ti: TestImplicit = ImplDefinition.q): Unit =
        TestImplicitClient.fu
    }
    
    object ImplTest {
      def test(): Unit = {
        implicit val ti2 = TestImplicitImpl2
        ClientOfClient.t()
      }
    }
    
    ImplTest.test() // 2: param
    

    2. Supply the implicit through a separate method that can be overridden

    If you want to make the implicit overridable, then make ClientOfClient extendable, and create a method (here "cocti") that returns the implicit, instead of importing the implicit directly. You can then override the method (whereas you cannot override an import).

    This here produces 2: param in the end of the day:

    trait TestImplicit {
      def f(s: String): Unit
    }
    object TestImplicitImpl1 extends TestImplicit {
      override def f(s: String): Unit = println(s"1: $s")
    }
    object TestImplicitImpl2 extends TestImplicit {
      override def f(s: String): Unit = println(s"2: $s")
    }
    
    object ImplDefinition {
      implicit val q: TestImplicit = TestImplicitImpl1
    }
    
    trait TestImplicitClient {
      def fu(implicit ti: TestImplicit): Unit
    }
    
    object TestImplicitClient extends TestImplicitClient {
      override def fu(implicit ti: TestImplicit): Unit = {
        println("client")
        ti.f("param")
      }
    }
    
    class ClientOfClient {
    
      implicit def cocti: TestImplicit = {
        ImplDefinition.q
      }
    
      def t():Unit =
        TestImplicitClient.fu
    }
    
    object ImplTest {
      def test(): Unit = {
        implicit val ti2 = TestImplicitImpl2
        new ClientOfClient {
          override def cocti = ti2
        }.t()
      }
    }
    
    ImplTest.test() // 2: param