Skip to content

Commit 24bddc9

Browse files
committed
Add CanEqual instance for NamedTuple
Require same type for names. A possible other formulation could compare name types with `=:=` evidence, but this only benefits error messages with an explicit summon for CanEqual, and users would likely identify that the name fields dont match anyway.
1 parent f08de70 commit 24bddc9

File tree

5 files changed

+73
-0
lines changed

5 files changed

+73
-0
lines changed

library/src/scala/NamedTuple.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import compiletime.ops.boolean.*
33
import collection.immutable.{SeqMap, ListMap}
44

55
import language.experimental.captureChecking
6+
import scala.annotation.experimental
67

78
object NamedTuple:
89

@@ -16,6 +17,16 @@ object NamedTuple:
1617
/** A type which is a supertype of all named tuples. */
1718
opaque type AnyNamedTuple = Any
1819

20+
// This formulation fixes the name types, following the standard established in this file.
21+
// Alternatively you could require additional evidence and compare `N1` and `N2` types.
22+
// However if this is explored, you must not use `CanEqual[N1, N2]` because `CanEqual`
23+
// for Strings widens singletons.
24+
// Comparing different name types with `=:=` could also be possible.
25+
@experimental
26+
given namedTupleCanEqual: [N <: Tuple, V1 <: Tuple, V2 <: Tuple]
27+
=> (eq: CanEqual[V1, V2])
28+
=> CanEqual[NamedTuple[N, V1], NamedTuple[N, V2]] = CanEqual.derived
29+
1930
def apply[N <: Tuple, V <: Tuple](x: V): NamedTuple[N, V] = x
2031

2132
def unapply[N <: Tuple, V <: Tuple](x: NamedTuple[N, V]): Some[V] = Some(x)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- [E172] Type Error: tests/neg/named-tuples-strictEquality.scala:9:20 -------------------------------------------------
2+
9 | val b2: Boolean = u == v // error
3+
| ^^^^^^
4+
|Values of types (name : String, age : Int) and (name : String, birthYear : Int) cannot be compared with == or !=.
5+
|I found:
6+
|
7+
| NamedTuple.namedTupleCanEqual[N, V1, V2]
8+
|
9+
|But given instance namedTupleCanEqual in object NamedTuple does not match type CanEqual[(name : String, age : Int), (name : String, birthYear : Int)].
10+
-- [E172] Type Error: tests/neg/named-tuples-strictEquality.scala:15:11 ------------------------------------------------
11+
15 | case ScalaBook => true // error
12+
| ^^^^^^^^^
13+
|Values of types (name : String, published : Int) and (name : String, age : Int) cannot be compared with == or !=.
14+
|I found:
15+
|
16+
| NamedTuple.namedTupleCanEqual[N, V1, V2]
17+
|
18+
|But given instance namedTupleCanEqual in object NamedTuple does not match type CanEqual[(name : String, published : Int), (name : String, age : Int)].
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//> using options -language:strictEquality
2+
3+
object Test:
4+
5+
val u: (name: String, age: Int) = (name = "Bob", age = 25)
6+
val v: (name: String, birthYear: Int) = (name = "Charlie", birthYear = 1990)
7+
8+
val b1: Boolean = u.toTuple == v.toTuple // ok
9+
val b2: Boolean = u == v // error
10+
11+
val ScalaBook = (name = "Programming in Scala, 5th edition", published = 2021)
12+
13+
def hasScalaBook(books: IterableOnce[(name: String, age: Int)]): Boolean =
14+
books.exists {
15+
case ScalaBook => true // error
16+
case _ => false
17+
}

tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ val experimentalDefinitionInLibrary = Set(
9898

9999
// New feature: Erased trait
100100
"scala.compiletime.Erased",
101+
102+
// New API: Multiversal equality for Named Tuples
103+
"scala.NamedTuple$.namedTupleCanEqual",
101104
)
102105

103106

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//> using options -language:strictEquality
2+
3+
@main def Test =
4+
5+
val u: (name: String, age: Int) = (name = "Bob", age = 25)
6+
val v: (name: String, birthYear: Int) = (name = "Charlie", birthYear = 1990)
7+
8+
val ScalaBook = (name = "Programming in Scala, 5th edition", published = 2021)
9+
val books = Map(
10+
ScalaBook,
11+
(name = "Hands on Scala, 2nd edition", published = 2026),
12+
)
13+
14+
assert(u.toTuple != v.toTuple)
15+
assert(u == u)
16+
assert(v == v)
17+
18+
def hasScalaBook(books: IterableOnce[(name: String, published: Int)]): Boolean =
19+
books.exists {
20+
case ScalaBook => true
21+
case _ => false
22+
}
23+
24+
assert(hasScalaBook(books))

0 commit comments

Comments
 (0)