我正在尝试在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的答案适用于所有可能的场景,只是因为它是一个非常优雅和明确的答案。我希望有一些方法可以最终发挥作用。
答案 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
它的工作方式大致如下:
z: V[Long] + x: V[Int]
+[Long, Long]
V[Int]
到V[Long]
toV
Int
Long
到toV
的转换
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
Long
Int
到toV
的转换
+[Long, Long]
我们回到上一个例子的第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)
我想到了一些方法:
直接使用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特征,并使用上面的加号示例进行跟踪。