7

我来自 C++ 并试图围绕 scala 的类型系统。

考虑以下 C++ 模板类:

template<class T>
class Point2
{
  Point2( T x, T y ) :
     x(x),
     y(y)
  {}

  T x;
  T y;
  Point2<T> operator+( Point<T> const& other ) const
  {
     return Point<T>(x+other.x, y+other.y);
  }
  T sumComponents() const { return x+y; }
}

Point2<Double> p0(12.3, 45.6) 
Point2<Double> p1(12.3, 45.6) 
Point2<Double> p = p1+p2
Double d = p1.sumComponents()

我发现我想写这样的东西:

case class Point2[ T ] (x:T, y:T) {
   def +() Point2[T]: = x+y
   def sumComponents() T: = x+y
}

或者,(因为编译有问题),

trait Addable[T] {   // Require T supports the + operatory
   def +( that:T ):T
}
case class Point2[ T<:Addable[T] ] (x:T, y:T) {
   def +() Point2[T]: = x+y
   def sumComponents() T: = x+y
}

这同样是有问题的,因为我不能要求 Double 来扩展 Addable。

一般来说,我发现 scala 的类型系统适用于一组我不太了解的约束。

在scala中实现上述内容的惯用方式是什么?

C++模板程序员理解scala中泛型限制的正确方法是什么?(为什么我不能在 scala 中这样做?例如是因为泛型是在实例化之前编译的吗?)

4

5 回答 5

17

在scala中实现上述内容的惯用方式是什么?

通过指定适当的要求T,或使用类型类来提供所需的行为。我稍后再谈。

C++模板程序员理解scala中泛型限制的正确方法是什么?(为什么我不能在 scala 中这样做?例如是因为泛型是在实例化之前编译的吗?)

C++ 模板在“使用”站点编译,并且为模板的每个参数组合生成不同的代码。因此,如果将上面的类与intand一起使用,则会编译出double两个不同的类。Point2

基本上,C++ 模板是宏,尽管远没有#define宏那么愚蠢。事实上,C++ 模板是图灵完备的。也许将来有可能完成类似的事情,即将为 Scala 2.11 及更高版本计划的宏功能,但现在让我们忽略它。

类型参数(Scala 等价于 Java泛型)不会改变代码的编译方式。参数化类在编译时生成其字节码,而不是在使用时。因此,当用 实例化 aPoint2Double,生成字节码为时已晚。

这意味着参数化类生成的代码必须与类可以实例化的所有类型兼容

这就是问题的根源:任何调用的方法都T必须知道TPoint2编译时存在。因此,T必须定义为具有定义此类方法的特征或类的上限,如您在示例中所示。

当然,正如您正确指出的那样,这并不总是可能的,这就是类型类的用武之地。类型类是一组类型,为其定义了一组行为。在 Scala 中实现的类型类被定义为类,其实例定义了其他类的行为。

在您给出的示例中,如果您还需要分数除法,您将使用Numeric类型类或类型类。Fractional类型类使用的一个简单示例是:

scala> import scala.math.Numeric
import scala.math.Numeric

scala> def sum[T](x: T, y: T)(implicit num: Numeric[T]): T = num.plus(x, y)
sum: [T](x: T, y: T)(implicit num: scala.math.Numeric[T])T

或者,使用称为“上下文边界”的特殊符号,

scala> def sum[T : Numeric](x: T, y: T): T = implicitly[Numeric[T]].plus(x, y)
sum: [T](x: T, y: T)(implicit evidence$1: scala.math.Numeric[T])T

该表示法T : Numeric可以这样解读,T即存在一个Numeric[T]可用的隐式实例。如果可以找到(或在编译时失败),则代码implicitly[X]返回类型的隐式值。X

现在,注意没有方法被调用,x而是y——相反,我们调用num类为 的方法Numeric[T]。该类Numeric[T]有一个plus知道如何添加两个Ts 的方法。

因为我们需要的是类型类实例,所以可以很容易地添加新类型来满足类型类。可以很容易地声明一个Numeric类型类Point2(假设它的所有方法都可以实现):

class Point2Numeric[T](implicit num: Numeric[T]) extends Numeric[Point2[T]] {
  def plus(x: Point2[T], y: Point2[T]): Point2[T] = x + y
  // etc
}
implicit def ToPoint2Numeric[T : Numeric] = new Point2Numeric[T]

有了它,那么对于任何T有 a 的Numeric[T]人,也会有 a Numeric[Point2[T]]

在普通类型继承(类型上限)之后,类型类是 Scala 中最常见的类型约束形式。还有其他一些更复杂的形式,对于它们是类型类还是不同的东西——例如磁铁模式,有一些讨论。以无形为例,了解人们可以采取多远的方式。

另一种过去很常见但现在更谨慎地使用的类型约束是视图边界。我不会详细介绍(实际上,搜索上下文边界和视图边界以从我自己那里找到关于它的长答案),但它们可以用于使类型类在使用时更具可读性。例如:

scala> import scala.math.Numeric.Implicits._
import scala.math.Numeric.Implicits._

scala> def sum[T : Numeric](x: T, y: T): T = x + y
sum: [T](x: T, y: T)(implicit evidence$1: scala.math.Numeric[T])T

导入的定义包含隐式转换,这些转换使得可以使用具有Ta 的类型值,就Numeric[T]好像它们本身具有类似+or的方法一样-

作为最后一点,重要的是要意识到这要经过许多间接级别,因此可能不太适合高性能代码。

于 2013-08-21T06:54:48.610 回答
6

简单地说,您可以执行以下操作:

scala> :paste
// Entering paste mode (ctrl-D to finish)

import math.Numeric
import math.Numeric.Implicits._

case class Point2[A: Numeric](x: A, y: A) {
  def + (other: Point2[A]): Point2[A] =
    Point2(this.x + other.x, this.y + other.y)

  def sumComponents: A = x + y
}

// Exiting paste mode, now interpreting.

import math.Numeric
import math.Numeric.Implicits._
defined class Point2

scala> val p1 = Point2(1, 2)
p1: Point2[Int] = Point2(1,2)

scala> val p2 = Point2(3, 4)
p2: Point2[Int] = Point2(3,4)

scala> p1 + p2
res2: Point2[Int] = Point2(4,6)

scala> val p3 = Point2(1.2, 3.4)
p3: Point2[Double] = Point2(1.2,3.4)

scala> val p4 = Point2(1.6, 6.4)
p4: Point2[Double] = Point2(1.6,6.4)

scala> p3 + p4
res3: Point2[Double] = Point2(2.8,9.8)

scala>
于 2013-08-21T01:15:09.550 回答
2

这需要一个类型类(我称之为Addition)和一个隐式转换(我通过一个名为 的隐式类来定义Op)。在实践中,您会在Numeric这种特殊情况下使用该类型,但为了便于说明,您可以这样定义自己的类型:

trait Addition[T] {

  def add(a: T, b: T): T

  implicit class Op(a: T) { 
    def +(b: T) = add(a, b)
  }
}

implicit object IntAddition extends Addition[Int] {
  def add(a: Int, b: Int) = a + b
}

implicit object DoubleAddition extends Addition[Double] {
  def add(a: Double, b: Double) = a + b
}

case class Point2[T](x: T, y: T)(implicit addition: Addition[T]) {

  import addition.Op

  def +(p: Point2[T]): Point2[T] = Point2(x + p.x, y + p.y)
  def sumComponents(): T = x + y
}
于 2013-08-21T01:17:02.530 回答
2

使用Numeric, 可作为implicit.

import scala.math.Numeric;
case class Point2[T](x: T, y: T)(implicit num: Numeric[T])

Numeric 在 API中查看,做你需要的。

于 2013-08-21T01:04:04.147 回答
2

我创建了一个库template.scala。您可以使用该库创建 C++ 风格的模板,避免复杂implicit的 s.

import com.thoughtworks.template
case class Point2[T](x:T, y:T) {
   @template def +(rhs: Point2[_]) = Point2(x + rhs.x, y + rhs.y)
   @template def sumComponents() = x + y
}

println(Point2(1, 3).sumComponents()) // Output: 4
println(Point2(1, 3) + Point2(100, 200)) // Output: Point2(101,203)

请注意,您甚至可以加上两个Point2具有不同组件类型的 s。

println(Point2(1.5, 0.3) + Point2(100, 200)) // Output: Point2(101.5,200.3)

甚至嵌套Point2

// Output: Point2(Point2(10.1,20.2),Point2(101.0,202.0))
println(Point2(Point2(0.1, 0.2), Point2(1.0, 2.0)) + Point2(Point2(10, 20), Point2(100, 200)))

它之所以有效,是因为@template函数是将在调用站点内联的代码模板。

于 2017-03-09T05:10:31.850 回答