diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 4c31728d6626..1f1b6f3e2d9f 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -28,100 +28,27 @@ object NamedTuple: extension [V <: Tuple](x: V) inline def withNames[N <: Tuple]: NamedTuple[N, V] = x - export NamedTupleDecomposition.{Names, DropNames} + export NamedTupleDecomposition.{ + Names, DropNames, + apply, size, init, last, tail, take, drop, splitAt, ++, map, reverse, zip, toList, toArray, toIArray + } extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) + // ALL METHODS DEPENDING ON `toTuple` MUST BE EXPORTED FROM `NamedTupleDecomposition` /** The underlying tuple without the names */ inline def toTuple: V = x - /** The number of elements in this tuple */ - inline def size: Tuple.Size[V] = toTuple.size - // This intentionally works for empty named tuples as well. I think NonEmptyTuple is a dead end // and should be reverted, just like NonEmptyList is also appealing at first, but a bad idea // in the end. - /** The value (without the name) at index `n` of this tuple */ - inline def apply(n: Int): Tuple.Elem[V, n.type] = - inline toTuple match - case tup: NonEmptyTuple => tup(n).asInstanceOf[Tuple.Elem[V, n.type]] - case tup => tup.productElement(n).asInstanceOf[Tuple.Elem[V, n.type]] - /** The first element value of this tuple */ - inline def head: Tuple.Elem[V, 0] = apply(0) - - /** The tuple consisting of all elements of this tuple except the first one */ - inline def tail: NamedTuple[Tuple.Tail[N], Tuple.Tail[V]] = - toTuple.drop(1).asInstanceOf[NamedTuple[Tuple.Tail[N], Tuple.Tail[V]]] - - /** The last element value of this tuple */ - inline def last: Tuple.Last[V] = apply(size - 1).asInstanceOf[Tuple.Last[V]] - - /** The tuple consisting of all elements of this tuple except the last one */ - inline def init: NamedTuple[Tuple.Init[N], Tuple.Init[V]] = - toTuple.take(size - 1).asInstanceOf[NamedTuple[Tuple.Init[N], Tuple.Init[V]]] - - /** The tuple consisting of the first `n` elements of this tuple, or all - * elements if `n` exceeds `size`. - */ - inline def take(n: Int): NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]] = - toTuple.take(n) - - /** The tuple consisting of all elements of this tuple except the first `n` ones, - * or no elements if `n` exceeds `size`. - */ - inline def drop(n: Int): NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]] = - toTuple.drop(n) - - /** The tuple `(x.take(n), x.drop(n))` */ - inline def splitAt(n: Int): - (NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]], - NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]]) = - // would be nice if this could have type `Split[NamedTuple[N, V]]` instead, but - // we get a type error then. Similar for other methods here. - toTuple.splitAt(n) - - /** The tuple consisting of all elements of this tuple followed by all elements - * of tuple `that`. The names of the two tuples must be disjoint. - */ - inline def ++ [N2 <: Tuple, V2 <: Tuple](that: NamedTuple[N2, V2])(using Tuple.Disjoint[N, N2] =:= true) - : NamedTuple[Tuple.Concat[N, N2], Tuple.Concat[V, V2]] - = toTuple ++ that.toTuple + inline def head: Tuple.Elem[V, 0] = x.apply(0) // inline def :* [L] (x: L): NamedTuple[Append[N, ???], Append[V, L] = ??? // inline def *: [H] (x: H): NamedTuple[??? *: N], H *: V] = ??? - /** The named tuple consisting of all element values of this tuple mapped by - * the polymorphic mapping function `f`. The names of elements are preserved. - * If `x = (n1 = v1, ..., ni = vi)` then `x.map(f) = `(n1 = f(v1), ..., ni = f(vi))`. - */ - inline def map[F[_]](f: [t] => t => F[t]): NamedTuple[N, Tuple.Map[V, F]] = - toTuple.map(f).asInstanceOf[NamedTuple[N, Tuple.Map[V, F]]] - - /** The named tuple consisting of all elements of this tuple in reverse */ - inline def reverse: NamedTuple[Tuple.Reverse[N], Tuple.Reverse[V]] = - toTuple.reverse - - /** The named tuple consisting of all elements values of this tuple zipped - * with corresponding element values in named tuple `that`. - * If the two tuples have different sizes, - * the extra elements of the larger tuple will be disregarded. - * The names of `x` and `that` at the same index must be the same. - * The result tuple keeps the same names as the operand tuples. - */ - inline def zip[V2 <: Tuple](that: NamedTuple[N, V2]): NamedTuple[N, Tuple.Zip[V, V2]] = - toTuple.zip(that.toTuple) - - /** A list consisting of all element values */ - inline def toList: List[Tuple.Union[V]] = toTuple.toList.asInstanceOf[List[Tuple.Union[V]]] - - /** An array consisting of all element values */ - inline def toArray: Array[Object] = toTuple.toArray - - /** An immutable array consisting of all element values */ - inline def toIArray: IArray[Object] = toTuple.toIArray - end extension /** The size of a named tuple, represented as a literal constant subtype of Int */ @@ -212,6 +139,85 @@ end NamedTuple @experimental object NamedTupleDecomposition: import NamedTuple.* + extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) + /** The value (without the name) at index `n` of this tuple */ + inline def apply(n: Int): Tuple.Elem[V, n.type] = + inline x.toTuple match + case tup: NonEmptyTuple => tup(n).asInstanceOf[Tuple.Elem[V, n.type]] + case tup => tup.productElement(n).asInstanceOf[Tuple.Elem[V, n.type]] + + /** The number of elements in this tuple */ + inline def size: Tuple.Size[V] = x.toTuple.size + + /** The last element value of this tuple */ + inline def last: Tuple.Last[V] = apply(size - 1).asInstanceOf[Tuple.Last[V]] + + /** The tuple consisting of all elements of this tuple except the last one */ + inline def init: NamedTuple[Tuple.Init[N], Tuple.Init[V]] = + x.toTuple.take(size - 1).asInstanceOf[NamedTuple[Tuple.Init[N], Tuple.Init[V]]] + + /** The tuple consisting of all elements of this tuple except the first one */ + inline def tail: NamedTuple[Tuple.Tail[N], Tuple.Tail[V]] = + x.toTuple.drop(1).asInstanceOf[NamedTuple[Tuple.Tail[N], Tuple.Tail[V]]] + + /** The tuple consisting of the first `n` elements of this tuple, or all + * elements if `n` exceeds `size`. + */ + inline def take(n: Int): NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]] = + x.toTuple.take(n) + + /** The tuple consisting of all elements of this tuple except the first `n` ones, + * or no elements if `n` exceeds `size`. + */ + inline def drop(n: Int): NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]] = + x.toTuple.drop(n) + + /** The tuple `(x.take(n), x.drop(n))` */ + inline def splitAt(n: Int): + (NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]], + NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]]) = + // would be nice if this could have type `Split[NamedTuple[N, V]]` instead, but + // we get a type error then. Similar for other methods here. + x.toTuple.splitAt(n) + + /** The tuple consisting of all elements of this tuple followed by all elements + * of tuple `that`. The names of the two tuples must be disjoint. + */ + inline def ++ [N2 <: Tuple, V2 <: Tuple](that: NamedTuple[N2, V2])(using Tuple.Disjoint[N, N2] =:= true) + : NamedTuple[Tuple.Concat[N, N2], Tuple.Concat[V, V2]] + = x.toTuple ++ that.toTuple + + /** The named tuple consisting of all element values of this tuple mapped by + * the polymorphic mapping function `f`. The names of elements are preserved. + * If `x = (n1 = v1, ..., ni = vi)` then `x.map(f) = `(n1 = f(v1), ..., ni = f(vi))`. + */ + inline def map[F[_]](f: [t] => t => F[t]): NamedTuple[N, Tuple.Map[V, F]] = + x.toTuple.map(f).asInstanceOf[NamedTuple[N, Tuple.Map[V, F]]] + + /** The named tuple consisting of all elements of this tuple in reverse */ + inline def reverse: NamedTuple[Tuple.Reverse[N], Tuple.Reverse[V]] = + x.toTuple.reverse + + /** The named tuple consisting of all elements values of this tuple zipped + * with corresponding element values in named tuple `that`. + * If the two tuples have different sizes, + * the extra elements of the larger tuple will be disregarded. + * The names of `x` and `that` at the same index must be the same. + * The result tuple keeps the same names as the operand tuples. + */ + inline def zip[V2 <: Tuple](that: NamedTuple[N, V2]): NamedTuple[N, Tuple.Zip[V, V2]] = + x.toTuple.zip(that.toTuple) + + /** A list consisting of all element values */ + inline def toList: List[Tuple.Union[V]] = x.toTuple.toList.asInstanceOf[List[Tuple.Union[V]]] + + /** An array consisting of all element values */ + inline def toArray: Array[Object] = x.toTuple.toArray + + /** An immutable array consisting of all element values */ + inline def toIArray: IArray[Object] = x.toTuple.toIArray + + end extension /** The names of a named tuple, represented as a tuple of literal string values. */ type Names[X <: AnyNamedTuple] <: Tuple = X match diff --git a/tests/pos/named-tuple-combinators.scala b/tests/pos/named-tuple-combinators.scala new file mode 100644 index 000000000000..a5134b2e7d26 --- /dev/null +++ b/tests/pos/named-tuple-combinators.scala @@ -0,0 +1,154 @@ +import scala.language.experimental.namedTuples + +object Test: + // original code from issue https://github.com/scala/scala3/issues/20427 + type NT = NamedTuple.Concat[(hi: Int), (bla: String)] + def foo(x: NT) = + x.hi // error + val y: (hi: Int, bla: String) = x + y.hi // ok + + // SELECTOR (reduces to apply) + def foo1(x: NT) = + val res1 = x.hi // error + summon[res1.type <:< Int] + val y: (hi: Int, bla: String) = x + val res2 = y.hi // ok + summon[res2.type <:< Int] + + // toTuple + def foo2(x: NT) = + val res1 = x.toTuple + summon[res1.type <:< (Int, String)] + val y: (hi: Int, bla: String) = x + val res2 = y.toTuple + summon[res2.type <:< (Int, String)] + + // apply + def foo3(x: NT) = + val res1 = x.apply(1) + summon[res1.type <:< String] + val y: (hi: Int, bla: String) = x + val res2 = y.apply(1) + summon[res2.type <:< String] + + // size + def foo4(x: NT) = + class Box: + final val res1 = x.size // final val constrains to a singleton type + summon[res1.type <:< 2] + val y: (hi: Int, bla: String) = x + final val res2 = y.size // final val constrains to a singleton type + summon[res2.type <:< 2] + + // head + def foo5(x: NT) = + val res1 = x.head + summon[res1.type <:< Int] + val y: (hi: Int, bla: String) = x + val res2 = y.head + summon[res2.type <:< Int] + + // last + def foo6(x: NT) = + val res1 = x.last + summon[res1.type <:< String] + val y: (hi: Int, bla: String) = x + val res2 = y.last + summon[res2.type <:< String] + + // init + def foo7(x: NT) = + val res1 = x.init + summon[res1.type <:< (hi: Int)] + val y: (hi: Int, bla: String) = x + val res2 = y.init + summon[res2.type <:< (hi: Int)] + + // tail + def foo8(x: NT) = + val res1 = x.tail + summon[res1.type <:< (bla: String)] + val y: (hi: Int, bla: String) = x + val res2 = y.tail + summon[res2.type <:< (bla: String)] + + // take + def foo9(x: NT) = + val res1 = x.take(1) + summon[res1.type <:< (hi: Int)] + val y: (hi: Int, bla: String) = x + val res2 = y.take(1) + summon[res2.type <:< (hi: Int)] + + // drop + def foo10(x: NT) = + val res1 = x.drop(1) + summon[res1.type <:< (bla: String)] + val y: (hi: Int, bla: String) = x + val res2 = y.drop(1) + summon[res2.type <:< (bla: String)] + + // splitAt + def foo11(x: NT) = + val res1 = x.splitAt(1) + summon[res1.type <:< ((hi: Int), (bla: String))] + val y: (hi: Int, bla: String) = x + val res2 = y.splitAt(1) + summon[res2.type <:< ((hi: Int), (bla: String))] + + // ++ + def foo12(x: NT) = + val res1 = x ++ (baz = 23) + summon[res1.type <:< (hi: Int, bla: String, baz: Int)] + val y: (hi: Int, bla: String) = x + val res2 = y ++ (baz = 23) + summon[res2.type <:< (hi: Int, bla: String, baz: Int)] + + // map + def foo13(x: NT) = + val res1 = x.map([T] => (t: T) => Option(t)) + summon[res1.type <:< (hi: Option[Int], bla: Option[String])] + val y: (hi: Int, bla: String) = x + val res2 = y.map([T] => (t: T) => Option(t)) + summon[res2.type <:< (hi: Option[Int], bla: Option[String])] + + // reverse + def foo14(x: NT) = + val res1 = x.reverse + summon[res1.type <:< (bla: String, hi: Int)] + val y: (hi: Int, bla: String) = x + val res2 = y.reverse + summon[res2.type <:< (bla: String, hi: Int)] + + // zip + def foo15(x: NT) = + val res1 = x.zip((hi = "xyz", bla = true)) + summon[res1.type <:< (hi: (Int, String), bla: (String, Boolean))] + val y: (hi: Int, bla: String) = x + val res2 = y.zip((hi = "xyz", bla = true)) + summon[res2.type <:< (hi: (Int, String), bla: (String, Boolean))] + + // toList + def foo16(x: NT) = + val res1 = x.toList + summon[res1.type <:< List[Tuple.Union[(Int, String)]]] + val y: (hi: Int, bla: String) = x + val res2 = y.toList + summon[res2.type <:< List[Tuple.Union[(Int, String)]]] + + // toArray + def foo17(x: NT) = + val res1 = x.toArray + summon[res1.type <:< Array[Object]] + val y: (hi: Int, bla: String) = x + val res2 = y.toArray + summon[res2.type <:< Array[Object]] + + // toIArray + def foo18(x: NT) = + val res1 = x.toIArray + summon[res1.type <:< IArray[Object]] + val y: (hi: Int, bla: String) = x + val res2 = y.toIArray + summon[res2.type <:< IArray[Object]]