Search code examples
scalaimplicit-class

Scala: make an implicit class accessible from classes injecting the class defining it


I have an implicit class defined in a class injected in other classes like this

class A {
  implicit class B(s: String) {
    def b = ???
  }
}

class C(a: A) {}

Is there a way to access implicit class B (and in particular its method b) from the class C, without importing it explicitly? (Please note that class A could not be a trait since it also injects some classes.)


Solution

  • Solution 1 (import a._)

    Well, yes, as already noted in the comments, from your requirements it is not obvious why you wouldn't just import a._ in the body of C:

    class A {
      implicit class B(arg: String) {
        def b: String = ???
      }
    }
    
    class C(a: A) { 
      import a._
      { 
        println("hello".b)
      }
    }
    

    This one line really doesn't hurt anyone.

    If you still don't like it, then the problem might be elsewhere. Thus my second proposal.


    Solution 2 (separating typeclass-like A-interface from .b-syntax)

    This other solution is less about the reduction of number of imports in your code, and it doesn't even keep class B inside A. But it might address another issue that you maybe just can't quite articulate: it separates the functionality provided by A from the syntax provided by B.

    The structure of the following snippet is inspired by the design of the Scala Cats library, that follows a very clear policy with the implicit declarations, always separating the typeclass defintions from the syntax.

    The main idea is that:

    • Implementations of AIntf provide actual functionality
    • B provides only some additional "pimp-my-library"-style methods

    and that we want to keep these two things separate.

    Here is how to separate them, thereby also avoiding import a._ inside of C. First, you define the interface that describes the functionality provided by A:

      trait AIntf {
        def usefulOperationOnStrings(s: String): String
      }
    

    Then you can implement it by a few different A`s:

      class A extends AIntf {
        def usefulOperationOnStrings(s: String): String = "<foo>" + s + "</foo>"
      }
    
      class A2 extends AIntf {
        def usefulOperationOnStrings(s: String): String = s.toUpperCase
      }
    

    Note that the object B has disappeared from A. Instead, it is moved in a separate syntax-package, and renamed to A_Ops. The method b is also renamed to a:

      object syntax /* should be package, object only for script */ {
        object a {
          class A_Ops(wrapped: String, ai: AIntf) {
            def a: String = ai.usefulOperationOnStrings(wrapped)
          }
          implicit def everyStringHasAOps(s: String)(implicit ai: AIntf): A_Ops = {
            new A_Ops(s, ai)
          }
        }
      }
    

    This is how you use it:

    • You say in the imports that you want to refer to interface A_Intf
    • You say in the imports that you want to use the syntax syntax.a._
    • You declare the a-argument of C as implicit
    • Then you can just use "string".a syntax inside C without further imports.

    In code:

    import myproject.AIntf
    import myproject.syntax.a._
    
    class C(implicit val a: AIntf) {
      {
        println("hello".a)
      }
    }
    

    Now the implementations of AIntf and the syntax .a become independent. You can inject A2 instead of A. Or you can change the syntax from "str".a to "str".somethingEntirelyDifferent.

    The full code snippet:

    import scala.language.implicitConversions
    
    object myproject /* should be package, object only for script */ {
    
      trait AIntf {
        def usefulOperationOnStrings(s: String): String
      }
    
      object syntax /* should be package, object only for script */ {
        object a {
          class A_Ops(wrapped: String, ai: AIntf) {
            def a: String = ai.usefulOperationOnStrings(wrapped)
          }
          implicit def everyStringHasAOps(s: String)(implicit ai: AIntf): A_Ops = {
            new A_Ops(s, ai)
          }
        }
      }
    
      class A extends AIntf {
        def usefulOperationOnStrings(s: String): String = "<foo>" + s + "</foo>"
      }
    
      class A2 extends AIntf {
        def usefulOperationOnStrings(s: String): String = s.toUpperCase
      }
    }
    
    
    
    import myproject.AIntf
    import myproject.syntax.a._
    
    class C(implicit val a: AIntf) {
      {
        println("hello".a)
      }
    }
    
    val c1 = new C()(new myproject.A)
    val c2 = new C()(new myproject.A2)
    
    // prints:
    // <foo>hello</foo>
    // HELLO
    

    Unfortunately, I have no clue what guice is going to do with an implicit argument, have not tried it yet. It might force you to write

    class C @Inject()(val a: AIntf) {
      implicit aintf: AIntf = a
      ...
    }
    

    which then becomes longer then the simple import mentioned in the first part.