Skip to content

Commit f8e1503

Browse files
Merge pull request #7073 from dotty-staging/tuple-ops-2
Add some tuple-related operations
2 parents 0314ab0 + 8e7ed32 commit f8e1503

File tree

8 files changed

+128
-5
lines changed

8 files changed

+128
-5
lines changed

compiler/test/dotc/run-test-pickling.blacklist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ t8905
1818
t10889
1919
i5257.scala
2020
enum-java
21+
tuple-ops.scala

library/src/scala/Tuple.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ sealed trait Tuple extends Any {
3333
inline def size[This >: this.type <: Tuple]: Size[This] =
3434
DynamicTuple.dynamicSize(this)
3535

36+
/** Given two tuples, `(a1, ..., an)` and `(a1, ..., an)`, returns a tuple
37+
* `((a1, b1), ..., (an, bn))`. If the two tuples have different sizes,
38+
* the extra elements of the larger tuple will be disregarded.
39+
* The result is typed as `((A1, B1), ..., (An, Bn))` if at least one of the
40+
* tuple types has a `Unit` tail. Otherwise the result type is
41+
* `(A1, B1) *: ... *: (Ai, Bi) *: Tuple`
42+
*/
43+
inline def zip[This >: this.type <: Tuple, T2 <: Tuple](t2: T2): Zip[This, T2] =
44+
DynamicTuple.dynamicZip(this, t2)
45+
46+
/** Called on a tuple `(a1, ..., an)`, returns a new tuple `(f(a1), ..., f(an))`.
47+
* The result is typed as `(F[A1], ..., F[An])` if the tuple type is fully known.
48+
* If the tuple is of the form `a1 *: ... *: Tuple` (that is, the tail is not known
49+
* to be the cons type.
50+
*/
51+
inline def map[F[_]](f: [t] => t => F[t]): Map[this.type, F] =
52+
DynamicTuple.dynamicMap(this, f)
3653
}
3754

3855
object Tuple {
@@ -74,6 +91,18 @@ object Tuple {
7491
case h *: t => F[h] *: Map[t, F]
7592
}
7693

94+
/** Given two tuples, `A1 *: ... *: An * At` and `B1 *: ... *: Bn *: Bt`
95+
* where at least one of `At` or `Bt` is `Unit` or `Tuple`,
96+
* returns the tuple type `(A1, B1) *: ... *: (An, Bn) *: Ct`
97+
* where `Ct` is `Unit` if `At` or `Bt` is `Unit`, otherwise `Ct` is `Tuple`.
98+
*/
99+
type Zip[T1 <: Tuple, T2 <: Tuple] <: Tuple = (T1, T2) match {
100+
case (h1 *: t1, h2 *: t2) => (h1, h2) *: Zip[t1, t2]
101+
case (Unit, _) => Unit
102+
case (_, Unit) => Unit
103+
case _ => Tuple
104+
}
105+
77106
/** Convert an array into a tuple of unknown arity and types */
78107
def fromArray[T](xs: Array[T]): Tuple = {
79108
val xs2 = xs match {
@@ -98,6 +127,8 @@ object Tuple {
98127
def fromProduct(product: Product): Tuple =
99128
runtime.DynamicTuple.dynamicFromProduct[Tuple](product)
100129

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

103134
/** Tuple of arbitrary non-zero arity */

library/src/scala/runtime/DynamicTuple.scala

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package scala.runtime
22

3-
import scala.Tuple.Concat
4-
import scala.Tuple.Size
5-
import scala.Tuple.Head
6-
import scala.Tuple.Tail
7-
import scala.Tuple.Elem
3+
import scala.Tuple.{ Concat, Size, Head, Tail, Elem, Zip, Map }
84

95
object DynamicTuple {
106
inline val MaxSpecialized = 22
@@ -253,6 +249,18 @@ object DynamicTuple {
253249
res.asInstanceOf[Result]
254250
}
255251

252+
def dynamicZip[This <: Tuple, T2 <: Tuple](t1: This, t2: T2): Zip[This, T2] = {
253+
if (t1.size == 0 || t2.size == 0) ().asInstanceOf[Zip[This, T2]]
254+
else Tuple.fromArray(
255+
t1.asInstanceOf[Product].productIterator.zip(
256+
t2.asInstanceOf[Product].productIterator).toArray // TODO use toIArray of Object to avoid double/triple array copy
257+
).asInstanceOf[Zip[This, T2]]
258+
}
259+
260+
def dynamicMap[This <: Tuple, F[_]](self: This, f: [t] => t => F[t]): Map[This, F] =
261+
Tuple.fromArray(self.asInstanceOf[Product].productIterator.map(f(_)).toArray) // TODO use toIArray of Object to avoid double/triple array copy
262+
.asInstanceOf[Map[This, F]]
263+
256264
def consIterator(head: Any, tail: Tuple): Iterator[Any] =
257265
Iterator.single(head) ++ tail.asInstanceOf[Product].productIterator
258266

tests/neg/product-to-tuple.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
case class Foo(x: Int, y: String)
2+
3+
val x = Foo(1, "2")
4+
val y: (Int, Boolean) = Tuple.fromProductTyped(x) // error
5+
6+
case class Bar[I <: Int, S <: String](x: I, y: S)
7+
val a: Bar[1, "2"] = Bar(1, "2")
8+
val b: (1, "3") = Tuple.fromProductTyped(a) // error

tests/neg/tuple-ops.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
val a: (1, 2, 3) = (1, 2, 3)
2+
val b: (4, 5, 6) = (4, 5, 6)
3+
val c: (7, 8) = (7, 8)
4+
val d: Unit = ()
5+
val e: (1, "foo", 10.1) = (1, "foo", 10.1)
6+
7+
// Zip
8+
val r1: ((1, 4), (2, 5), (6, 6)) = a.zip(b) // error
9+
val r2: ((1, 7), (1, 8)) = a.zip(c) // error
10+
val r3: ((2, 1), (8, 2)) = c.zip(a) // error
11+
12+
// Map
13+
case class Foo[X](x: X)
14+
15+
val r6: (Int, Int, String) = a.map[[t] =>> Int]([t] => x: t => x match { // error
16+
case x: Int => x * x
17+
case _ => ???
18+
})
19+
20+
val r7: ((1, Foo[1]), (2), (3, Foo[3])) =
21+
a.map[[t] =>> (t, Foo[t])]( [t] => x: t => (x, Foo(x)) ) // error
22+
23+
// More Zip
24+
val t1: Int *: Long *: Tuple = (1, 2l, 100, 200)
25+
val t2: Int *: Char *: Tuple = (1, 'c', 33, 42)
26+
val t3: (Int, Int) *: (Long, Long) *: Tuple = t1.zip(t2) // error

tests/pos/product-to-tuple.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
case class Foo(x: Int, y: String)
2+
3+
val x = Foo(1, "2")
4+
val y: (Int, String) = Tuple.fromProductTyped(x)
5+
6+
case class Bar[I <: Int, S <: String](x: I, y: S)
7+
val a: Bar[1, "2"] = Bar(1, "2")
8+
val b: (1, "2") = Tuple.fromProductTyped(a)

tests/run/tuple-ops.check

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
((1,4),(2,5),(3,6))
2+
((1,7),(2,8))
3+
((7,1),(8,2))
4+
()
5+
()
6+
(1,4,9)
7+
((1,Foo(1)),(2,Foo(2)),(3,Foo(3)))
8+
((1,1),(2,c),(100,33),(200,42))
9+
()

tests/run/tuple-ops.scala

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
val a: (1, 2, 3) = (1, 2, 3)
2+
val b: (4, 5, 6) = (4, 5, 6)
3+
val c: (7, 8) = (7, 8)
4+
val d: Unit = ()
5+
6+
// Zip
7+
val r1: ((1, 4), (2, 5), (3, 6)) = a.zip(b)
8+
val r2: ((1, 7), (2, 8)) = a.zip(c)
9+
val r3: ((7, 1), (8, 2)) = c.zip(a)
10+
val r4: Unit = d.zip(a)
11+
val r5: Unit = a.zip(d)
12+
13+
// Map
14+
case class Foo[X](x: X)
15+
16+
val r6: (Int, Int, Int) = a.map[[t] =>> Int]([t] => x: t => x match {
17+
case x: Int => x * x
18+
case _ => ???
19+
})
20+
21+
val r7: ((1, Foo[1]), (2, Foo[2]), (3, Foo[3])) =
22+
a.map[[t] =>> (t, Foo[t])]( [t] => x: t => (x, Foo(x)) )
23+
24+
// More Zip
25+
val t1: Int *: Long *: Tuple = (1, 2l, 100, 200)
26+
val t2: Int *: Char *: Tuple = (1, 'c', 33, 42)
27+
val t3: (Int, Int) *: (Long, Char) *: Tuple = t1.zip(t2)
28+
29+
val t4: Unit = d.zip(d)
30+
31+
@main def Test =
32+
List(r1, r2, r3, r4, r5, r6, r7, t3, t4).foreach(println)

0 commit comments

Comments
 (0)