In Idris, I can add two vectors of the same size via:
module MatrixMath
import Data.Vect
addHelper : (Num n) => Vect k n -> Vect k n -> Vect k n
addHelper = zipWith (+)
After compiling it on the REPL:
*MatrixMath> :l MatrixMath.idr
Type checking ./MatrixMath.idr
I can then call it with two vectors of size 3:
*MatrixMath> addHelper [1,2,3] [4,5,6]
[5, 7, 9] : Vect 3 Integer
But, it will fail to compile when I try to call addHelper
on two vectors of different size:
*MatrixMath> addHelper [1,2,3] [1]
(input):1:20:When checking argument xs to constructor Data.Vect.:::
Type mismatch between
Vect 0 a (Type of [])
and
Vect 2 n (Expected type)
Specifically:
Type mismatch between
0
and
2
How can I write this in Scala?
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.
Shapeless can you help you in that:
import shapeless._
import syntax.sized._
def row(cols : Seq[String]) = cols.mkString("\"", "\", \"", "\"")
def csv[N <: Nat](hdrs : Sized[Seq[String], N], rows : List[Sized[Seq[String], N]]) =
row(hdrs) :: rows.map(row(_))
val hdrs = Sized("Title", "Author") // Sized[IndexedSeq[String], _2]
val rows = List( // List[Sized[IndexedSeq[String], _2]]
Sized("Types and Programming Languages", "Benjamin Pierce"),
Sized("The Implementation of Functional Programming Languages", "Simon Peyton-Jones")
)
// hdrs and rows statically known to have the same number of columns
val formatted = csv(hdrs, rows)
Note how the method csv
constraints both Sized
by N <: Nat
, the same way in you example that you constraint by Num n
.
I copied this code from Shapeless examples, if it doesn't compile the way it is I might very well have missed something.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With