简体   繁体   English

在Scala中实现通用Vector

[英]Implementing a generic Vector in Scala

I'm trying to implement a generic (mathematical) vector in Scala, and I'm running into a couple of issues of how to do it properly: 我正在尝试在Scala中实现一个通用(数学)向量,我遇到了一些如何正确执行它的问题:

1) How do you handle + and - such that operating on a Vector[Int] and a Vector[Double] would return a Vector[Double] ? 1)你如何处理+和 - 这样在Vector[Int]Vector[Double]会返回Vector[Double] In short, how would I go about doing auto promotion of numeric types (preferably taking advantage of Scala's auto promotion)? 简而言之,我将如何进行数字类型的自动推广(最好利用Scala的自动推广)? Because using implicit n: Numeric[T] only works if the types of both vectors are the same. 因为使用implicit n: Numeric[T]仅在两个向量的类型相同时才有效。

2) Related, how should I define a * operation such that it takes in any Numeric type, and return a vector of the right numeric type? 2)相关的,我应该如何定义一个*操作,使其接受任何数值类型,并返回一个正确的数字类型的向量? That is, a Vector[Int] * 2.0 would return a Vector[Double] . 也就是说, Vector[Int] * 2.0将返回Vector[Double]

This is my current code (which doesn't behave as I would want it): 这是我当前的代码(它不像我想要的那样):

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

Update 更新

After a lot of thought, I've decided to accept Chris K's answer, because it works for all the situations I've asked about, despite the verbosity of the type class solution (the numeric types in Scala are Byte, Short, Int, Long, Float, Double, BigInt, BigDecimal, which makes for a very fun time implementing all the operations between each possible pair of types). 经过深思熟虑之后,我决定接受Chris K的答案,因为它适用于我所提出的所有情况,尽管类型类解决方案的冗长(Scala中的数字类型是Byte,Short,Int, Long,Float,Double,BigInt,BigDecimal,这使得在每个可能的类型对之间实现所有操作非常有趣。

I've upvoted both answers, because they're both excellent answers. 我赞同这两个答案,因为它们都是很好的答案。 And I really wish Gabriele Petronella's answer worked for all possible scenarios, if only because it's a very elegant and consise answer. 我真的希望Gabriele Petronella的答案适用于所有可能的场景,只是因为它是一个非常优雅和明确的答案。 I do hope there'll be some way that it'll work eventually. 我希望有一些方法可以最终发挥作用。

A possible approach is to unify the type of the two vectors before applying the operation. 一种可能的方法是在应用操作之前统一两个向量的类型。 By doing so, operations on Vector2[A] can alwyas take a Vector2[A ] as parameter. 通过这样做, Vector2[A]可以将Vector2[A ]作为参数。

A similar approach can be used for multiplication (see the example below). 类似的方法可用于乘法(参见下面的例子)。

Using an implicit conversion from Vector2[A] to Vector2[B] (provided that Numeric[A] and Numeric[B] both exist and that you have implicit evidence that A can be converted to B ), you can do: 使用从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(_))
}

examples: 例子:

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)

As per Chris' request in the comments, here's an example of how the implicit conversions chain work 根据Chris在评论中的请求,这里是隐式转换链如何工作的一个例子

If we run the scala REPL with scala -XPrint:typer , we can see the implicits at work explicitly For instance 如果我们使用scala -XPrint:typer运行scala REPL,我们可以看到显式工作中的implicits例如

z + x

becomes

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

which translated to more readable terms is 翻译成更易读的术语是

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

Conversely, x + z becomes 相反, x + z变为

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

The way it works is roughly this: 它的工作方式大致如下:

  1. we say z: V[Long] + x: V[Int] 我们说z: V[Long] + x: V[Int]
  2. the compiler sees that there's a method +[Long, Long] 编译器看到有一个方法+[Long, Long]
  3. it looks from a conversion from V[Int] to V[Long] 它看起来从V[Int]V[Long]
  4. it finds toV 它找到toV
  5. it looks for a conversion from Int to Long as required by toV 它根据toV要求寻找从IntLong的转换
  6. it finds Int.int2Long , ie a function Int => Long 它找到Int.int2Long ,即函数Int => Long
  7. it can then use toV[Int, Long] ie a function V[Int] => V[Long] 然后它可以使用toV[Int, Long]即函数V[Int] => V[Long]
  8. it does x + toV(z) 它做x + toV(z)

in case we do instead x: V[Int] + z: V[Long] 如果我们改为x: V[Int] + z: V[Long]

  1. the compiler sees that there's a method +[Int, Int] 编译器看到有一个方法+[Int, Int]
  2. it looks from a conversion from V[Long] to V[Int] 它看起来从V[Long]V[Int]
  3. it finds toV 它找到toV
  4. it looks for a conversion from Long to Int as required by toV 它根据toV要求寻找从LongInt的转换
  5. it can't find it! 它找不到它!
  6. it sees that there's a method +[Long, Long] 它看到有一种方法+[Long, Long]

and we're back to point 3 of the previous example 我们回到上一个例子的第3点


Update 更新

As noticed in the comments, there's a problem when doing 正如评论中所注意到的,这样做时会出现问题

Vector(2.0, 1.0) * 2.0f

This is pretty much the issue: 这几乎是个问题:

2.0f * 3.0 // 6.0: Double

but also 但是也

2.0 * 3.0f // 6.0: Double

So it doesn't matter what's the argument, when mixing doubles and floats we always end up with a double. 所以这个论点是什么并不重要,当混合双打和花车时我们总是以双倍结束。 Unfortunately we're requiring evidence of A => B in order to convert the vector to the type of s , but sometimes we actually want to convert s to the type of the vector. 不幸的是,为了将向量转换为s的类型,我们需要A => B证据,但有时我们实际上想要将s转换为向量的类型。

We need to handle the two cases. 我们需要处理这两个案件。 The first naive approach could be 第一个天真的方法可能是

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

Neat, right? 干净吧? Too bad it doesn't work: scala does not consider implicit arguments when disambiguating overloaded methods. 太糟糕了它不起作用:在消除重载方法的歧义时,scala不考虑隐式参数。 We have to work around this, using the magnet pattern, as suggested 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))
  }
}

We have only one * method, and we inspect the implicit parameter ev to know whether we have to convert everything to the type of the vector or to the type of s . 我们只有一个*方法,我们检查隐式参数ev以了解我们是否必须将所有内容转换为向量的类型或s的类型。

The only drawback of this approach is the result type. 这种方法的唯一缺点是结果类型。 ev match { ... } returns something that it's supertype of B with A , and I still haven't found a workaround for it. ev match { ... }返回的东西是B with A的超类型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)

A few approaches come to mind: 我想到了一些方法:

  1. Use type classes, an example follows 使用类型类,如下所示
  2. Use Spire, a maths lib for Scala. 使用Spire,Scala的数学库。 A tutorial for vectors using spire can be found here . 可以在此处找到使用尖顶的矢量教程。
  3. Combine type classes with Shapeless to support vectors of any dimension. 将类型类与Shapeless组合以支持任何维度的向量。 Read Shapeless' support for 'abstracting over arity'. 阅读Shapeless'支持'抽象过度'。
  4. Convert the Vectors to the same type before calling the operations on the Vector. 在向Vector调用操作之前,将Vectors转换为相同的类型。 Gabriele Petronella has given a great example of doing this on Scala 2.10 or later using implicits supplied by the standard Scala library. Gabriele Petronella给出了一个很好的例子,可以使用标准Scala库提供的implicit在Scala 2.10或更高版本上执行此操作。

Using Type Classes directly: 直接使用类型类:

This approach is somewhat verbose the first time that you create it, as one has to create implicit classes for each combination of values that one wants to support. 这种方法在您第一次创建时有点冗长,因为必须为每个想要支持的值组合创建隐式类。 But the approach is sound. 但这种方法很合理。 More details about type classes can be read here 有关类型类的更多详细信息,请参见此处

If you want to copy and paste the following code into the scala REPL, be sure to enter ':paste' first. 如果要将以下代码复制并粘贴到scala REPL中,请务必先输入':paste'。 Otherwise the relationship between the trait and the companion object will not be picked up and the implicit will not be found when one enters 'a+b'. 否则,将不会拾取特征与伴随对象之间的关系,并且当输入“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

To add more operators to the vector, like subtraction and divide then add them to the NumberLike trait and follow it through using the plus example above. 要向矢量添加更多运算符,例如减法和除法,然后将它们添加到NumberLike特征并使用上面的加号示例进行跟踪。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM