【问题标题】:TypeClasses in ScalaZScalaZ 中的类型类
【发布时间】:2017-11-17 18:59:32
【问题描述】:

我正在阅读 ScalaZ tutorial,现在我在 Yes-No 类型课程的部分。最终目标是将1.truthy 变为return true。这是类型类的实现:

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 个新特性来实现这一目标。但是我们可以通过使用隐式类来达到同样的效果:

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

在我看来是一样的。那么为什么我们需要使用教程中的方式呢?我错过了什么?你能解释一下有什么区别吗?

【问题讨论】:

  • 我认为你是对的,如果你只想这样,你的方式会更好。
  • @FedericoSawady 这就是我问的原因。我可以用第一种方法做哪些我不能用第二种方法做的事情。
  • 你如何用你的定义来定义对真值的操作?
  • @FedericoSawady 不太明白。真实值是布尔值。操作已定义。
  • 想想像 javascript、C++ 等语言中的真实值,即在不同场景中解释为布尔值的值。我可以定义1 || 0,这将导致true。换句话说,我可以定义真值之间的操作,因此您可能希望将真值封装在一个对象中。这就是为什么我要问你如何定义这种操作。

标签: scala typeclass scalaz


【解决方案1】:

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

最终目标是让1.truthy 返回true

这是我们试图用CanTruthyOps 做的事情,但这不是CanTruthy 类型类的 目标,更一般地,像这样的语法问题不是类型类的目标。

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

以教程中的这个方法为例(稍作修改以避免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 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

时髦的&lt;% 是隐式参数的语法糖:

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

但是类型类风格中的:也是语法糖:

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

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

后一种方法有几个优点。一种抽象的方法是,它允许我们将“这里有一些证据表明我知道如何为这种类型做 X”关注点与纯粹的句法“我可以在这件事上调用 .x”关注点分开。当然,这种分离需要一些额外的机制(两个特征而不是一个),但是在句法和语义问题之间保持清晰的界限是值得的。

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

// 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 =&gt; Addable 隐式转换这样的好方法。

类型类方法类似地处理您需要操作处理的多种类型等情况,而类型无参数方法实际上并非如此(至少不是以任何合理干净的方式)。

所以总结一下:如果你只是想要一些很好的扩充方法,而这些方法通常在你有具体类型的情况下使用,那么无类型参数的方法是完全合理的,并且可能涉及更少的代码。如果您希望能够以高效、灵活、通用且相当优雅的方式对支持某些操作的类型进行抽象,请编写一个类型类。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-04-26
    • 2016-08-08
    • 2018-06-03
    • 1970-01-01
    • 1970-01-01
    • 2012-09-07
    • 1970-01-01
    相关资源
    最近更新 更多