【问题标题】:Implementing a generic Vector in Scala在 Scala 中实现泛型 Vector
【发布时间】:2014-08-17 19:45:55
【问题描述】:

我正在尝试在 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 的答案适用于所有可能的情况,如果只是因为它是一个非常优雅和简洁的答案。我确实希望它最终会通过某种方式发挥作用。

【问题讨论】:

    标签: scala


    【解决方案1】:

    一种可能的方法是在应用操作之前统一两个向量的类型。通过这样做,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 在 cmets 中的要求,这是一个隐式转换链如何工作的示例

    如果我们使用 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. 它找到toV
    5. 它根据toV 的要求查找从IntLong 的转换
    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. 它根据toV 的要求查找从LongInt 的转换
    5. 找不到!
    6. 它看到有一个方法+[Long, Long]

    我们回到前面例子的第 3 点


    更新

    正如在 cmets 中注意到的那样,在做的时候有一个问题

    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)
    

    【讨论】:

    • 不错的方法,我喜欢。这种方法对我使用 Scala 2.9 不起作用(是的,我仍然坚持这一点),但它在 2.10 上确实有效。最好记录下对 x+z 和 z+x 的转换的隐式扫描是如何工作的。 Double 位于 LHS 时相当明显,但 Long 位于 LHS 时则不明显。
    • 这很好用,我真的很喜欢它,因为它非常简洁。它似乎不起作用的唯一部分是如果我们执行y * 2.0f,那么编译器会抱怨No implicit view available from Double => Float。这样做感觉有点奇怪,因为 Scala 处理 Double * Float+ 在添加 FloatDouble 向量的情况下确实做了正确的事情。还是 Double 自己专门处理这个案子?
    【解决方案2】:

    我想到了几种方法:

    1. 使用类型类,示例如下
    2. 使用 Spire,一个 Scala 的数学库。可以找到使用 spire 的向量教程here
    3. 将类型类与Shapeless 组合以支持任何维度的向量。阅读 Shapeless' 对'abstracting over arity' 的支持。
    4. 在调用向量上的操作之前将向量转换为相同的类型。 Gabriele Petronella 给出了一个很好的例子,在 Scala 2.10 或更高版本上使用标准 Scala 库提供的隐式执行此操作。

    直接使用类型类:

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

    如果要将以下代码复制并粘贴到 scala REPL 中,请务必先输入 ':paste'。否则 trait 与伴生对象之间的关系将不会被拾取,并且在输入 '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 特征并使用上面的加号示例进行后续操作。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-08-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多