在Scala中实现通用Vector

时间:2014-08-15 09:47:57

标签: scala

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

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

2)相关,我应该如何定义一个*操作,使其接受任何数值类型,并返回正确数字类型的向量?也就是说,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)
}

更新

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

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

2 个答案:

答案 0 :(得分:8)

一种可能的方法是在应用操作之前统一两个向量的类型。通过这样做,Vector2[A]上的操作可以使用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(_))
}

示例:

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)

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

如果我们使用scala -XPrint:typer运行scala REPL,我们可以明确地看到工作中的含义 例如

z + x

变为

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

转换为更易读的术语

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

相反,x + z变为

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

它的工作方式大致如下:

  1. 我们说z: V[Long] + x: V[Int]
  2. 编译器发现有一个方法+[Long, Long]
  3. V[Int]V[Long]
  4. 的转化看起来
  5. 找到toV
  6. 根据Int
  7. 的要求,它会查找从LongtoV的转换
  8. 找到Int.int2Long,即函数Int => Long
  9. 然后可以使用toV[Int, Long],即函数V[Int] => V[Long]
  10. x + toV(z)
  11. 如果我们改为x: V[Int] + z: V[Long]

    1. 编译器发现有一个方法+[Int, Int]
    2. V[Long]V[Int]
    3. 的转化看起来
    4. 找到toV
    5. 根据Long
    6. 的要求,它会查找从InttoV的转换
    7. 找不到它!
    8. 它看到有方法+[Long, Long]
    9. 我们回到上一个例子的第3点


      更新

      正如评论中所注意到的那样,在执行

      时会出现问题
      Vector(2.0, 1.0) * 2.0f
      

      这就是问题所在:

      2.0f * 3.0 // 6.0: Double
      

      但也

      2.0 * 3.0f // 6.0: Double
      

      所以这个论点是什么并不重要,当混合双打和花车时我们总是以双倍结束。 很遗憾,我们需要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
      

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

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

      我们只有一个*方法,我们检查隐式参数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)
      

答案 1 :(得分:5)

我想到了一些方法:

  1. 使用类型类,例如
  2. 使用Spire,Scala的数学库。可以找到使用尖顶的矢量教程here
  3. 将类型类与Shapeless组合以支持任何维度的向量。阅读无形'支持'抽象arity'。
  4. 在调用Vector上的操作之前,将Vectors转换为相同的类型。 Gabriele Petronella在Scala 2.10或更高版本上使用标准Scala库提供的隐含提供了一个很好的例子。
  5. 直接使用Type Classes:

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

    如果要将以下代码复制并粘贴到scala REPL中,请务必输入':粘贴'第一。否则,特征和伴侣对象之间的关系将不会被拾取,当一个人进入' 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
    

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