Scala:通过依赖注入协调类型类

发布于 2024-09-10 08:36:48 字数 1507 浏览 1 评论 0原文

最近,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)

在您纠正我之前,我说过这是一个过于简化的示例)

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

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

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

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

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


澄清

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

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

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

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 additional class conforming to some trait or pattern. As a vastly oversimplified example, the simple class:

case class Wotsit (value: Int)

can be adapted to the Foo trait:

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

with the help of this type class:

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

The type class is typically captured at compile time with implicts, allowing both the Wotsit and its type class to be passed together into a higher order function:

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

writeAll(wotsits)

(before you correct me, I said it was an oversimplified example)

However, the use of implicits assumes that the precise type of the items is known at compile time. I find in my code this often isn't the case: I will have a list of some type of item List[T], and need to discover the correct type class to work on them.

The suggested approach of Scala would appear to be to add the typeclass argument at all points in the call hierarchy. This can get annoying as an the code scales and these dependencies need to be passed down increasingly long chains, through methods to which they are increasingly irrelevant. This makes the code cluttered and harder to maintain, the opposite of what Scala is for.

Typically this is where dependency injection would step in, using a library to supply the desired object at the point it's needed. Details vary with the library chosen for DI - I've written my own in Java in the past - but typically the point of injection needs to define precisely the object desired.

Trouble is, in the case of a type class the precise value isn't known at compile time. It must be selected based on a polymorphic description. And crucially, the type information has been erased by the compiler. Manifests are Scala's solution to type erasure, but it's far from clear to me how to use them to address this issue.

What techniques and dependency injection libraries for Scala would people suggest as a way of tackling this? Am I missing a trick? The perfect DI library? Or is this really the sticking point it seems?


Clarification

I think there are really two aspects to this. In the first case, the point where the type class is needed is reached by direct function calls from the point where the exact type of its operand is known, and so sufficient type wrangling and syntactic sugar can allow the type class to be passed to the point it's needed.

In the second case, the two points are separated by a barrier - such as an API that can't be altered, or being stored in a database or object store, or serialised and send to another computer - that means the type class can't be passed along with its operand. In this case, given an object whose type and value are known only at runtime, the type class needs somehow to be discovered.

I think functional programmers have a habit of assuming the first case - that with a sufficiently advanced language, the type of the operand will always be knowable. David and mkniessl provided good answers for this, and I certainly don't want to criticise those. But the second case definitely does exist, and that's why I brought dependency injection into the question.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

轻拂→两袖风尘 2024-09-17 08:36:48

通过使用新的上下文绑定语法,可以减轻传递这些隐式依赖项的大量繁琐工作。您的示例将

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

进行相同的编译,但会产生漂亮且清晰的签名,并且周围有更少的“噪音”变量。

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

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.

梦幻之岛 2024-09-17 08:36:48

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

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

类型类实例作为类参数

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

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...

(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...

漫漫岁月 2024-09-17 08:36:48

这里反对类型类作为依赖注入的论点是,对于类型类,“项目的精确类型在编译时是已知的”,而对于依赖注入,则不是。您可能对Scala 项目重写工作感兴趣,我从蛋糕模式转向了类型类 用于依赖注入。看一下这个文件,其中 进行了隐式声明。请注意环境变量的使用如何确定精确类型?这就是如何协调类型类的编译时要求与依赖项注入的运行时需要。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文