ScalaZ 中的类型类

TypeClasses in ScalaZ

我正在阅读 ScalaZ tutorial,现在我在是-否类型的部分 class。最终目标是 1.truthyreturn true。下面是 typeclass:

的实现
trait CanTruthy[A] { self =>
  /** @return true, if `a` is truthy. */
  def truthys(a: A): Boolean
}
object CanTruthy {
  def apply[A](implicit ev: CanTruthy[A]): CanTruthy[A] = ev
  def truthys[A](f: A => Boolean): CanTruthy[A] = new CanTruthy[A] {
    def truthys(a: A): Boolean = f(a)
  }
}
trait CanTruthyOps[A] {
  def self: A
  implicit def F: CanTruthy[A]
  final def truthy: Boolean = F.truthys(self)
}
object ToCanIsTruthyOps {
  implicit def toCanIsTruthyOps[A](v: A)(implicit ev: CanTruthy[A]) =
    new CanTruthyOps[A] {
      def self = v
      implicit def F: CanTruthy[A] = ev
    }
}

implicit val intCanTruthy: CanTruthy[Int] = CanTruthy.truthys({
         case 0 => false
         case _ => true
       })

我觉得有点吓人。我们引入了 2 个新特性来实现这一目标。但是我们可以通过使用隐式 classes:

来达到同样的效果
trait CanTruthy {
  def truthy: Boolean
}

object CanTruthy{
  implicit class CanTruthyInt(i: Int) extends CanTruthy{
    override def truthy: Boolean = i match {
      case 0 => false
      case _ => true
    }
  }
}

我觉得一样。那么为什么要使用教程中的方式呢?我错过了什么样的事情?你能解释一下有什么区别吗?

我认为这里的问题是对这句话范围的误读:

Eventual goal is to get 1.truthy to return true.

这就是我们试图用 CanTruthyOps 做的事情,但这不是 CanTruthy 类型的目标 class,更普遍的语法问题不是 classes 类型的目标。

type classes 的目标是允许我们以简单、灵活、组合的方式约束类型。类型无参数 CanTruthy 方法并不能很好地支持简单部分或灵活部分或组合部分(可以说 Scala 中类型 classes 的实现也不是很简单,但它至少更简单一些,而且绝对更灵活和组合。

以教程中的这个方法为例(稍微修改以避免Any):

// Type class style
def truthyIf[A: CanTruthy, B](cond: A)(ifyes: => B)(ifno: => B): B =
  if (cond.truthy) ifyes else ifno

如果您想将其转换为您的类型无参数样式,乍一看似乎还不错:

// Parameterless style
def truthyIf[B](cond: CanTruthy)(ifyes: => B)(ifno: => B): B =
  if (cond.truthy) ifyes else ifno

但现在假设您需要保留原始类型。这可能是必要的原因有很多——例如,您可能希望在检查其中一个值的真实性之前使用 scala.Ordering 对一组值进行排序,或者您可能有此方法的变体,其中原始方法type 也是 return 类型(此处为 type class 样式):

// Type class style
def truthyOrElse[A: CanTruthy](cond: A)(ifno: => A): A =
  if (cond.truthy) cond else ifno

现在翻译没那么有趣了:

// Type parameter-less style
def truthyOrElse[A <% CanTruthy](cond: A)(ifno: => A): A =
  if (cond.truthy) ifyes else ifno

时髦的 <% 东西是隐式参数的语法糖:

// Type parameter-less style (desugared)
def truthyOrElse[A](cond: A)(ifno: => A)(implicit evidence: A => CanTruthy): A =
  if (cond.truthy) cond else ifno

但是类型class中的:也是语法糖:

// Type class style, desugared
def truthyOrElse[A](cond: A)(ifno: => A)(implicit evidence: CanTruthy[A]): A =
  if (cond.truthy) cond else ifno

请注意,这些方法看起来几乎相同 — 在这两种方法中,您都在编写一个方法,需要一些隐含的证据(在编译时)证明 A 是真实的。在类型无参数样式中,此证据是隐式转换,而在类型 class 样式中,它是泛型类型的隐式值。

后一种方法有几个优点。一种抽象的是它允许我们将 "here's some evidence that I know how to do X for this type" 关注点与纯句法 "I can call .x on this thing" 关注点分开。当然,这种分离需要一些额外的机制(两个特征而不是一个),但是在句法和语义问题之间保持清晰的界线可以说是值得的。

另一个(相关的)优点是类型 class 可以更有效,因为它允许我们放弃语法,因此也放弃了它涉及的额外分配:

// Type class style, no syntax
def truthyOrElse[A](cond: A)(ifno: => A)(implicit ev: CanTruthy[A]): A =
  if (ev.truthys(cond)) cond else ifno

如果您尝试提供证据的操作涉及多个值,则会出现另一个优势:

trait Addable[A] {
  def plus(a: A, b: A): A
}

object Addable {
  implicit val intAddable: Addable[Int] = new Addable[Int] {
    def plus(a: Int, b: Int): Int = a + b
  }
}

没有很好的方法来做这种事情作为 Int => Addable 隐式转换。

类型 class 方法类似地处理您有多个类型需要您的操作等的情况,而类型无参数方法实际上不是(至少在任何情况下都不是)相当干净的方式)。

所以总结一下:如果您只是想要一些通常在具有具体类型的情况下使用的不错的充实方法,则无类型参数的方法是完全合理的,并且可能涉及的代码会少一些。如果您希望能够以高效、灵活、通用且相当优雅的方式抽象支持某些操作的类型,请编写类型 class.