Scala: reconciling type classes with dependency injection

前端 未结 3 1439
野的像风
野的像风 2021-01-31 20:06

There seems to be a lot of enthusiasm among Scala bloggers lately for the type classes pattern, in which a simple class has functionality added to it by an addi

相关标签:
3条回答
  • 2021-01-31 20:45

    A fair amount of the tediousness of passing down those implicit dependencies can be alleviated by using the new context bound syntax. Your example becomes

    def writeAll[T:Foo] (items: List[T]) =
      items.foreach(w => implicitly[Foo[T]].write(w))
    

    which compiles identically but makes for nice and clear signatures and has fewer "noise" variables floating around.

    Not a great answer, but the alternatives probably involve reflection, and I don't know of any library that will just make this automatically work.

    0 讨论(0)
  • 2021-01-31 20:49

    The argument against type classes as dependency injection here is that with type classes the "precise type of the items is known at compile time" whereas with dependency injection, they are not. You might be interested in this Scala project rewrite effort where I moved from the cake pattern to type classes for dependency injection. Take a look at this file where the implicit declarations are made. Notice how the use of environment variables determines the precise type? That is how you can reconcile the compile time requirements of type classes with the run time needs of dependency injection.

    0 讨论(0)
  • 2021-01-31 20:50

    (I have substituted the names in the question, they did not help me think about the problem)

    I'll attack the problem in two steps. First I show how nested scopes avoid having to declare the type class parameter all the way down its usage. Then I'll show a variant, where the type class instance is "dependency injected".

    Type class instance as class parameter

    To avoid having to declare the type class instance as implicit parameter in all intermediate calls, you can declare the type class instance in a class defining a scope where the specific type class instance should be available. I'm using the shortcut syntax ("context bound") for the definition of the class parameter.

    object TypeClassDI1 {
    
      // The type class
      trait ATypeClass[T] {
        def typeClassMethod(t: T): Unit
      }
    
      // Some data type
      case class Something (value: Int)
    
      // The type class instance as implicit
      implicit object SomethingInstance extends ATypeClass[Something] {
        def typeClassMethod(s: Something): Unit =
          println("SomthingInstance " + s.value)
      }
    
      // A method directly using the type class
      def writeAll[T:ATypeClass](items: List[T]) =
        items.foreach(w => implicitly[ATypeClass[T]].typeClassMethod(w))
    
      // A class defining a scope with a type class instance known to be available    
      class ATypeClassUser[T:ATypeClass] {
    
        // bar only indirectly uses the type class via writeAll
        // and does not declare an implicit parameter for it.
        def bar(items: List[T]) {
          // (here the evidence class parameter defined 
          // with the context bound is used for writeAll)
          writeAll(items)
        }
      }
    
      def main(args: Array[String]) {
        val aTypeClassUser = new ATypeClassUser[Something]
        aTypeClassUser.bar(List(Something(42), Something(4711)))
      }
    }
    

    Type class instance as writable field (setter injection)

    A variant of the above which would be usable using setter injection. This time the type class instance is passed via a setter call to the bean using the type class.

    object TypeClassDI2 {
    
      // The type class
      trait ATypeClass[T] {
        def typeClassMethod(t: T): Unit
      }
    
      // Some data type
      case class Something (value: Int)
    
      // The type class instance (not implicit here)
      object SomethingInstance extends ATypeClass[Something] {
        def typeClassMethod(s: Something): Unit =
          println("SomthingInstance " + s.value)
      }
    
      // A method directly using the type class
      def writeAll[T:ATypeClass](items: List[T]) =
        items.foreach(w => implicitly[ATypeClass[T]].typeClassMethod(w))
    
      // A "service bean" class defining a scope with a type class instance.
      // Setter based injection style for simplicity.
      class ATypeClassBean[T] {
        implicit var aTypeClassInstance: ATypeClass[T] = _
    
        // bar only indirectly uses the type class via writeAll
        // and does not declare an implicit parameter for it.
        def bar(items: List[T]) {
          // (here the implicit var is used for writeAll)
          writeAll(items)
        }
      }
    
      def main(args: Array[String]) {
        val aTypeClassBean = new ATypeClassBean[Something]()
    
        // "inject" the type class instance
        aTypeClassBean.aTypeClassInstance = SomethingInstance
    
        aTypeClassBean.bar(List(Something(42), Something(4711)))
      }
    }
    

    Note that the second solution has the common flaw of setter based injection that you can forget to set the dependency and get a nice NullPointerException upon use...

    0 讨论(0)
提交回复
热议问题