我正在尝试在Scala中实现一个通用(数学)向量,我遇到了一些如何正确执行它的问题:
1)你如何处理+和 - 这样操作a Vector[Int]和a Vector[Double]会返回Vector[Double]?简而言之,我将如何进行数字类型的自动推广(最好利用Scala的自动推广)?因为implicit n: Numeric[T]只有两个矢量的类型相同才能使用.
2)相关,我应该如何定义一个*操作,使它接受任何数值类型,并返回一个正确的数字类型的向量?也就是说,a 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)
}
Run Code Online (Sandbox Code Playgroud)
经过深思熟虑之后,我决定接受Chris K的答案,因为它适用于我所提出的所有情况,尽管类型类解决方案的冗长(Scala中的数字类型是Byte,Short,Int, Long,Float,Double,BigInt,BigDecimal,这使得在每个可能的类型对之间实现所有操作非常有趣.
我赞同这两个答案,因为它们都是很好的答案.我真的希望Gabriele Petronella的答案适用于所有可能的场景,只是因为它是一个非常优雅和明确的答案.我希望有一些方法可以最终发挥作用.
一种可能的方法是在应用操作之前统一两个向量的类型.通过这样做,对Vector2[A]alwyas的操作可以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(_))
}
Run Code Online (Sandbox Code Playgroud)
例子:
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)
Run Code Online (Sandbox Code Playgroud)
根据Chris在评论中的请求,这里是隐式转换链如何工作的一个例子
如果我们运行scala REPL scala -XPrint:typer,我们可以明确地看到implicits工作.例如
z + x
Run Code Online (Sandbox Code Playgroud)
变
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))
}));
Run Code Online (Sandbox Code Playgroud)
翻译成更易读的术语是
val res: Vector2[Long] = z + toV[Int, Long](x){ i: Int => Int.int2long(i) }
^____________________________________________^
the result of this is a Vector[Long]
Run Code Online (Sandbox Code Playgroud)
相反,x + z成为
val res: Vector2[Long] = toV[Int, Long](x){ i: Int => Int.int2long(i) } + z
Run Code Online (Sandbox Code Playgroud)
它的工作方式大致如下:
z: V[Long] + x: V[Int]+[Long, Long]V[Int]到V[Long]toVInt会Long根据需要寻找转换toVInt.int2Long,即一个功能Int => LongtoV[Int, Long]即函数V[Int] => V[Long]x + toV(z)如果我们改为 x: V[Int] + z: V[Long]
+[Int, Int]V[Long]到V[Int]toVLong会Int根据需要寻找转换toV+[Long, Long] 我们回到上一个例子的第3点
正如评论中所注意到的,这样做时会出现问题
Vector(2.0, 1.0) * 2.0f
Run Code Online (Sandbox Code Playgroud)
这几乎是个问题:
2.0f * 3.0 // 6.0: Double
Run Code Online (Sandbox Code Playgroud)
但是也
2.0 * 3.0f // 6.0: Double
Run Code Online (Sandbox Code Playgroud)
所以这个论点是什么并不重要,当混合双打和花车时,我们总是以双倍结束.不幸的是,我们需要证据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
Run Code Online (Sandbox Code Playgroud)
干净吧?太糟糕了它不起作用:在消除重载方法的歧义时,scala不考虑隐式参数.我们必须使用磁铁模式解决这个问题,如此处所示.
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))
}
}
Run Code Online (Sandbox Code Playgroud)
我们只有一个*方法,我们检查隐式参数,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)
Run Code Online (Sandbox Code Playgroud)
我想到了一些方法:
直接使用类型类:
这种方法在您第一次创建时有点冗长,因为必须为每个想要支持的值组合创建隐式类.但这种方法很合理.有关类型类的更多详细信息,请参见此处
如果要将以下代码复制并粘贴到scala REPL中,请务必先输入':paste'.否则,将不会拾取特征与伴随对象之间的关系,并且当输入"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
Run Code Online (Sandbox Code Playgroud)
要向矢量添加更多运算符,例如减法和除法,然后将它们添加到NumberLike特征并使用上面的加号示例进行跟踪.