9

I need to measure distance in n-dimensional euclidean space, so I have to create multidimensional vectors and to be able to compare their dimensions and perform some basic operations like '+' or '-'. So, I thought I'll use type classes + shapeless as shown here:

Implementing a generic Vector in Scala

But after investing much time into this I still cannot understand how to implement this. I mean, I can understand idea that stands behind type classes and can use them, but have no idea how to apply shapeless to them. Thanks in advance for any help, like at least the simplest example showing how to use type classes with shapeless.

Community
  • 1
  • 1
Cauchy
  • 117
  • 6
  • The link in the question is about generic numeric types, It seems you want Shapeless' `Sized` (https://github.com/milessabin/shapeless/blob/master/examples/src/main/scala/shapeless/examples/sized.scala), which are list with type-constrained sizes, since you talk about dimensions. Is that right? It's also possible you may need both, still these would be orthogonal concerns. – pedrofurla Feb 25 '16 at 18:26
  • Thanks you for answer. `Sized` might be what I am looking for, but I need some time to get how it works. I will try to use it. – Cauchy Feb 25 '16 at 19:09

1 Answers1

30

The first step is either to find or define a type class with the operations you want to support. 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 include more appropriate candidates.

Defining the type class

To keep things simple we can just define our own:

trait VectorLike[A] {
  def dim: Int
  def add(x: A, y: A): A
  def subtract(x: A, y: A): A
}

(Note that I'm using the Like suffix to avoid a collision with the collection library's Vector. This is a naming convention you'll see sometimes, but it's not by any means a requirement for working with type classes in Scala—in this context more abstract mathematical names like Monoid or Group are more common.)

Defining instances

Next we can define a type class instance for a two-dimensional vector of doubles, for example:

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

And now we can use this instance like this:

scala> implicitly[VectorLike[(Double, Double)]].add((0.0, 0.0), (1.0, 1.0))
res0: (Double, Double) = (1.0,1.0)

This is pretty verbose, but it works.

Implicit ops classes

It's often convenient to define an implicit "ops" class to make it look like values of a type with a type class instance have methods that are derived from the type class's operations:

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

Now you can write the following:

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)

This isn't necessary—it's just a convenient pattern you'll often see.

Generic instances

While our doubleVector2D instance works, defining these instances for every numeric type is annoying and boilerplate-y. We can improve the situation by providing instances for any two-dimensional vector of numeric types using 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))
  }

Note that I've given the Numeric type class instance the same name as the generic type (A). This is a common convention for methods where a single type class instance is required for a type, but not at all necessary—we could have called it numericA or anything else we wanted.

And now we can use our operators on any tuple of types with a Numeric instance:

scala> (1, 2) |+| (3, 4)
res3: (Int, Int) = (4,6)

This is a big improvement, but it's still only for a single vector size.

Deriving instances with Shapeless

We've not seen any Shapeless yet, but now that we want to abstract over tuple arity, it's exactly what we need. We can rewrite our generic instance to work with arbitrary tuples of numeric types. There are a number of ways we could write this, but the following is how I'd start:

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

This looks complex, and it is, but the basic pattern is something you'll see any time you're using Shapeless for generic derivation. I won't describe what's going on in detail here, but see e.g. my blog post here for discussion of a similar example.

Now we can write something like this:

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

Or this:

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)

And it just works.

Other vector representations

I've been using tuples to represent multi-dimensional vectors in all of the examples above, but you could also use Shapeless's Sized, which is a homogeneous collection that encodes its length at the type level. You could provide VectorLike instances for Sized types instead of (or in addition to) the tuple instances, without making any changes to VectorLike itself.

Which representation you should choose depends on a number of factors. Tuples are easy to write and will look natural to most Scala developers, but if you need vectors with more than 22 dimensions, they're not going to work.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • 2
    Thank you, sir. That is exactly what I needed with understandable explanation. I will now try to use `Sized` by myself. – Cauchy Feb 25 '16 at 19:40
  • 2
    The implementation you've given isn't restricted to tuples ... it works with any product (ie. case class like) type ... so you could break the Tuple22 barrier using a case class with more than 22 elements. – Miles Sabin Feb 25 '16 at 22:55
  • 2
    @MilesSabin Yeah, I almost added a section about that. There are other reasons you might prefer `Sized`, like the fact that tuples are more expressive than we need here (we have to go out of our way to constrain them to be homogeneous), but the answer was already getting pretty long. – Travis Brown Feb 26 '16 at 14:58
  • Excuse me my lack of knowledge, but what does `_` stands for in `(x._1 + y._1, x._2 + y._2)`. I mean, I understand its usage here, but can't really recognize such a syntax. – Cauchy Feb 26 '16 at 16:01
  • 1
    @Cauchy—it's more or less a hack, and doesn't mean anything on its own. Tuples need positional accessors, but `1` isn't a valid method name, so the standard library designers picked `_1`. It could just as well have been `at1`, `first`, etc. – Travis Brown Feb 26 '16 at 16:05
  • So basically, it's only about naming a method and is up to me, right? – Cauchy Feb 26 '16 at 16:20
  • 1
    Well, if you're using the standard library's tuples you're stuck with their names, but if you're designing your own vector representation you can use whatever you like. – Travis Brown Feb 26 '16 at 16:24
  • @TravisBrown I have yet another question - what is this `Aux` in this snippet of code: `implicit def vectorLikeHNil[A]: Aux[HNil, A] =`? I am using IntelliJ and it says symbol cannot be resolved as it does now know which `Aux`, from which class should it be imported. – Cauchy Feb 27 '16 at 09:00
  • 1
    The `Aux` is a type alias defined just before that point—it's just a syntactic convenience. See my answer [here](http://stackoverflow.com/a/34548518/334519) for details. – Travis Brown Feb 27 '16 at 14:58