【问题标题】:Scala: reconciling type classes with dependency injectionScala:使用依赖注入协调类型类
【发布时间】:2011-03-16 10:06:02
【问题描述】:

Scala 博主最近似乎对 type classes 模式充满热情,其中一个简单的类通过符合某些特征或模式的附加类添加了功能。作为一个过于简单化的例子,简单类:

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)

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

然而,隐式的使用假定项目的精确类型在编译时是已知的。我发现在我的代码中通常情况并非如此:我将拥有某种类型的项目 List[T] 的列表,并且需要发现正确的类型类来处理它们。

Scala 建议的方法似乎是在调用层次结构中的所有点添加 typeclass 参数。随着代码的扩展,这可能会变得很烦人,并且这些依赖项需要通过越来越不相关的方法传递到越来越长的链中。这使得代码变得混乱且难以维护,这与 Scala 的用途相反。

通常这是依赖注入介入的地方,使用库在需要的时候提供所需的对象。细节因为 DI 选择的库而异 - 我过去曾用 Java 编写过自己的库 - 但通常注入点需要精确定义所需的对象。

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

人们会建议使用哪些 Scala 技术和依赖注入库来解决这个问题?我错过了一个技巧吗?完美的 DI 库?或者这真的是症结所在?


澄清

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

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

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

【问题讨论】:

  • 这真的只是缺乏推理的问题。例如,在推理较好的 Haskell 中,4 的类型为forAll a. (Num a) => a。用 Scala 术语来说,就是[A](implicit e: Num[A])A。 Scala 目前无法像那样自动引入类型变量,或者发现隐式需要特定的类型类。据我了解,Scala 的子类型化使这个问题变得非常困难或难以处理。这使得 Scala 中的类型类的用户友好性大大低于 Haskell,但它们仍然比替代品更好。
  • 我不知道 Haskell 会对此提出异议,但除非 Scala 的规则在未来版本中发生变化,否则它对我并没有真正的帮助。

标签: scala dependency-injection implicit typeclass


【解决方案1】:

可以通过使用新的上下文绑定语法来减轻传递这些隐式依赖项的相当多的乏味。你的例子变成了

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

它的编译方式相同,但签名清晰明了,并且浮动的“噪声”变量更少。

不是一个很好的答案,但替代方案可能涉及反射,我不知道有任何库可以让它自动工作。

【讨论】:

  • 这作为一种麻醉剂很有帮助,即使它不能治愈任何东西:)
【解决方案2】:

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

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

键入类实例作为类参数

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

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 注入)

上面的一个变体,可以使用 setter 注入来使用。这一次,类型类实例通过 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...

【讨论】:

  • 在我完全遵循之前,我将不得不尝试这个,但是谢谢。
【解决方案3】:

这里反对类型类作为依赖注入的论点是,对于类型类,“项目的精确类型在编译时是已知的”,而对于依赖注入,它们不是。你可能对这个 Scala project rewrite effort where I moved from the cake pattern to type classes 感兴趣,用于依赖注入。看看implicit declarations are made 所在的这个文件。请注意环境变量的使用如何确定精确的类型?这就是你如何协调类型类的编译时间需求和依赖注入的运行时间需求。

【讨论】:

    猜你喜欢
    • 2012-12-29
    • 1970-01-01
    • 2014-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-23
    • 2018-06-29
    • 1970-01-01
    相关资源
    最近更新 更多