Skip to content

Commit 56fa8f6

Browse files
committed
Add experimental NamedTuple copyFrom method
`copyFrom` chosen as it has a slight difference i.e. the argument should be surrounded in extra parentheses. Also, it can not be used for target typing of fields.
1 parent dbce4e8 commit 56fa8f6

File tree

6 files changed

+149
-4
lines changed

6 files changed

+149
-4
lines changed

library/src-bootstrapped/scala/NamedTuple.scala

+45-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package scala
22
import compiletime.ops.boolean.*
3+
import compiletime.ops.int.*
4+
import scala.annotation.experimental
5+
import scala.annotation.implicitNotFound
36

47
object NamedTuple:
58

@@ -25,10 +28,10 @@ object NamedTuple:
2528
extension [V <: Tuple](x: V)
2629
inline def withNames[N <: Tuple]: NamedTuple[N, V] = x
2730

28-
import NamedTupleDecomposition.{Names, DropNames}
31+
import NamedTupleDecomposition.{Names, DropNames, Decompose}
2932
export NamedTupleDecomposition.{
30-
Names, DropNames,
31-
apply, size, init, head, last, tail, take, drop, splitAt, ++, map, reverse, zip, toList, toArray, toIArray
33+
Names, DropNames, Decompose,
34+
apply, size, init, head, last, tail, take, drop, splitAt, ++, copyFrom, map, reverse, zip, toList, toArray, toIArray
3235
}
3336

3437
extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V])
@@ -116,6 +119,14 @@ object NamedTuple:
116119
case Names[Y] =>
117120
NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]]
118121

122+
@experimental
123+
type Copy[X <: AnyNamedTuple, Y <: AnyNamedTuple] = (Decompose[X], Decompose[Y]) match
124+
case ((nx, vx), (ny, vy)) =>
125+
NamedTuple[
126+
nx,
127+
NamedTupleDecomposition.Copy0[nx, vx, ny, vy, EmptyTuple]
128+
]
129+
119130
/** A type specially treated by the compiler to represent all fields of a
120131
* class argument `T` as a named tuple. Or, if `T` is already a named tuple,
121132
* `From[T]` is the same as `T`.
@@ -176,6 +187,18 @@ object NamedTupleDecomposition:
176187
: Concat[NamedTuple[N, V], NamedTuple[N2, V2]]
177188
= x.toTuple ++ that.toTuple
178189

190+
/** The named tuple consisting of all elements of this tuple, with fields replaced by those from `that`.
191+
* The field names of the update tuple of updates must all be present in this tuple, but not all fields are required.
192+
*/
193+
@experimental
194+
inline def copyFrom[N2 <: Tuple, V2 <: Tuple](that: NamedTuple[N2, V2])(using Tuple.ContainsAll[N2, N] =:= true)
195+
: Copy[NamedTuple[N, V], NamedTuple[N2, V2]]
196+
= scala.runtime.Tuples.copy(
197+
self = x.toTuple,
198+
that = that.toTuple,
199+
indices = compiletime.constValueTuple[Tuple.Indices[N, N2]]
200+
).asInstanceOf[Copy[NamedTuple[N, V], NamedTuple[N2, V2]]]
201+
179202
/** The named tuple consisting of all element values of this tuple mapped by
180203
* the polymorphic mapping function `f`. The names of elements are preserved.
181204
* If `x = (n1 = v1, ..., ni = vi)` then `x.map(f) = `(n1 = f(v1), ..., ni = f(vi))`.
@@ -207,10 +230,29 @@ object NamedTupleDecomposition:
207230

208231
end extension
209232

233+
@experimental
234+
type LookupName[X, N <: Tuple, V <: Tuple] <: Option[Any] =
235+
(N, V) match
236+
case (X *: _, v *: _) => Some[v]
237+
case (_ *: ns, _ *: vs) => LookupName[X, ns, vs]
238+
case (EmptyTuple, EmptyTuple) => None.type
239+
240+
@experimental
241+
type Copy0[Nx <: Tuple, Vx <: Tuple, Ny <: Tuple, Vy <: Tuple, Acc <: Tuple] <: Tuple =
242+
(Nx, Vx) match
243+
case (nx *: nxs, vx *: vxs) => LookupName[nx, Ny, Vy] match
244+
case Some[vy] => Copy0[nxs, vxs, Ny, Vy, vy *: Acc]
245+
case _ => Copy0[nxs, vxs, Ny, Vy, vx *: Acc]
246+
case (EmptyTuple, EmptyTuple) => Tuple.Reverse[Acc]
247+
210248
/** The names of a named tuple, represented as a tuple of literal string values. */
211249
type Names[X <: AnyNamedTuple] <: Tuple = X match
212250
case NamedTuple[n, _] => n
213251

214252
/** The value types of a named tuple represented as a regular tuple. */
215253
type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match
216254
case NamedTuple[_, x] => x
255+
256+
@experimental
257+
type Decompose[NT <: AnyNamedTuple] <: Tuple = NT match
258+
case NamedTuple[n, x] => (n, x)

library/src/scala/Tuple.scala

+27
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scala
33
import annotation.showAsInfix
44
import compiletime.*
55
import compiletime.ops.int.*
6+
import scala.annotation.experimental
67

78
/** Tuple of arbitrary arity */
89
sealed trait Tuple extends Product {
@@ -254,6 +255,32 @@ object Tuple {
254255
case false => Disjoint[xs, Y]
255256
case EmptyTuple => true
256257

258+
@experimental
259+
type ContainsAll[X <: Tuple, Y <: Tuple] <: Boolean = X match
260+
case x *: xs => Contains[Y, x] match
261+
case true => ContainsAll[xs, Y]
262+
case false => false
263+
case EmptyTuple => true
264+
265+
@experimental
266+
type IndexOfOptionOnto[X, N <: Tuple, Acc <: Int] <: Option[Int] = N match
267+
case X *: _ => Some[Acc]
268+
case _ *: ns => IndexOfOptionOnto[X, ns, S[Acc]]
269+
case EmptyTuple => None.type
270+
271+
@experimental
272+
type IndexOfOption[X, N <: Tuple] = IndexOfOptionOnto[X, N, 0]
273+
274+
@experimental
275+
type Indices[X <: Tuple, Y <: Tuple] = IndicesOnto[X, Y, 0, EmptyTuple]
276+
277+
@experimental
278+
type IndicesOnto[X <: Tuple, Y <: Tuple, Idx <: Int, Acc <: Tuple] <: Tuple = X match
279+
case x *: xs => IndexOfOption[x, Y] match
280+
case Some[i] => IndicesOnto[xs, Y, S[Idx], i *: Acc]
281+
case _ => IndicesOnto[xs, Y, S[Idx], (-1 * S[Idx]) *: Acc] // no problem if Int overflow
282+
case EmptyTuple => Reverse[Acc]
283+
257284
/** Empty tuple */
258285
def apply(): EmptyTuple = EmptyTuple
259286

library/src/scala/runtime/Tuples.scala

+17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package scala.runtime
22

3+
import scala.annotation.experimental
4+
35
object Tuples {
46

57
inline val MaxSpecialized = 22
@@ -283,6 +285,21 @@ object Tuples {
283285
case self: Product => self.productArity
284286
}
285287

288+
@experimental
289+
def copy(self: Tuple, that: Tuple, indices: Tuple): Tuple = indices match
290+
case EmptyTuple => self
291+
case _ =>
292+
val is = indices.productIterator.asInstanceOf[Iterator[Int]]
293+
val arr = IArray.from(
294+
is.map: i =>
295+
if i < 0 then
296+
val i0 = Math.abs(i) - 1 // nice that it is correct even for min value
297+
self.productElement(i0)
298+
else
299+
that.productElement(i)
300+
)
301+
Tuple.fromIArray(arr)
302+
286303
// Tail for Tuple1 to Tuple22
287304
private def specialCaseTail(self: Tuple): Tuple = {
288305
(self: Any) match {

tests/neg/named-tuple-ops-copy.scala

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//> using options -experimental
2+
3+
trait Mod
4+
5+
given Conversion[String, Mod] = _ => new Mod {}
6+
7+
type Foo = (name: String, mod: Mod)
8+
case class Foo0(name: String, mod: Mod)
9+
10+
@main def Test =
11+
val foo: Foo = (name = "foo", mod = "some_mod")
12+
val foo_updated: Foo = foo.copyFrom((mod = "bar")) // error, stays as String
13+
14+
15+
val foo0: Foo0 = Foo0(name = "foo", mod = "some_mod")
16+
val foo0_updated: Foo0 = foo0.copy(mod = "bar") // ok - does the conversion

tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala

+15-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,21 @@ val experimentalDefinitionInLibrary = Set(
8787
"scala.quoted.runtime.Patterns$.higherOrderHoleWithTypes",
8888

8989
// New feature: SIP 57 - runtimeChecked replacement of @unchecked
90-
"scala.Predef$.runtimeChecked", "scala.annotation.internal.RuntimeChecked"
90+
"scala.Predef$.runtimeChecked", "scala.annotation.internal.RuntimeChecked",
91+
92+
// New experimental method - copyFrom in NamedTuple.scala (and supporting definitions)
93+
"scala.NamedTuple$.copyFrom", "scala.NamedTuple$.Copy",
94+
"scala.NamedTuple$.Decompose",
95+
"scala.NamedTupleDecomposition$.Copy0",
96+
"scala.NamedTupleDecomposition$.Decompose",
97+
"scala.NamedTupleDecomposition$.LookupName",
98+
"scala.NamedTupleDecomposition$.copyFrom",
99+
"scala.Tuple$.ContainsAll",
100+
"scala.Tuple$.IndexOfOption",
101+
"scala.Tuple$.IndexOfOptionOnto",
102+
"scala.Tuple$.Indices",
103+
"scala.Tuple$.IndicesOnto",
104+
"scala.runtime.Tuples$.copy",
91105
)
92106

93107

tests/run/named-tuple-ops-copy.scala

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//> using options -experimental
2+
3+
type City = (name: String, zip: Int, pop: Int)
4+
type Coord = (x: Double, y: Double)
5+
type Labels = (x: String, y: String)
6+
7+
@main def Test =
8+
val city: City = (name = "Lausanne", zip = 1000, pop = 140000)
9+
val coord: Coord = (x = 1.0, y = 0.0)
10+
val labels: Labels = (x = "west", y = "north")
11+
12+
// first field updated
13+
val coord_update = coord.copyFrom((x = 2.0))
14+
val _: Coord = coord_update
15+
assert(coord_update.x == 2.0 && coord_update.y == 0.0)
16+
17+
// last field updated
18+
val city_update = city.copyFrom((pop = 150000))
19+
val _: City = city_update
20+
assert(city_update.name == "Lausanne" && city_update.zip == 1000 && city_update.pop == 150000)
21+
22+
// replace field types
23+
val coord_to_labels = coord.copyFrom((x = "east", y = "south"))
24+
val _: Labels = coord_to_labels
25+
assert(coord_to_labels.x == "east" && coord_to_labels.y == "south")
26+
27+
// out of order
28+
val city_update2 = city.copyFrom((pop = 150000, name = "Lausanne", zip = 1000))
29+
val _: City = city_update2

0 commit comments

Comments
 (0)