在Scala中实现通用Vector

Ben*_*Lee 4 scala

我正在尝试在Scala中实现一个通用(数学)向量,我遇到了一些如何正确执行它的问题:

1)你如何处理+和 - 这样操作a Vector[Int]和a Vector[Double]会返回Vector[Double]?简而言之,我将如何进行数字类型的自动推广(最好利用Scala的自动推广)?因为implicit n: Numeric[T]只有两个矢量的类型相同才能使用.

2)相关,我应该如何定义一个*操作,使它接受任何数值类型,并返回一个正确的数字类型的向量?也就是说,a Vector[Int] * 2.0会返回一个Vector[Double].

这是我当前的代码(它不像我想要的那样):

case class Vector2[T](val x: T, val y: T)(implicit n: Numeric[T]) {
  import n._

  def length = sqrt(x.toDouble() * x.toDouble() + y.toDouble() * y.toDouble())
  def unary_- = new Vector2(-x, -y)

  def +(that: Vector2) = new Vector2(x + that.x, y + that.y)
  def -(that: Vector2) = new Vector2(x - that.x, y - that.y)

  def *(s: ???) = new Vector2(x * s, y * s)
}
Run Code Online (Sandbox Code Playgroud)

更新

经过深思熟虑之后,我决定接受Chris K的答案,因为它适用于我所提出的所有情况,尽管类型类解决方案的冗长(Scala中的数字类型是Byte,Short,Int, Long,Float,Double,BigInt,BigDecimal,这使得在每个可能的类型对之间实现所有操作非常有趣.

我赞同这两个答案,因为它们都是很好的答案.我真的希望Gabriele Petronella的答案适用于所有可能的场景,只是因为它是一个非常优雅和明确的答案.我希望有一些方法可以最终发挥作用.

Gab*_*lla 8

一种可能的方法是在应用操作之前统一两个向量的类型.通过这样做,对Vector2[A]alwyas的操作可以Vector2[A作为参数.

类似的方法可用于乘法(参见下面的例子).

使用从隐式转换Vector2[A]Vector2[B](前提是Numeric[A]Numeric[B]同时存在,并且您有隐含的证据表明,A可以转换成B),你可以这样做:

case class Vector2[A](val x: A, val y: A)(implicit n: Numeric[A]) {
  import n.mkNumericOps
  import scala.math.sqrt

  def map[B: Numeric](f: (A => B)): Vector2[B] = Vector2(f(x), f(y))

  def length = sqrt(x.toDouble * x.toDouble + y.toDouble * y.toDouble)
  def unary_- = this.map(-_)

  def +(that: Vector2[A]) = Vector2(x + that.x, y + that.y)
  def -(that: Vector2[A]) = Vector2(x - that.x, y - that.y)
  def *[B](s: B)(implicit ev: A => B, nb: Numeric[B]) = this.map(ev(_)).map(nb.times(_, s))
}

object Vector2 {
  implicit def toV[A: Numeric, B: Numeric](v: Vector2[A])(
    implicit ev: A => B // kindly provided by scala std library for all numeric types
  ): Vector2[B] = v.map(ev(_))
}
Run Code Online (Sandbox Code Playgroud)

例子:

val x = Vector2(1, 2)         //> x  : Solution.Vector2[Int] = Vector2(1,2)
val y = Vector2(3.0, 4.0)     //> y  : Solution.Vector2[Double] = Vector2(3.0,4.0)
val z = Vector2(5L, 6L)       //> z  : Solution.Vector2[Long] = Vector2(5,6)

x + y                         //> res0: Solution.Vector2[Double] = Vector2(4.0,6.0)
y + x                         //> res1: Solution.Vector2[Double] = Vector2(4.0,6.0)
x + z                         //> res2: Solution.Vector2[Long] = Vector2(6,8)
z + x                         //> res3: Solution.Vector2[Long] = Vector2(6,8)
y + z                         //> res4: Solution.Vector2[Double] = Vector2(8.0,10.0)
z + y                         //> res5: Solution.Vector2[Double] = Vector2(8.0,10.0)

x * 2                         //> res6: Solution.Vector2[Int] = Vector2(2,4)
x * 2.0                       //> res7: Solution.Vector2[Double] = Vector2(2.0,4.0)
x * 2L                        //> res8: Solution.Vector2[Long] = Vector2(2,4)
x * 2.0f                      //> res9: Solution.Vector2[Float] = Vector2(2.0,4.0)
x * BigDecimal(2)             //> res10: Solution.Vector2[scala.math.BigDecimal] = Vector2(2,4)
Run Code Online (Sandbox Code Playgroud)

根据Chris在评论中的请求,这里是隐式转换链如何工作的一个例子

如果我们运行scala REPL scala -XPrint:typer,我们可以明确地看到implicits工作.例如

z + x
Run Code Online (Sandbox Code Playgroud)

val res1: Vector2[Long] = $line7.$read.$iw.$iw.$iw.z.+($iw.this.Vector2.toV[Int, Long]($line4.$read.$iw.$iw.$iw.x)(math.this.Numeric.IntIsIntegral, math.this.Numeric.LongIsIntegral, {
        ((x: Int) => scala.this.Int.int2long(x))
      }));
Run Code Online (Sandbox Code Playgroud)

翻译成更易读的术语是

val res: Vector2[Long] = z + toV[Int, Long](x){ i: Int => Int.int2long(i) }
                             ^____________________________________________^
                              the result of this is a Vector[Long]
Run Code Online (Sandbox Code Playgroud)

相反,x + z成为

val res: Vector2[Long] = toV[Int, Long](x){ i: Int => Int.int2long(i) } + z
Run Code Online (Sandbox Code Playgroud)

它的工作方式大致如下:

  1. 我们说 z: V[Long] + x: V[Int]
  2. 编译器看到有一个方法 +[Long, Long]
  3. 它看起来从转换V[Int]V[Long]
  4. 它找到了 toV
  5. IntLong根据需要寻找转换toV
  6. 它发现Int.int2Long,即一个功能Int => Long
  7. 然后它可以使用toV[Int, Long]即函数V[Int] => V[Long]
  8. 它确实 x + toV(z)

如果我们改为 x: V[Int] + z: V[Long]

  1. 编译器看到有一个方法 +[Int, Int]
  2. 它看起来从转换V[Long]V[Int]
  3. 它找到了 toV
  4. LongInt根据需要寻找转换toV
  5. 它找不到它!
  6. 它看到有一种方法 +[Long, Long]

我们回到上一个例子的第3点


更新

正如评论中所注意到的,这样做时会出现问题

Vector(2.0, 1.0) * 2.0f
Run Code Online (Sandbox Code Playgroud)

这几乎是个问题:

2.0f * 3.0 // 6.0: Double
Run Code Online (Sandbox Code Playgroud)

但是也

2.0 * 3.0f // 6.0: Double
Run Code Online (Sandbox Code Playgroud)

所以这个论点是什么并不重要,当混合双打和花车时,我们总是以双倍结束.不幸的是,我们需要证据A => B才能将向量转换为类型s,但有时我们实际上想要转换s为向量的类型.

我们需要处理这两个案件.第一个天真的方法可能是

def *[B](s: B)(implicit ev: A => B, nb: Numeric[B]): Vector[B] =
  this.map(nb.times(ev(_), s)) // convert A to B
def *[B](s: B)(implicit ev: B => A, na: Numeric[A]): Vector[A] =
  this.map(na.times(_, ev(s))) // convert B to A
Run Code Online (Sandbox Code Playgroud)

干净吧?太糟糕了它不起作用:在消除重载方法的歧义时,scala不考虑隐式参数.我们必须使用磁铁模式解决这个问题,如此处所示.

case class Vector2[A](val x: A, val y: A)(implicit na: Numeric[A]) {
  object ToBOrToA {
    implicit def fromA[B: Numeric](implicit ev: A => B): ToBOrToA[B] = ToBOrToA(Left(ev))
    implicit def fromB[B: Numeric](implicit ev: B => A): ToBOrToA[B] = ToBOrToA(Right(ev))
  }
  case class ToBOrToA[B: Numeric](e: Either[(A => B), (B => A)])

  def *[B](s: B)(implicit ev: ToBOrToA[B], nb: Numeric[B]) = ev match {
    case ToBOrToA(Left(f)) => Vector2[B](nb.times(f(x), s), nb.times(ev(y), s))
    case ToBOrToA(Right(f)) => Vector2[A](na.times(x, f(s)), na.times(y, f(s))
  }
}
Run Code Online (Sandbox Code Playgroud)

我们只有一个*方法,我们检查隐式参数,ev以了解是否必须将所有内容转换为向量的类型或类型s.

这种方法的唯一缺点是结果类型.ev match { ... }返回它的超类型的东西,B with A我仍然没有找到它的解决方法.

val a = x * 2.0    //> a  : Solution.Vector2[_ >: Double with Int] = Vector2(2.0,4.0)
val b = y * 2      //> b  : Solution.Vector2[_ >: Int with Double] = Vector2(6.0,8.0)
Run Code Online (Sandbox Code Playgroud)


Chr*_*s K 5

我想到了一些方法:

  1. 使用类型类,如下所示
  2. 使用Spire,Scala的数学库.可以在此处找到使用尖顶的矢量教程.
  3. 将类型类与Shapeless组合以支持任何维度的向量.阅读Shapeless'支持'抽象过度'.
  4. 在向Vector调用操作之前,将Vectors转换为相同的类型.Gabriele Petronella给出了一个很好的例子,可以使用标准Scala库提供的implicit在Scala 2.10或更高版本上执行此操作.

直接使用类型类:

这种方法在您第一次创建时有点冗长,因为必须为每个想要支持的值组合创建隐式类.但这种方法很合理.有关类型类的更多详细信息,请参见此处

如果要将以下代码复制并粘贴到scala REPL中,请务必先输入':paste'.否则,将不会拾取特征与伴随对象之间的关系,并且当输入"a + b"时将不会发现隐式.

trait NumberLike[A,B,C] {
  def plus(x: A, y: B): C
}
object NumberLike {
  implicit object NumberLikeIntDouble extends NumberLike[Int,Double,Double] {
    def plus(x: Int, y: Double): Double = x + y
  }
  implicit object NumberLikeDoubleInt extends NumberLike[Double,Int,Double] {
    def plus(x: Double, y: Int): Double = x + y
  }
  implicit object NumberLikeIntInt extends NumberLike[Int,Int,Int] {
    def plus(x: Int, y: Int): Int = x + y
  }
}


case class Vector2[T](val x: T, val y: T) {
  def +[B,C](that: Vector2[B])(implicit c:NumberLike[T,B,C]) : Vector2[C] = new Vector2[C](c.plus(this.x,that.x), c.plus(this.y,that.y))
}

val a = Vector2(1,2)
val b = Vector2(2.0,2.0)

a+a
a+b
b+a
Run Code Online (Sandbox Code Playgroud)

要向矢量添加更多运算符,例如减法和除法,然后将它们添加到NumberLike特征并使用上面的加号示例进行跟踪.