Scala:使用依赖注入协调类型类

时间:2010-07-15 13:35:33

标签: scala dependency-injection implicit typeclass

最近Scala博主似乎对类型模式有很多热情,其中一个简单的类通过符合某些特征或模式的附加类添加了功能。作为一个极其简单的例子,简单的类:

case class Wotsit (value: Int)

可以适应Foo特征:

trait Foo[T] {
  def write (t: T): Unit
}

借助此类型类:

implicit object WotsitIsFoo extends Foo[Wotsit] {
  def write (wotsit: Wotsit) = println(wotsit.value)
}

通常在编译时捕获类型类,并允许将Wotsit及其类型类一起传递给更高阶函数:

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

writeAll(wotsits)

(在你纠正我之前,我说这是一个过于简单的例子)

但是,使用implicits假定在编译时已知项的精确类型。我发现在我的代码中经常不是这样的:我将列出某些类型的项目List [T],并且需要发现正确的类型类来处理它们。

Scala建议的方法似乎是在调用层次结构的所有点添加类型参数。这可能会因为代码规模而变得烦人,并且这些依赖关系需要通过越来越长的链传递,通过它们越来越无关紧要的方法。这使得代码变得混乱并且难以维护,这与Scala的目的相反。

通常,这是依赖注入将介入的地方,使用库在需要的位置提供所需的对象。详细信息因为DI选择的库而异 - 我以前用Java编写了自己的库 - 但通常注入点需要精确定义所需的对象。

麻烦的是,在类型类的情况下,在编译时不知道精确值。必须根据多态描述选择它。而且至关重要的是,类型信息已被编译器删除。清单是Scala对类型擦除的解决方案,但我不清楚如何使用它们来解决这个问题。

Scala的哪些技术和依赖注入库会被人们建议作为解决这个问题的方法?我错过了一招吗?完美的DI库?或者这真的是它似乎的关键点吗?


澄清

我认为这有两个方面。在第一种情况下,需要类型类的点是通过直接函数调用从其操作数的确切类型已知的点到达的,因此足够的类型争论和语法糖可以允许类型类传递给指出它是必要的。

在第二种情况下,两个点由屏障隔开 - 例如无法更改的API,或存储在数据库或对象存储中,或序列化并发送到另一台计算机 - 这意味着类型class不能与其操作数一起传递。在这种情况下,给定一个只在运行时知道类型和值的对象,需要以某种方式发现类型类。

我认为函数式程序员习惯于假设第一种情况 - 使用足够先进的语言,操作数的类型总是可以知道的。 David和mkniessl为此提供了很好的答案,我当然不想批评这些。但第二种情况确实存在,这就是我将依赖注入带入问题的原因。

3 个答案:

答案 0 :(得分:13)

通过使用新的上下文绑定语法,可以减轻传递这些隐式依赖项的相当繁琐的繁琐程度。你的例子变成了

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

编译相同,但可以获得漂亮和清晰的签名,并且可以使用较少的“噪音”变量。

不是一个很好的答案,但替代方案可能涉及反思,而且我不知道任何库会让它自动生效。

答案 1 :(得分:11)

(我已经在问题中替换了名字,他们没有帮我思考这个问题)

我将分两步攻击这个问题。首先,我展示了嵌套作用域如何避免必须在其使用中一直声明类型类参数。然后我将展示一个变体,其中类型类实例是“依赖注入”。

将类实例键入为类参数

为了避免在所有中间调用中将类型类实例声明为隐式参数,可以在定义特定类型类实例应该可用的范围的类中声明类型类实例。我正在使用快捷语法(“context bound”)来定义类参数。

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)))
  }
}

将类实例键入可写字段(setter injection)

上述变体,可使用二次注射器使用。这次类型类实例通过setter调用使用类型类传递给bean。

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)))
  }
}

请注意,第二个解决方案具有基于setter的注入的常见缺陷,您可以忘记设置依赖项并在使用时获得一个好的NullPointerException ...

答案 2 :(得分:0)

这里针对类型类作为依赖注入的参数是使用类型类"项目的精确类型在编译时已知"而依赖注入,他们不是。您可能对此Scala project rewrite effort where I moved from the cake pattern to type classes感兴趣的依赖注入。看一下implicit declarations are made所在的文件。请注意环境变量的使用如何确定精确类型?这就是如何协调类型类的编译时需求与依赖注入的运行时需求。