For this kind of problem, shapeless is very often the right thing to turn to.
Shapeless already has type-level numbers (shapless.Nat
) and an abstraction for a collection with a compile-time known size (shapeless.Sized
).
A first take at an implementation could look something like this
import shapeless.{ Sized, Nat }
import shapeless.ops.nat.ToInt
import shapeless.syntax.sized._
def Vect[A](n: Nat)(xs: A*)(implicit toInt : ToInt[n.N]) =
xs.toVector.sized(n).get
def add[A, N <: Nat](left: Sized[Vector[A], N], right: Sized[Vector[A], N])(implicit A: Numeric[A]) =
Sized.wrap[Vector[A], N]((left, right).zipped.map(A.plus))
And its usage:
scala> add(Vect(3)(1, 2, 3), Vect(3)(4, 5, 6))
res0: shapeless.Sized[Vector[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]] = Vector(5, 7, 9)
scala> add(Vect(3)(1, 2, 3), Vect(1)(1))
<console>:30: error: type mismatch;
// long and misleading error message about variance
// but at least it failed to compile
While this looks like it works, it a serious downside – you have to make sure that the provided length and the number of arguments match, otherwise you'll get a runtime error.
scala> Vect(1)(1, 2, 3)
java.util.NoSuchElementException: None.get
at scala.None$.get(Option.scala:347)
at scala.None$.get(Option.scala:345)
at .Vect(<console>:27)
... 33 elided
We can do better than this. You can use Sized
directly instead of another constructor.
Also, if we define add
with two parameter lists, we can get a better error message (It's not as nice as what Idris offers, but it's usable):
import shapeless.{ Sized, Nat }
def add[A, N <: Nat](left: Sized[IndexedSeq[A], N])(right: Sized[IndexedSeq[A], N])(implicit A: Numeric[A]) =
Sized.wrap[IndexedSeq[A], N]((left, right).zipped.map(A.plus))
// ...
add(Sized(1, 2, 3))(Sized(4, 5, 6))
res0: shapeless.Sized[IndexedSeq[Int],shapeless.nat._3] = Vector(5, 7, 9)
scala> add(Sized(1, 2, 3))(Sized(1))
<console>:24: error: polymorphic expression cannot be instantiated to expected type;
found : [CC[_]]shapeless.Sized[CC[Int],shapeless.nat._1]
(which expands to) [CC[_]]shapeless.Sized[CC[Int],shapeless.Succ[shapeless._0]]
required: shapeless.Sized[IndexedSeq[Int],shapeless.nat._3]
(which expands to) shapeless.Sized[IndexedSeq[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]]
add(Sized(1, 2, 3))(Sized(1))
But we can go further. Shapeless also offers a conversion between tuples and Sized
, so we could write:
import shapeless.{ Sized, Nat }
import shapeless.ops.tuple.ToSized
def Vect[A, P <: Product](xs: P)(implicit toSized: ToSized[P, Vector]) =
toSized(xs)
def add[A, N <: Nat](left: Sized[Vector[A], N], right: Sized[Vector[A], N])(implicit A: Numeric[A]) =
Sized.wrap[Vector[A], N]((left, right).zipped.map(A.plus))
And this works, the size is inferred from the provided tuple:
scala> add(Vect(1, 2, 3), Vect(4, 5, 6))
res0: shapeless.Sized[Vector[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]] = Vector(5, 7, 9)
scala> add(Vect(1, 2, 3))(Vect(1))
<console>:27: error: type mismatch;
found : shapeless.Sized[scala.collection.immutable.Vector[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]]]
required: shapeless.Sized[Vector[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]]
add(Vect(1, 2, 3))(Vect(4, 5, 6, 7))
Unfortunately, the syntax from the example only works due to a feature called argument adaption, where scalac would automatically convert the multiple arguments from Vect
into the tuple we needed. As this "feature" can also lead to very nasty bugs, I find myself disabling it almost always with -Yno-adapted-args
. Using this flag, we have to explicitly write the tuple ourselves:
scala> Vect(1, 2, 3)
<console>:26: warning: No automatic adaptation here: use explicit parentheses.
signature: Vect[A, P <: Product](xs: P)(implicit toSized: shapeless.ops.tuple.ToSized[P,Vector]): toSized.Out
given arguments: 1, 2, 3
after adaptation: Vect((1, 2, 3): (Int, Int, Int))
Vect(1, 2, 3)
^
<console>:26: error: too many arguments for method Vect: (xs: (Int, Int, Int))(implicit toSized: shapeless.ops.tuple.ToSized[(Int, Int, Int),Vector])toSized.Out
Vect(1, 2, 3)
^
scala> Vect((1, 2, 3))
res1: shapeless.Sized[scala.collection.immutable.Vector[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]] = Vector(1, 2, 3)
scala> add(Vect((1, 2, 3)))(Vect((4, 5, 6)))
res2: shapeless.Sized[Vector[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]] = Vector(5, 7, 9)
Also, we could only use length up to 22, scala has no support for larger tuples.
scala> Vect((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23))
<console>:26: error: object <none> is not a member of package scala
Vect((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23))
So, can we get the slightly nicer syntax? Turns out, we can. Shapeless can do the wrapping for us, using an HList instead:
import shapeless.ops.hlist.ToSized
import shapeless.{ ProductArgs, HList, Nat, Sized }
object Vect extends ProductArgs {
def applyProduct[L <: HList](l: L)(implicit toSized: ToSized[L, Vector]) =
toSized(l)
}
def add[A, N <: Nat](left: Sized[Vector[A], N])(right: Sized[Vector[A], N])(implicit A: Numeric[A]) =
Sized.wrap[Vector[A], N]((left, right).zipped.map(A.plus))
And it works:
scala> add(Vect(1, 2, 3))(Vect(4, 5, 6))
res0: shapeless.Sized[Vector[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]] = Vector(5, 7, 9)
scala> add(Vect(1, 2, 3))(Vect(1))
<console>:27: error: type mismatch;
found : shapeless.Sized[scala.collection.immutable.Vector[Int],shapeless.Succ[shapeless._0]]
required: shapeless.Sized[Vector[Int],shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]]
add(Vect(1, 2, 3))(Vect(1))
^
scala> Vect(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
res2: shapeless.Sized[scala.collection.immutable.Vector[Int],shapeless.Succ[shapeless.Succ[... long type elided... ]]] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
You could go further from there, wrapping the Sized
in your own class for example
import shapeless.ops.hlist.ToSized
import shapeless.{ ProductArgs, HList, Nat, Sized }
object Vect extends ProductArgs {
def applyProduct[L <: HList](l: L)(implicit toSized: ToSized[L, Vector]): Vect[toSized.Lub, toSized.N] =
new Vect(toSized(l))
}
class Vect[A, N <: Nat] private (val self: Sized[Vector[A], N]) extends Proxy.Typed[Sized[Vector[A], N]] {
def add(other: Vect[A, N])(implicit A: Numeric[A]): Vect[A, N] =
new Vect(Sized.wrap[Vector[A], N]((self, other.self).zipped.map(A.plus)))
}
// ...
scala> Vect(1, 2, 3) add Vect(4, 5, 6)
res0: Vect[Int,shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]] = Vector(5, 7, 9)
scala> Vect(1, 2, 3) add Vect(1)
<console>:26: error: type mismatch;
found : Vect[Int,shapeless.Succ[shapeless._0]]
required: Vect[Int,shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]]
Vect(1, 2, 3) add Vect(1)
Essentially, all boils down to using Sized
and Nat
for the type constraints.