使用 shapeless 的任何维度的 Scala 向量
Scala vector of any dimension using shapeless
我需要测量 n 维欧氏距离 space,因此我必须创建多维向量并能够比较它们的维度并执行一些基本操作,如“+”或“-”。所以,我想我会使用 type 类 + shapeless ,如下所示:
Implementing a generic Vector in Scala
但是在投入了很多时间之后,我仍然不明白如何实现它。我的意思是,我可以理解 type 类 背后的想法并可以使用它们,但不知道如何将 shapeless 应用于它们。提前感谢您提供的任何帮助,至少是最简单的示例,展示了如何将类型 类 与 shapeless 一起使用。
第一步是找到或定义一个类型 class,其中包含您要支持的操作。 scala.math.Numeric
is one possibility—it provides addition, subtraction, etc., but the fact that it also requires conversions to and from e.g. Int
means it's probably not the right choice here. Projects like algebra and Spire 包括更多合适的候选人。
定义类型class
为了简单起见,我们可以自己定义:
trait VectorLike[A] {
def dim: Int
def add(x: A, y: A): A
def subtract(x: A, y: A): A
}
(请注意,我使用 Like
后缀以避免与集合库的 Vector
冲突。这是您有时会看到的命名约定,但绝不是在 Scala 中使用类型 classes 的要求——在这种情况下,更抽象的数学名称如 Monoid
或 Group
更常见。)
定义实例
接下来我们可以为double的二维向量定义一个类型class实例,例如:
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)
}
现在我们可以像这样使用这个实例了:
scala> implicitly[VectorLike[(Double, Double)]].add((0.0, 0.0), (1.0, 1.0))
res0: (Double, Double) = (1.0,1.0)
这很冗长,但它确实有效。
隐式操作 classes
定义隐式 "ops" class 使其看起来像具有类型 class 实例的类型的值具有从类型 [=] 派生的方法通常很方便91=]的操作:
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)
}
现在你可以这样写了:
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)
这不是必需的 - 它只是您经常会看到的一种方便的模式。
通用实例
虽然我们的 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))
}
请注意,我已为 Numeric
类型 class 实例指定了与通用类型 (A
) 相同的名称。这是方法的常见约定,其中类型需要单个类型 class 实例,但根本不是必需的——我们可以将其命名为 numericA
或我们想要的任何其他名称。
现在我们可以在具有 Numeric
实例的任何类型的元组上使用我们的运算符:
scala> (1, 2) |+| (3, 4)
res3: (Int, Int) = (4,6)
这是一个很大的改进,但它仍然只针对单个矢量大小。
使用 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)))
}
这看起来很复杂,确实如此,但是基本模式是您在使用 Shapeless 进行泛型推导时随时会看到的东西。我不会在这里详细描述发生了什么,但请参阅例如我的博客 post here 讨论了一个类似的例子。
现在我们可以这样写:
scala> (1, 2, 3, 4) |+| (5, 6, 7, 8)
res1: (Int, Int, Int, Int) = (6,8,10,12)
或者这个:
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)
而且效果很好。
其他矢量表示法
在上面的所有示例中,我一直使用元组来表示多维向量,但您也可以使用 Shapeless 的 Sized
,这是一个在类型级别对其长度进行编码的同类集合。您可以为 Sized
类型提供 VectorLike
实例,而不是(或除此之外)元组实例,而无需对 VectorLike
本身进行任何更改。
您应该选择哪种表示取决于多种因素。元组很容易编写,并且对于大多数 Scala 开发人员来说看起来很自然,但是如果您需要超过 22 维的向量,它们将无法工作。
我需要测量 n 维欧氏距离 space,因此我必须创建多维向量并能够比较它们的维度并执行一些基本操作,如“+”或“-”。所以,我想我会使用 type 类 + shapeless ,如下所示:
Implementing a generic Vector in Scala
但是在投入了很多时间之后,我仍然不明白如何实现它。我的意思是,我可以理解 type 类 背后的想法并可以使用它们,但不知道如何将 shapeless 应用于它们。提前感谢您提供的任何帮助,至少是最简单的示例,展示了如何将类型 类 与 shapeless 一起使用。
第一步是找到或定义一个类型 class,其中包含您要支持的操作。 scala.math.Numeric
is one possibility—it provides addition, subtraction, etc., but the fact that it also requires conversions to and from e.g. Int
means it's probably not the right choice here. Projects like algebra and Spire 包括更多合适的候选人。
定义类型class
为了简单起见,我们可以自己定义:
trait VectorLike[A] {
def dim: Int
def add(x: A, y: A): A
def subtract(x: A, y: A): A
}
(请注意,我使用 Like
后缀以避免与集合库的 Vector
冲突。这是您有时会看到的命名约定,但绝不是在 Scala 中使用类型 classes 的要求——在这种情况下,更抽象的数学名称如 Monoid
或 Group
更常见。)
定义实例
接下来我们可以为double的二维向量定义一个类型class实例,例如:
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)
}
现在我们可以像这样使用这个实例了:
scala> implicitly[VectorLike[(Double, Double)]].add((0.0, 0.0), (1.0, 1.0))
res0: (Double, Double) = (1.0,1.0)
这很冗长,但它确实有效。
隐式操作 classes
定义隐式 "ops" class 使其看起来像具有类型 class 实例的类型的值具有从类型 [=] 派生的方法通常很方便91=]的操作:
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)
}
现在你可以这样写了:
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)
这不是必需的 - 它只是您经常会看到的一种方便的模式。
通用实例
虽然我们的 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))
}
请注意,我已为 Numeric
类型 class 实例指定了与通用类型 (A
) 相同的名称。这是方法的常见约定,其中类型需要单个类型 class 实例,但根本不是必需的——我们可以将其命名为 numericA
或我们想要的任何其他名称。
现在我们可以在具有 Numeric
实例的任何类型的元组上使用我们的运算符:
scala> (1, 2) |+| (3, 4)
res3: (Int, Int) = (4,6)
这是一个很大的改进,但它仍然只针对单个矢量大小。
使用 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)))
}
这看起来很复杂,确实如此,但是基本模式是您在使用 Shapeless 进行泛型推导时随时会看到的东西。我不会在这里详细描述发生了什么,但请参阅例如我的博客 post here 讨论了一个类似的例子。
现在我们可以这样写:
scala> (1, 2, 3, 4) |+| (5, 6, 7, 8)
res1: (Int, Int, Int, Int) = (6,8,10,12)
或者这个:
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)
而且效果很好。
其他矢量表示法
在上面的所有示例中,我一直使用元组来表示多维向量,但您也可以使用 Shapeless 的 Sized
,这是一个在类型级别对其长度进行编码的同类集合。您可以为 Sized
类型提供 VectorLike
实例,而不是(或除此之外)元组实例,而无需对 VectorLike
本身进行任何更改。
您应该选择哪种表示取决于多种因素。元组很容易编写,并且对于大多数 Scala 开发人员来说看起来很自然,但是如果您需要超过 22 维的向量,它们将无法工作。