Skip to content

Add some tuple-related operations #7073

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/test/dotc/run-test-pickling.blacklist
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ t8905
t10889
i5257.scala
enum-java
tuple-ops.scala
31 changes: 31 additions & 0 deletions library/src/scala/Tuple.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ sealed trait Tuple extends Any {
inline def size[This >: this.type <: Tuple]: Size[This] =
DynamicTuple.dynamicSize(this)

/** Given two tuples, `(a1, ..., an)` and `(a1, ..., an)`, returns a tuple
* `((a1, b1), ..., (an, bn))`. If the two tuples have different sizes,
* the extra elements of the larger tuple will be disregarded.
* The result is typed as `((A1, B1), ..., (An, Bn))` if at least one of the
* tuple types has a `Unit` tail. Otherwise the result type is
* `(A1, B1) *: ... *: (Ai, Bi) *: Tuple`
*/
inline def zip[This >: this.type <: Tuple, T2 <: Tuple](t2: T2): Zip[This, T2] =
DynamicTuple.dynamicZip(this, t2)

/** Called on a tuple `(a1, ..., an)`, returns a new tuple `(f(a1), ..., f(an))`.
* The result is typed as `(F[A1], ..., F[An])` if the tuple type is fully known.
* If the tuple is of the form `a1 *: ... *: Tuple` (that is, the tail is not known
* to be the cons type.
*/
inline def map[F[_]](f: [t] => t => F[t]): Map[this.type, F] =
DynamicTuple.dynamicMap(this, f)
}

object Tuple {
Expand Down Expand Up @@ -74,6 +91,18 @@ object Tuple {
case h *: t => F[h] *: Map[t, F]
}

/** Given two tuples, `A1 *: ... *: An * At` and `B1 *: ... *: Bn *: Bt`
* where at least one of `At` or `Bt` is `Unit` or `Tuple`,
* returns the tuple type `(A1, B1) *: ... *: (An, Bn) *: Ct`
* where `Ct` is `Unit` if `At` or `Bt` is `Unit`, otherwise `Ct` is `Tuple`.
*/
type Zip[T1 <: Tuple, T2 <: Tuple] <: Tuple = (T1, T2) match {
case (h1 *: t1, h2 *: t2) => (h1, h2) *: Zip[t1, t2]
case (Unit, _) => Unit
case (_, Unit) => Unit
case _ => Tuple
}

/** Convert an array into a tuple of unknown arity and types */
def fromArray[T](xs: Array[T]): Tuple = {
val xs2 = xs match {
Expand All @@ -98,6 +127,8 @@ object Tuple {
def fromProduct(product: Product): Tuple =
runtime.DynamicTuple.dynamicFromProduct[Tuple](product)

def fromProductTyped[P <: Product](p: P) given (m: scala.deriving.Mirror.ProductOf[P]): m.MirroredElemTypes =
Tuple.fromArray(p.productIterator.toArray).asInstanceOf[m.MirroredElemTypes] // TODO use toIArray of Object to avoid double/triple array copy
}

/** Tuple of arbitrary non-zero arity */
Expand Down
18 changes: 13 additions & 5 deletions library/src/scala/runtime/DynamicTuple.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package scala.runtime

import scala.Tuple.Concat
import scala.Tuple.Size
import scala.Tuple.Head
import scala.Tuple.Tail
import scala.Tuple.Elem
import scala.Tuple.{ Concat, Size, Head, Tail, Elem, Zip, Map }

object DynamicTuple {
inline val MaxSpecialized = 22
Expand Down Expand Up @@ -253,6 +249,18 @@ object DynamicTuple {
res.asInstanceOf[Result]
}

def dynamicZip[This <: Tuple, T2 <: Tuple](t1: This, t2: T2): Zip[This, T2] = {
if (t1.size == 0 || t2.size == 0) ().asInstanceOf[Zip[This, T2]]
else Tuple.fromArray(
t1.asInstanceOf[Product].productIterator.zip(
t2.asInstanceOf[Product].productIterator).toArray // TODO use toIArray of Object to avoid double/triple array copy
).asInstanceOf[Zip[This, T2]]
}

def dynamicMap[This <: Tuple, F[_]](self: This, f: [t] => t => F[t]): Map[This, F] =
Tuple.fromArray(self.asInstanceOf[Product].productIterator.map(f(_)).toArray) // TODO use toIArray of Object to avoid double/triple array copy
.asInstanceOf[Map[This, F]]

def consIterator(head: Any, tail: Tuple): Iterator[Any] =
Iterator.single(head) ++ tail.asInstanceOf[Product].productIterator

Expand Down
8 changes: 8 additions & 0 deletions tests/neg/product-to-tuple.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
case class Foo(x: Int, y: String)

val x = Foo(1, "2")
val y: (Int, Boolean) = Tuple.fromProductTyped(x) // error

case class Bar[I <: Int, S <: String](x: I, y: S)
val a: Bar[1, "2"] = Bar(1, "2")
val b: (1, "3") = Tuple.fromProductTyped(a) // error
26 changes: 26 additions & 0 deletions tests/neg/tuple-ops.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
val a: (1, 2, 3) = (1, 2, 3)
val b: (4, 5, 6) = (4, 5, 6)
val c: (7, 8) = (7, 8)
val d: Unit = ()
val e: (1, "foo", 10.1) = (1, "foo", 10.1)

// Zip
val r1: ((1, 4), (2, 5), (6, 6)) = a.zip(b) // error
val r2: ((1, 7), (1, 8)) = a.zip(c) // error
val r3: ((2, 1), (8, 2)) = c.zip(a) // error

// Map
case class Foo[X](x: X)

val r6: (Int, Int, String) = a.map[[t] =>> Int]([t] => x: t => x match { // error
case x: Int => x * x
case _ => ???
})

val r7: ((1, Foo[1]), (2), (3, Foo[3])) =
a.map[[t] =>> (t, Foo[t])]( [t] => x: t => (x, Foo(x)) ) // error

// More Zip
val t1: Int *: Long *: Tuple = (1, 2l, 100, 200)
val t2: Int *: Char *: Tuple = (1, 'c', 33, 42)
val t3: (Int, Int) *: (Long, Long) *: Tuple = t1.zip(t2) // error
8 changes: 8 additions & 0 deletions tests/pos/product-to-tuple.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
case class Foo(x: Int, y: String)

val x = Foo(1, "2")
val y: (Int, String) = Tuple.fromProductTyped(x)

case class Bar[I <: Int, S <: String](x: I, y: S)
val a: Bar[1, "2"] = Bar(1, "2")
val b: (1, "2") = Tuple.fromProductTyped(a)
9 changes: 9 additions & 0 deletions tests/run/tuple-ops.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
((1,4),(2,5),(3,6))
((1,7),(2,8))
((7,1),(8,2))
()
()
(1,4,9)
((1,Foo(1)),(2,Foo(2)),(3,Foo(3)))
((1,1),(2,c),(100,33),(200,42))
()
32 changes: 32 additions & 0 deletions tests/run/tuple-ops.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
val a: (1, 2, 3) = (1, 2, 3)
val b: (4, 5, 6) = (4, 5, 6)
val c: (7, 8) = (7, 8)
val d: Unit = ()

// Zip
val r1: ((1, 4), (2, 5), (3, 6)) = a.zip(b)
val r2: ((1, 7), (2, 8)) = a.zip(c)
val r3: ((7, 1), (8, 2)) = c.zip(a)
val r4: Unit = d.zip(a)
val r5: Unit = a.zip(d)

// Map
case class Foo[X](x: X)

val r6: (Int, Int, Int) = a.map[[t] =>> Int]([t] => x: t => x match {
case x: Int => x * x
case _ => ???
})

val r7: ((1, Foo[1]), (2, Foo[2]), (3, Foo[3])) =
a.map[[t] =>> (t, Foo[t])]( [t] => x: t => (x, Foo(x)) )

// More Zip
val t1: Int *: Long *: Tuple = (1, 2l, 100, 200)
val t2: Int *: Char *: Tuple = (1, 'c', 33, 42)
val t3: (Int, Int) *: (Long, Char) *: Tuple = t1.zip(t2)

val t4: Unit = d.zip(d)

@main def Test =
List(r1, r2, r3, r4, r5, r6, r7, t3, t4).foreach(println)