Skip to content

Commit 01b404f

Browse files
authored
Improve type inference for literal named tuples (#20497)
Adds a new `NamedTuple.build` method which fixes the types of the labels first, as suggested in #20456 (comment) It requires `language.experimental.clauseInterleaving` language import. Keeps `withNames` as a friendlier option for end-users fixes #20456
2 parents 3c9d985 + bf0cd3c commit 01b404f

File tree

5 files changed

+145
-9
lines changed

5 files changed

+145
-9
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

+7-3
Original file line numberDiff line numberDiff line change
@@ -1596,9 +1596,13 @@ object desugar {
15961596
if ctx.mode.is(Mode.Type) then
15971597
AppliedTypeTree(ref(defn.NamedTupleTypeRef), namesTuple :: tup :: Nil)
15981598
else
1599-
TypeApply(
1600-
Apply(Select(ref(defn.NamedTupleModule), nme.withNames), tup),
1601-
namesTuple :: Nil)
1599+
Apply(
1600+
Apply(
1601+
TypeApply(
1602+
Select(ref(defn.NamedTupleModule), nme.build), // NamedTuple.build
1603+
namesTuple :: Nil), // ++ [(names...)]
1604+
Nil), // ++ ()
1605+
tup :: Nil) // .++ ((values...))
16021606

16031607
/** When desugaring a list pattern arguments `elems` adapt them and the
16041608
* expected type `pt` to each other. This means:

library/src/scala/NamedTuple.scala

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package scala
2+
import scala.language.experimental.clauseInterleaving
23
import annotation.experimental
34
import compiletime.ops.boolean.*
45

@@ -19,6 +20,11 @@ object NamedTuple:
1920

2021
def unapply[N <: Tuple, V <: Tuple](x: NamedTuple[N, V]): Some[V] = Some(x)
2122

23+
/** A named tuple expression will desugar to a call to `build`. For instance,
24+
* `(name = "Lyra", age = 23)` will desugar to `build[("name", "age")]()(("Lyra", 23))`.
25+
*/
26+
inline def build[N <: Tuple]()[V <: Tuple](x: V): NamedTuple[N, V] = x
27+
2228
extension [V <: Tuple](x: V)
2329
inline def withNames[N <: Tuple]: NamedTuple[N, V] = x
2430

@@ -214,4 +220,3 @@ object NamedTupleDecomposition:
214220
/** The value types of a named tuple represented as a regular tuple. */
215221
type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match
216222
case NamedTuple[_, x] => x
217-
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import language.experimental.namedTuples
2+
import NamedTuple.*
3+
4+
@FailsWith[HttpError]
5+
trait GreetService derives HttpService:
6+
@HttpInfo("GET", "/greet/{name}")
7+
def greet(@HttpPath name: String): String
8+
@HttpInfo("POST", "/greet/{name}")
9+
def setGreeting(@HttpPath name: String, @HttpBody greeting: String): Unit
10+
11+
@main def Test =
12+
13+
val e = HttpService.endpoints[GreetService]
14+
15+
println(e.greet.describe)
16+
println(e.setGreeting.describe)
17+
18+
// Type-safe server logic, driven by the ops-mirror,
19+
// requires named tuple with same labels in the same order,
20+
// and function that matches the required signature.
21+
val logic = e.serverLogic:
22+
(
23+
greet = (name) => Right("Hello, " + name),
24+
setGreeting = (name, greeting) => Right(())
25+
)
26+
27+
val server = ServerBuilder()
28+
.handleAll(logic)
29+
.create(port = 8080)
30+
31+
sys.addShutdownHook(server.close())
32+
33+
end Test
34+
35+
// IMPLEMENTATION DETAILS FOLLOW
36+
37+
/** Assume existence of macro to generate this */
38+
given (OpsMirror.Of[GreetService] {
39+
type MirroredType = GreetService
40+
type OperationLabels = ("greet", "setGreeting")
41+
type Operations = (
42+
OpsMirror.Operation { type InputTypes = (String *: EmptyTuple); type OutputType = String; type ErrorType = HttpError },
43+
OpsMirror.Operation { type InputTypes = (String *: String *: EmptyTuple); type OutputType = Unit; type ErrorType = HttpError }
44+
)
45+
}) = new OpsMirror:
46+
type MirroredType = GreetService
47+
type OperationLabels = ("greet", "setGreeting")
48+
type Operations = (
49+
OpsMirror.Operation { type InputTypes = (String *: EmptyTuple); type OutputType = String; type ErrorType = HttpError },
50+
OpsMirror.Operation { type InputTypes = (String *: String *: EmptyTuple); type OutputType = Unit; type ErrorType = HttpError }
51+
)
52+
53+
object OpsMirror:
54+
type Of[T] = OpsMirror { type MirroredType = T }
55+
56+
type Operation_I[I <: Tuple] = Operation { type InputTypes = I }
57+
type Operation_O[O] = Operation { type OutputType = O }
58+
type Operation_E[E] = Operation { type ErrorType = E }
59+
60+
trait Operation:
61+
type InputTypes <: Tuple
62+
type OutputType
63+
type ErrorType
64+
65+
trait OpsMirror:
66+
type MirroredType
67+
type OperationLabels <: Tuple
68+
type Operations <: Tuple
69+
70+
trait HttpService[T]:
71+
def route(str: String): Route
72+
trait Route
73+
74+
type Func[I <: Tuple, O, E] = I match
75+
case EmptyTuple => Either[E, O]
76+
case t *: EmptyTuple => t => Either[E, O]
77+
case t *: u *: EmptyTuple => (t, u) => Either[E, O]
78+
79+
type ToFunc[T] = T match
80+
case HttpService.Endpoint[i, o, e] => Func[i, o, e]
81+
82+
final class FailsWith[E] extends scala.annotation.Annotation
83+
final class HttpInfo(method: String, route: String) extends scala.annotation.Annotation
84+
final class HttpBody() extends scala.annotation.Annotation
85+
final class HttpPath() extends scala.annotation.Annotation
86+
87+
sealed trait HttpError
88+
89+
object HttpService:
90+
opaque type Endpoint[I <: Tuple, O, E] = Route
91+
92+
extension [I <: Tuple, O, E](e: Endpoint[I, O, E])
93+
def describe: String = ??? // some thing that looks inside the Route to debug it
94+
95+
type ToEndpoints[Ops <: Tuple] <: Tuple = Ops match
96+
case EmptyTuple => EmptyTuple
97+
case op *: ops => (op, op, op) match
98+
case (OpsMirror.Operation_I[i]) *: (OpsMirror.Operation_O[o]) *: (OpsMirror.Operation_E[e]) *: _ =>
99+
Endpoint[i, o, e] *: ToEndpoints[ops]
100+
101+
trait Handler
102+
103+
class Endpoints[T](val model: HttpService[T]) extends Selectable:
104+
type Fields <: AnyNamedTuple
105+
def selectDynamic(name: String): Route = model.route(name)
106+
107+
def serverLogic(funcs: NamedTuple[Names[Fields], Tuple.Map[DropNames[Fields], ToFunc]]): List[Handler] = ???
108+
109+
def derived[T](using OpsMirror.Of[T]): HttpService[T] = ??? // inline method to create routes
110+
111+
def endpoints[T](using model: HttpService[T], m: OpsMirror.Of[T]): Endpoints[T] {
112+
type Fields = NamedTuple[m.OperationLabels, ToEndpoints[m.Operations]]
113+
} =
114+
new Endpoints(model) { type Fields = NamedTuple[m.OperationLabels, ToEndpoints[m.Operations]] }
115+
116+
class ServerBuilder():
117+
def handleAll(hs: List[HttpService.Handler]): this.type = this
118+
def create(port: Int): Server = Server()
119+
120+
class Server():
121+
def close(): Unit = ()

tests/run/named-tuples.check

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Bob is younger than Bill
88
Bob is younger than Lucy
99
Bill is younger than Lucy
1010
(((Lausanne,Pully),Preverenges),((1003,1009),1028))
11+
118

tests/run/named-tuples.scala

+10-5
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,20 @@ val _: CombinedInfo = bob ++ addr
100100
val addr4 = addr3.zip("Preverenges", 1028)
101101
println(addr4)
102102

103+
val reducer: (map: Person => Int, reduce: (Int, Int) => Int) =
104+
(map = _.age, reduce = _ + _)
105+
106+
extension [T](xs: List[T])
107+
def mapReduce[U](reducer: (map: T => U, reduce: (U, U) => U)): U =
108+
xs.map(reducer.map).reduce(reducer.reduce)
109+
110+
val totalAge = persons.mapReduce(reducer)
111+
println(totalAge)
112+
103113
// testing conversions
104114
object Conv:
105115

106116
val p: (String, Int) = bob
107117
def f22(x: (String, Int)) = x._1
108118
def f22(x: String) = x
109119
f22(bob)
110-
111-
112-
113-
114-

0 commit comments

Comments
 (0)