Scala类型类最佳实践

spa*_*rkr 5 scala typeclass

我正在阅读并通过使用类型类来解决这个问题,并且我从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
}
Run Code Online (Sandbox Code Playgroud)

我不明白需要申请方法吗?在上面这个背景下它做了什么?

稍后,该指南描述了如何创建类型类实例:

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

实际上缩写为:

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

所以现在我的问题是,这是如何工作的?我没有得到的是需要申请方法?

编辑:我发现了一篇博客文章,描述了创建类型类的步骤,如下所示:

  1. 定义类型类合同特征Foo.
  2. 使用辅助方法定义伴随对象Foo,其方式类似于隐式方法,以及通常从函数定义Foo实例的方法.
  3. 定义定义一元或二元运算符的FooOps类.
  4. 定义从Foo实例隐式提供FooOps的FooSyntax特征.

那么点数2,3和4的交易是什么?

dk1*_*k14 5

大多数这些实践来自 Haskell(基本上模仿 Haskell 的类型类的意图是产生这么多样板的原因),其中一些只是为了方便。所以,

2)正如@Alexey Romanov 所提到的,伴随对象 withapply只是为了方便起见,因此implicitly[CsvEncoder[IceCream]]您可以只编写CsvEncoder[IceCream](又名CsvEncoder.apply[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)
Run Code Online (Sandbox Code Playgroud)

但是有时候调用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
Run Code Online (Sandbox Code Playgroud)

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

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

2 |+| 2
Run Code Online (Sandbox Code Playgroud)

如果你想知道为什么有这么多样板,为什么 scala 的implicit class语法不从头开始提供这个功能 - 答案是它implicit class实际上提供了一种创建 DSL 的方法 - 它只是不那么强大 - 它(主观上)更难提供操作别名,处理更复杂的调度(当需要时)等。

但是,有一个宏解决方案可以为您自动生成样板:https : //github.com/mpilquist/simulacrum


关于您的CsvEncoder示例的另一个重点是,这instance是创建类型类实例的便捷方法,但它apply是“召唤”(要求)这些实例的快捷方式。因此,第一个用于库扩展器(一种实现接口的方法),另一个用于用户(一种调用为该接口提供的特定操作的方法)。


Jas*_*r-M 5

另一件要注意的事情是,在无形中,该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)
Run Code Online (Sandbox Code Playgroud)

现在,当我打电话时,Generic[Foo]我会得到一个类型为Generic[Foo] { type Repr = Int :: String :: HNil }. 但是如果我调用implicitly[Generic[Foo]]所有编译器知道的结果是它是一个Generic[Foo]. 换句话说: 的具体类型Repr丢失了,我不能用它做任何有用的事情。原因是implicitly实现如下:

def implicitly[T](implicit e: T): T = e
Run Code Online (Sandbox Code Playgroud)

那个方法声明基本上是说:如果你要求一个,T我保证给你一个T,如果我找到一个,仅此而已。所以这意味着你必须询问implicitly[Generic[Foo] { type Repr = Int :: String :: HNil }],这违背了自动推导的目的。