使用无形的任何维度的Scala矢量

Cau*_*chy 9 math scala vector shapeless

我需要测量n维欧几里得空间的距离,所以我必须要创建多维向量,并能够比较它们的尺寸和执行像"+"或一些基本操作" - ".所以,我想我会使用类型类+无形,如下所示:

在Scala中实现通用Vector

但是在投入了大量时间之后,我仍然无法理解如何实现这一点.我的意思是,我可以理解类型类背后的想法可以使用它们,但不知道如何对它们应用无形.提前感谢任何帮助,比如至少最简单的示例,展示如何使用无形的类型类.

Tra*_*own 30

第一步是查找或定义包含您要支持的操作的类型类.scala.math.Numeric是一种可能性 - 它提供了加法,减法等,但事实上它还需要转换和转换,例如Int意味着它可能不是正确的选择.代数Spire等项目包括更合适的候选人.

定义类型类

为了简单起见,我们可以定义自己的:

trait VectorLike[A] {
  def dim: Int
  def add(x: A, y: A): A
  def subtract(x: A, y: A): A
}
Run Code Online (Sandbox Code Playgroud)

(请注意,我使用Like后缀来避免与集合库的冲突Vector.这是您有时会看到的命名约定,但在任何情况下都不需要在Scala中使用类型类 - 在此上下文中更抽象数学名称喜欢MonoidGroup更常见.)

定义实例

接下来,我们可以为双精度二维矢量定义一个类型类实例,例如:

implicit val doubleVector2D: VectorLike[(Double, Double)] =
  new VectorLike[(Double, Double)] {
    def dim: Int = 2
    def add(x: (Double, Double), y: (Double, Double)): (Double, Double) =
      (x._1 + y._1, x._2 + y._2)
    def subtract(x: (Double, Double), y: (Double, Double)): (Double, Double) =
      (x._1 - y._1, x._2 - y._2)
  }
Run Code Online (Sandbox Code Playgroud)

现在我们可以像这样使用这个实例:

scala> implicitly[VectorLike[(Double, Double)]].add((0.0, 0.0), (1.0, 1.0))
res0: (Double, Double) = (1.0,1.0)
Run Code Online (Sandbox Code Playgroud)

这非常冗长,但它确实有效.

隐含的操作类

定义一个隐式的"ops"类通常很方便,使它看起来像一个带有类型类实例的类型的值具有从类类操作派生的方法:

implicit class VectorLikeOps[A: VectorLike](wrapped: A) {
  def dim: Int = implicitly[VectorLike[A]].dim
  def |+|(other: A): A = implicitly[VectorLike[A]].add(wrapped, other)
  def |-|(other: A): A = implicitly[VectorLike[A]].subtract(wrapped, other)
}
Run Code Online (Sandbox Code Playgroud)

现在您可以编写以下内容:

scala> (0.0, 0.0) |-| (1.0, 1.0)
res1: (Double, Double) = (-1.0,-1.0)

scala> (0.0, 0.0) |+| (1.0, 1.0)
res2: (Double, Double) = (1.0,1.0)
Run Code Online (Sandbox Code Playgroud)

这不是必需的 - 它只是您经常看到的一种方便的模式.

通用实例

虽然我们的doubleVector2D实例有效,但为每种数字类型定义这些实例都很烦人且是样板文件.我们可以通过使用以下方法为任何二维数字类型向量提供实例来改善这种情况scala.math.Numeric:

implicit def numericVector2D[A](implicit A: Numeric[A]): VectorLike[(A, A)] =
  new VectorLike[(A, A)] {
    def dim: Int = 2
    def add(x: (A, A), y: (A, A)): (A, A) = 
      (A.plus(x._1, y._1), A.plus(x._2, y._2))
    def subtract(x: (A, A), y: (A, A)): (A, A) = 
      (A.minus(x._1, y._1), A.minus(x._2, y._2))
  }
Run Code Online (Sandbox Code Playgroud)

请注意,我给Numeric类型类实例的名称与泛型类型(A)相同.对于类型需要单个类型类实例的方法,这是常见的约定,但根本不需要 - 我们可以调用它numericA或我们想要的任何其他类型.

现在我们可以在任何类型的元组上使用我们的运算符Numeric:

scala> (1, 2) |+| (3, 4)
res3: (Int, Int) = (4,6)
Run Code Online (Sandbox Code Playgroud)

这是一个很大的改进,但它仍然只适用于单个矢量大小.

使用Shapeless派生实例

我们还没有见过任何Shapeless,但现在我们想要抽象出元组,这正是我们所需要的.我们可以重写我们的通用实例来处理任意数值类型的元组.我们有很多方法可以写这个,但以下是我的开始:

import shapeless._

trait HListVectorLike[L <: HList] extends VectorLike[L] { type El }

object HListVectorLike {
  type Aux[L <: HList, A] = HListVectorLike[L] { type El = A }

  implicit def vectorLikeHNil[A]: Aux[HNil, A] =
    new HListVectorLike[HNil] {
      type El = A
      def dim: Int = 0
      def add(x: HNil, y: HNil): HNil = HNil
      def subtract(x: HNil, y: HNil): HNil = HNil
    }

  implicit def vectorLikeHCons[T <: HList, A](implicit
    numeric: Numeric[A],
    instT: Aux[T, A]
  ): Aux[A :: T, A] = new HListVectorLike[A :: T] {
    type El = A
    def dim: Int = instT.dim + 1
    def add(x: A :: T, y: A :: T): A :: T =
      numeric.plus(x.head, y.head) :: instT.add(x.tail, y.tail)
    def subtract(x: A :: T, y: A :: T): A :: T =
      numeric.minus(x.head, y.head) :: instT.subtract(x.tail, y.tail)
  }
}

implicit def numericVector[P, Repr <: HList](implicit
  gen: Generic.Aux[P, Repr],
  inst: HListVectorLike[Repr]
): VectorLike[P] = new VectorLike[P] {
  def dim: Int = inst.dim
  def add(x: P, y: P): P = gen.from(inst.add(gen.to(x), gen.to(y)))
  def subtract(x: P, y: P): P = gen.from(inst.subtract(gen.to(x), gen.to(y)))
}
Run Code Online (Sandbox Code Playgroud)

这看起来很复杂,但是,无论何时使用Shapeless进行泛型推导,基本模式都是你会看到的.我不会在这里详细描述正在发生的事情,但请参阅我的博客文章,以便讨论类似的例子.

现在我们可以这样写:

scala> (1, 2, 3, 4) |+| (5, 6, 7, 8)
res1: (Int, Int, Int, Int) = (6,8,10,12)
Run Code Online (Sandbox Code Playgroud)

或这个:

scala> (0.0, 0.0, 0.0, 0.0, 0.0, 0.0) |-| (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
res4: (Double, Double, Double, Double, Double, Double) = (-1.0,-2.0,-3.0,-4.0,-5.0,-6.0)
Run Code Online (Sandbox Code Playgroud)

它只是有效.

其他矢量表示

我一直在使用元组来表示上面所有例子中的多维向量,但你也可以使用Shapeless's Sized,它是一个同类集合,在类型级别对其长度进行编码.您可以为元组提供VectorLike实例,Sized而不是(或除了)元组实例,而不对其VectorLike自身进行任何更改.

您应该选择哪种表示取决于许多因素.元组很容易编写,并且对于大多数Scala开发人员来说看起来很自然,但是如果你需要超过22维的向量,它们就无法工作.

  • 谢谢你,先生.这正是我需要的可理解的解释.我现在尝试自己使用`Sized`. (2认同)
  • 您给出的实现不仅限于元组...它适用于任何产品(例如类似案例类型)...因此您可以使用具有超过22个元素的案例类来破坏Tuple22屏障. (2认同)
  • @MilesSabin是的,我几乎添加了一个关于它的部分.还有其他原因你可能更喜欢'Sized`,比如元组比我们需要的更具表现力(我们必须尽力将它们限制为同质),但答案已经很长了. (2认同)