Skip to content

Commit

Permalink
move NamedTuple methods to separate scope. re-export (#20504)
Browse files Browse the repository at this point in the history
By moving the methods to NamedTupleDecomposition, there is no issue with
trying to reconcile types at inlining

fixes #20427
  • Loading branch information
smarter authored Jun 14, 2024
2 parents de2d35c + 6af7022 commit 0e63753
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 79 deletions.
164 changes: 85 additions & 79 deletions library/src/scala/NamedTuple.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand Down
154 changes: 154 additions & 0 deletions tests/pos/named-tuple-combinators.scala
Original file line number Diff line number Diff line change
@@ -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]]

0 comments on commit 0e63753

Please sign in to comment.