在Scala中构建类型类的不同方法?

qed*_*qed 5 scala typeclass

从Scala编程书中获取的类型示例:

case class Address(street: String, city: String)
case class Person(name: String, address: Address)

trait ToJSON {
  def toJSON(level: Int = 0): String

  val INDENTATION = "  "
  def indentation(level: Int = 0): (String,String) = 
    (INDENTATION * level, INDENTATION * (level+1))
}

implicit class AddressToJSON(address: Address) extends ToJSON {
  def toJSON(level: Int = 0): String = {
    val (outdent, indent) = indentation(level)
    s"""{
      |${indent}"street": "${address.street}", 
      |${indent}"city":   "${address.city}"
      |$outdent}""".stripMargin
  }
}

implicit class PersonToJSON(person: Person) extends ToJSON {
  def toJSON(level: Int = 0): String = {
    val (outdent, indent) = indentation(level)
    s"""{
      |${indent}"name":    "${person.name}", 
      |${indent}"address": ${person.address.toJSON(level + 1)} 
      |$outdent}""".stripMargin
  }
}

val a = Address("1 Scala Lane", "Anytown")
val p = Person("Buck Trends", a)

println(a.toJSON())
println()
println(p.toJSON())
Run Code Online (Sandbox Code Playgroud)

代码工作正常,但我的印象(来自一些博客帖子),类型通常在scala中以这种方式完成:

// src/main/scala/progscala2/implicits/toJSON-type-class.sc

case class Address(street: String, city: String)
case class Person(name: String, address: Address)

trait ToJSON[A] {
  def toJSON(a: A, level: Int = 0): String

  val INDENTATION = "  "
  def indentation(level: Int = 0): (String,String) =
    (INDENTATION * level, INDENTATION * (level+1))
}

object ToJSON {
  implicit def addressToJson: ToJSON[Address] = new ToJSON[Address] {
    override def toJSON(address: Address, level: Int = 0) : String = {
          val (outdent, indent) = indentation(level)
          s"""{
              |${indent}"street": "${address.street}",
              |${indent}"city":   "${address.city}"
              |$outdent}""".stripMargin
    }
  }
  implicit def personToJson: ToJSON[Person] = new ToJSON[Person] {
    override def toJSON(a: Person, level: Int): String = {
          val (outdent, indent) = indentation(level)
          s"""{
              |${indent}"name":    "${a.name}",
              |${indent}"address": ${implicitly[ToJSON[Address]].toJSON(a.address, level + 1)}
              |$outdent}""".stripMargin
    }
  }
  def toJSON[A](a: A, level: Int = 0)(implicit ev: ToJSON[A]) = {
    ev.toJSON(a, level)
  }
}


val a = Address("1 Scala Lane", "Anytown")
val p = Person("Buck Trends", a)


import ToJSON.toJSON
println(toJSON(a))
println(toJSON(p))
Run Code Online (Sandbox Code Playgroud)

哪种方式更好或更正确?任何见解都是受欢迎的.

Tra*_*own 18

ToJSON根本就是将第一个称为"类型类"(尽管它们并不像这些术语是标准化的,甚至你的第二个,更多的Scala-idiomatic版本在许多重要方面与Haskell中的类型类不同).

我认为定义的类型类的一个属性是它们允许您约束泛型类型.Scala提供了特殊的语法来支持上下文绑定的形式,所以我可以编写如下代码:

import io.circe.Encoder

def foo[A: Numeric: Encoder](a: A) = ...
Run Code Online (Sandbox Code Playgroud)

这会将类型约束A为具有两个NumericEncoder实例.

此语法不适用于第一个ToJSON,您必须使用视图边界(现已弃用)或隐式隐式转换参数.

第一种ToJSON风格也无法提供多种操作.例如,假设我们有一个Monoid使用类型类的标准Scala编码:

trait Monoid[A] {
  def empty: A
  def plus(x: A, y: A): A
}
Run Code Online (Sandbox Code Playgroud)

我们想把它翻译成第一种风格,我们有一个非参数化的Monoid特征,它将成为我们希望能够被视为幺半群的类型的隐式转换的目标.我们完全没有运气,因为我们没有可以参考我们emptyplus签名的类型参数.

另一个论点:标准库(Ordering,CanBuildFrom等)中的类型类都使用第二种样式,您将遇到的绝大多数第三方Scala库也是如此.

简而言之,不要使用第一个版本.只有当你只有表格的操作A => Whatever(对于某些具体的Whatever),没有良好的句法支持,并且通常不被社区认为是惯用的时候它才会起作用.