Generalizing Vector

I would like to generalize the Vector class to add some basic math features. I have done this for Vector[Scalar] (based on my Scalar class), a portion of which is shown below. How can I generalize this to also work for Vector[Int] and Vector[Double] without repeating the whole implementation? I am using Scala 3. Thanks.

implicit class Vectorx(self: Vector[Scalar]):
  // adds basic math operations to the Vector[Scalar] class

  import self._

  def apply(i: Int): Scalar = self(i)
  def unary_- = map(-_)

  protected def zip(v: Vector[Scalar]) = { check(v); self zip v }

  def + (v: Vector[Scalar]) = zip(v).map(t => t._1 + t._2)
  def - (v: Vector[Scalar]) = zip(v).map(t => t._1 - t._2)

  def * (s: Scalar) = map(_ * s)
  def / (s: Scalar) = map(_ / s)
  def % (s: Scalar) = map(_ % s)

  // more

Using a typeclas.
Numeric from the stdlib could be used or serve as inspiration.

1 Like

I’d still like to know how to generalize this implicit class to handle any numeric type that has basic mathematical operators such as +, -, *, /, etc. When I try to generalize it as

implicit class Vectorx[A](self: Vector[A]):

the compiler tells me that type A does not support those operators. I looked for a trait, but there does not seem to be one. Is there a way to limit the type of A to a discrete set of types, such as Int, Long, Float, Double, etc.? I am using Scala 3. Thanks.

You could specify a subtype for each primitive numeric type, and maybe one for BigInt. There aren’t that many, and maybe you don’t need them all? Maybe Double and Long or BigInt is all you need?

I used to think, in the past, that there should be a trait Number, which provides basic math operators, and from which all primitive numbers inherit.

I have since changed my mind, since the practical use of such a super-type is very limited.

Although all the numbers have basic math, they behave differently. Division by zero throws an exception for integers, but returns a special value for floating points. And 3/2 is very different from 3.0/2.0.

Overflow is different. Integers wrap around, while floating points result in special values.

So, what’s left? Matrix multiplication can be written the same for integers and floating points, but for integers, you can run into overflows very quickly, so maybe you will need a BigInt anyway? For more serious linear algebra, like matrix inversion or finding eigenvalues, I don’t think one can use the same algorithm for integers as for floating points.

So, you can try to generalize over all primitive numeric types, but it may be a lot of work for relatively little gain.

1 Like

Thanks for the reply. The generalized numeric Vector is not something I really need. I was really just curious about it and wondering if I was missing some simple little trick.

I’m not sure what you mean by “specify a subtype for each primitive numeric type.” Can you elaborate on that? You are right that I don’t need very many. I’d settle for Int, Long, Bigint, Float, Double, and perhaps BigDecimal.

I meant something like:

trait Vectorx[A]
implicit class VectorxLong(self: Vector[Long]) extends Vectorx[Long]
etc

If you really want this, in spite of the drawbacks and limitations @curoli has listed, @BalmungSan’s advice sounds just right. Based on Numeric (in Scala 2):

implicit class NumVector[T](v: Vector[T])(implicit num: Numeric[T]) {
  def +(o: Vector[T]): Vector[T] = v.zip(o).map((num.plus _).tupled)
  def -(o: Vector[T]): Vector[T] = v.zip(o).map((num.minus _).tupled)
  def *(o: Vector[T]): Vector[T] = v.zip(o).map((num.times _).tupled)
  def unary_- : Vector[T] = v.map(num.negate)
  // ...
}
1 Like

I was hoping somebody would bring up Numeric so I wouldn’t have to :slight_smile:

A few caveats about Numeric:

  • It wasn’t created as a full-on effort to really fully support abstracting across numeric types. It’s more a minimal thing that was made to support collections methods such as .sum, .product, and .mean. So it has basic stuff but depending on what you’re doing you may hit a wall.
  • In high-performance contexts where you’re crunching tons of data and every cycle and every byte counts, routing operations through the typeclass might be unacceptable performancewise. Of course in many applications it doesn’t matter at all.
3 Likes

It is usually better to put the implicit argument in each method so the whole implicit class can be a value class.

Thankfully, the new syntax for extension methods in Scala 3 removes the need for such boilerplate.

1 Like

Also, I would like to point out that it doesn’t support division and if you want that you have to choose between Integral and Fractional.

BTW,

routing operations through the typeclass might be unacceptable performancewise

Just out of curiosity, shouldn’t the JIT just inline all the indirections?

Uh, maybe? Not sure if anybody has benchmarked that. I probably should have used some more cautious wording like “consider a possible performance impact, depending”. Would love to learn that I was too pessimistic on this point.

1 Like