Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding Two Lists of Same Size at Compile-time [duplicate]

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?

like image 436
Kevin Meredith Avatar asked Jan 20 '16 01:01

Kevin Meredith


2 Answers

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.

like image 90
knutwalker Avatar answered Sep 24 '22 17:09

knutwalker


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.

like image 32
pedrofurla Avatar answered Sep 26 '22 17:09

pedrofurla