Scala类型类最佳实践

8
我正在阅读和学习使用类型类,我在Shapeless指南中发现了一种定义类型类的方法:
以下是示例:
object CsvEncoder {
  // "Summoner" method
  def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] =
    enc
  // "Constructor" method
  def instance[A](func: A => List[String]): CsvEncoder[A] =
    new CsvEncoder[A] {
      def encode(value: A): List[String] =
        func(value)
      }
    // Globally visible type class instances
}

我不理解的是apply方法的必要性?在上下文中它是做什么的?

之后,该指南描述了我如何创建一个类型类实例:

implicit val booleanEncoder: CsvEncoder[Boolean] =
  new CsvEncoder[Boolean] {
    def encode(b: Boolean): List[String] =
      if(b) List("yes") else List("no") 
  } 

实际上缩短为:

implicit val booleanEncoder: CsvEncoder[Boolean] =
instance(b => if(b) List("yes") else List("no"))

我的问题是,这是如何工作的?我不明白为什么需要apply方法?
编辑:我看到一篇博客文章,描述创建类型类的步骤如下:
1.定义类型类合同trait Foo。
2.定义一个伴生对象Foo,其中有一个类似于implicitly的helper方法apply,以及一种通常从函数中定义Foo实例的方式。
3.定义FooOps类,定义一元或二元运算符。
4.定义FooSyntax trait,它从Foo实例隐式提供FooOps。
那么第2、3和4点是什么意思呢?
3个回答

6
大多数这些做法来自于Haskell(基本上是为了模仿Haskell的类型类而产生了如此多的样板代码),有些则只是为了方便。所以,
2)正如@Alexey Romanov所提到的,伴生对象中的apply只是为了方便,因此您可以直接写CsvEncoder[IceCream](又名CsvEncoder.apply[IceCream]()),而不是implicitly[CsvEncoder[IceCream]],它将返回所需的类型类实例。
3)FooOps为DSL提供了方便的方法。例如,您可以拥有以下内容:
trait Semigroup[A] {
   ...
   def append(a: A, b: A)
}

import implicits._ //you should import actual instances for `Semigroup[Int]` etc.
implicitly[Semigroup[Int]].append(2,2)

但有时调用append(2,2)方法不太方便,因此提供一个符号别名是一个好的做法:

  trait Ops[A] {
    def typeClassInstance: Semigroup[A]
    def self: A
    def |+|(y: A): A = typeClassInstance.append(self, y)
  }

  trait ToSemigroupOps {
    implicit def toSemigroupOps[A](target: A)(implicit tc: Semigroup[A]): Ops[A] = new Ops[A] {
      val self = target
      val typeClassInstance = tc
    }
  }

  object SemiSyntax extends ToSemigroupOps

4) 您可以按照以下方式使用它:

import SemiSyntax._ 
import implicits._ //you should also import actual instances for `Semigroup[Int]` etc.

2 |+| 2

如果你想知道为什么有这么多样板代码,以及为什么scala的implicit class语法不能从头开始提供此功能-答案是implicit class实际上提供了一种创建DSL的方法-只是不太强大-(主观地)更难提供操作别名,处理更复杂的分派(如果需要)等等。
然而,有一个宏解决方案可以自动为您生成样板代码:https://github.com/mpilquist/simulacrum
关于您的CsvEncoder示例的另一个重要点是,instance是用于创建类型类实例的便捷方法,但apply是“召唤”(要求)这些实例的快捷方式。因此,第一个是用于库扩展者(实现接口的一种方式),而另一个是用于用户(调用为该接口提供的特定操作的一种方式)。

6

需要注意的另一件事是,在shapeless中,apply方法不仅适用于更简洁的语法。

以shapeless的Generic和某个case class Foo的简化版本为例。

trait Generic[T] {
  type Repr
}
object Generic {
  def apply[T](implicit gen: Generic[T]): Generic[T] { type Repr = gen.Repr } = gen

  /* lots of macros to generate implicit instances omitted */
}

case class Foo(a: Int, b: String)

现在,当我调用Generic[Foo]时,我将获得一个类型为Generic[Foo] { type Repr = Int :: String :: HNil }的实例。但是,如果我调用implicitly[Generic[Foo]],编译器只知道结果是一个Generic[Foo],换句话说:Repr的具体类型丢失了,我无法对其进行任何有用的操作。原因是implicitly的实现如下:
def implicitly[T](implicit e: T): T = e

这个方法声明基本上是这样的:如果你要求一个 T,我保证会给你一个 T,如果我找到了的话,除此之外就没有其他了。这意味着你必须要这样请求 implicitly[Generic[Foo] { type Repr = Int :: String :: HNil }],这就违背了自动派生的目的。


哇!你的解释真赞,我给你点个赞! - joesan

2

object CsvEncoder定义之后立即引用指南:

The apply method ... allows us to summon a type class instance given a target type:

CsvEncoder[IceCream]
// res9: CsvEncoder[IceCream] = ...

请您对创建类型类的做法进行详细解释好吗?我已经编辑了我的问题! - joesan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接