Skip to content
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

move NamedTuple methods to separate scope. re-export #20504

Merged
merged 3 commits into from
Jun 14, 2024
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
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])

bishabosha marked this conversation as resolved.
Show resolved Hide resolved
// ALL METHODS DEPENDING ON `toTuple` MUST BE EXPORTED FROM `NamedTupleDecomposition`
/** The underlying tuple without the names */
inline def toTuple: V = x
bishabosha marked this conversation as resolved.
Show resolved Hide resolved

/** 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)
EugeneFlesselle marked this conversation as resolved.
Show resolved Hide resolved

// 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]]
Loading