在Scala中实现泛型向量

在Scala中实现泛型向量,scala,Scala,我试图在Scala中实现一个通用(数学)向量,我遇到了几个问题,即如何正确地实现它: 1) 如何处理+和-使得对向量[Int]和向量[Double]进行操作将返回向量[Double]?简而言之,我将如何进行数字类型的自动升级(最好利用Scala的自动升级)?因为只有当两个向量的类型相同时,使用隐式n:Numeric[T]才有效 2) 相关的,我应该如何定义一个*操作,使其接受任何数值类型,并返回正确数值类型的向量?也就是说,一个向量[Int]*2.0将返回一个向量[Double] 这是我当前的代

我试图在Scala中实现一个通用(数学)向量,我遇到了几个问题,即如何正确地实现它:

1) 如何处理+和-使得对
向量[Int]
向量[Double]
进行操作将返回
向量[Double]
?简而言之,我将如何进行数字类型的自动升级(最好利用Scala的自动升级)?因为只有当两个向量的类型相同时,使用
隐式n:Numeric[T]
才有效

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

这是我当前的代码(其行为与我所希望的不同):

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


我对这两个答案都投了较高的票,因为它们都是很好的答案。我真的希望加布里埃尔·佩特罗内拉的答案适用于所有可能的情况,哪怕只是因为这是一个非常优雅和深思熟虑的答案。我真的希望最终会有办法奏效。

想到一些方法:

  • 使用类型类,下面是一个示例
  • 使用Spire,Scala的数学库。可以找到使用Spire的向量教程
  • 将类型类与相结合,以支持任何维度的向量。请阅读“无形状”支持“抽象于算术”
  • 在调用对向量的操作之前,先将向量转换为相同的类型。Gabriele Petronella给出了一个很好的例子,说明如何使用标准Scala库提供的隐式在Scala 2.10或更高版本上执行此操作
  • 直接使用类型类:

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

    如果要将以下代码复制并粘贴到scala REPL中,请确保首先输入“:paste”。否则,当输入“a+b”时,将无法提取trait和同伴对象之间的关系,也无法找到隐式对象

    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
    

    向向量中添加更多运算符,如减法和除法,然后将它们添加到类似数字的特征中,然后使用上面的加号示例进行操作。

    一种可能的方法是在应用操作之前统一两个向量的类型。这样,对
    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
    

    其工作方式大致如下:

  • 我们说
    z:V[Long]+x:V[Int]
  • 编译器看到有一个方法
    +[Long,Long]
  • 它从
    V[Int]
    V[Long]
  • 它发现
    toV
  • 它根据
    toV
    的要求,查找从
    Int
    Long
    的转换
  • 它查找
    Int.int2Long
    ,即函数
    Int=>Long
  • 然后它可以使用
    toV[Int,Long]
    即一个函数
    V[Int]=>V[Long]
  • 它可以
    x+toV(z)
  • 如果我们改为
    x:V[Int]+z:V[Long]

  • 编译器看到有一个方法
    +[Int,Int]
  • 它从
    V[Long]
    V[Int]
  • 它发现
    toV
  • 它根据
    toV
    的要求,查找从
    Long
    Int
    的转换
  • 它找不到它
  • 它看到有一个方法
    +[Long,Long]
  • 现在我们回到上一个例子的第3点


    更新 正如在评论中所注意到的,执行时有一个问题

    Vector(2.0, 1.0) * 2.0f
    
    这几乎就是问题所在:

    2.0f * 3.0 // 6.0: Double
    
    而且

    2.0 * 3.0f // 6.0: Double
    
    所以不管什么论点,当混合双倍和浮点数时,我们总是以双倍结束。 不幸的是,为了将向量转换为
    s
    的类型,我们需要
    A=>B
    的证据,但有时我们实际上想要将
    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在消除重载方法时不考虑隐含的参数。我们必须用磁铁模式来解决这个问题。 我们只有一个

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

    这种方法唯一的缺点是结果类型。
    ev match{…}
    返回的东西是带有A的
    B的超类型,我还没有找到解决方法

    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)
    
    不错的方法,我喜欢。
    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))
      }
    }
    
    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)