From bf0cd3c317c06a9e2cec335d06a112808e6e7db1 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Wed, 29 May 2024 23:53:38 +0200 Subject: [PATCH] implement NamedTuple.build for better inference with target types --- .../src/dotty/tools/dotc/ast/Desugar.scala | 10 +- library/src/scala/NamedTuple.scala | 7 +- tests/pos/named-tuples-ops-mirror.scala | 121 ++++++++++++++++++ tests/run/named-tuples.check | 1 + tests/run/named-tuples.scala | 15 ++- 5 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 tests/pos/named-tuples-ops-mirror.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index b1b771bc7512..977eac5df4fc 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1596,9 +1596,13 @@ object desugar { if ctx.mode.is(Mode.Type) then AppliedTypeTree(ref(defn.NamedTupleTypeRef), namesTuple :: tup :: Nil) else - TypeApply( - Apply(Select(ref(defn.NamedTupleModule), nme.withNames), tup), - namesTuple :: Nil) + Apply( + Apply( + TypeApply( + Select(ref(defn.NamedTupleModule), nme.build), // NamedTuple.build + namesTuple :: Nil), // ++ [(names...)] + Nil), // ++ () + tup :: Nil) // .++ ((values...)) /** When desugaring a list pattern arguments `elems` adapt them and the * expected type `pt` to each other. This means: diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index dc6e6c3144f6..4c31728d6626 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -1,4 +1,5 @@ package scala +import scala.language.experimental.clauseInterleaving import annotation.experimental import compiletime.ops.boolean.* @@ -19,6 +20,11 @@ object NamedTuple: def unapply[N <: Tuple, V <: Tuple](x: NamedTuple[N, V]): Some[V] = Some(x) + /** A named tuple expression will desugar to a call to `build`. For instance, + * `(name = "Lyra", age = 23)` will desugar to `build[("name", "age")]()(("Lyra", 23))`. + */ + inline def build[N <: Tuple]()[V <: Tuple](x: V): NamedTuple[N, V] = x + extension [V <: Tuple](x: V) inline def withNames[N <: Tuple]: NamedTuple[N, V] = x @@ -214,4 +220,3 @@ object NamedTupleDecomposition: /** The value types of a named tuple represented as a regular tuple. */ type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match case NamedTuple[_, x] => x - diff --git a/tests/pos/named-tuples-ops-mirror.scala b/tests/pos/named-tuples-ops-mirror.scala new file mode 100644 index 000000000000..f66eb89534fb --- /dev/null +++ b/tests/pos/named-tuples-ops-mirror.scala @@ -0,0 +1,121 @@ +import language.experimental.namedTuples +import NamedTuple.* + +@FailsWith[HttpError] +trait GreetService derives HttpService: + @HttpInfo("GET", "/greet/{name}") + def greet(@HttpPath name: String): String + @HttpInfo("POST", "/greet/{name}") + def setGreeting(@HttpPath name: String, @HttpBody greeting: String): Unit + +@main def Test = + + val e = HttpService.endpoints[GreetService] + + println(e.greet.describe) + println(e.setGreeting.describe) + + // Type-safe server logic, driven by the ops-mirror, + // requires named tuple with same labels in the same order, + // and function that matches the required signature. + val logic = e.serverLogic: + ( + greet = (name) => Right("Hello, " + name), + setGreeting = (name, greeting) => Right(()) + ) + + val server = ServerBuilder() + .handleAll(logic) + .create(port = 8080) + + sys.addShutdownHook(server.close()) + +end Test + +// IMPLEMENTATION DETAILS FOLLOW + +/** Assume existence of macro to generate this */ +given (OpsMirror.Of[GreetService] { + type MirroredType = GreetService + type OperationLabels = ("greet", "setGreeting") + type Operations = ( + OpsMirror.Operation { type InputTypes = (String *: EmptyTuple); type OutputType = String; type ErrorType = HttpError }, + OpsMirror.Operation { type InputTypes = (String *: String *: EmptyTuple); type OutputType = Unit; type ErrorType = HttpError } + ) +}) = new OpsMirror: + type MirroredType = GreetService + type OperationLabels = ("greet", "setGreeting") + type Operations = ( + OpsMirror.Operation { type InputTypes = (String *: EmptyTuple); type OutputType = String; type ErrorType = HttpError }, + OpsMirror.Operation { type InputTypes = (String *: String *: EmptyTuple); type OutputType = Unit; type ErrorType = HttpError } + ) + +object OpsMirror: + type Of[T] = OpsMirror { type MirroredType = T } + + type Operation_I[I <: Tuple] = Operation { type InputTypes = I } + type Operation_O[O] = Operation { type OutputType = O } + type Operation_E[E] = Operation { type ErrorType = E } + + trait Operation: + type InputTypes <: Tuple + type OutputType + type ErrorType + +trait OpsMirror: + type MirroredType + type OperationLabels <: Tuple + type Operations <: Tuple + +trait HttpService[T]: + def route(str: String): Route +trait Route + +type Func[I <: Tuple, O, E] = I match + case EmptyTuple => Either[E, O] + case t *: EmptyTuple => t => Either[E, O] + case t *: u *: EmptyTuple => (t, u) => Either[E, O] + +type ToFunc[T] = T match + case HttpService.Endpoint[i, o, e] => Func[i, o, e] + +final class FailsWith[E] extends scala.annotation.Annotation +final class HttpInfo(method: String, route: String) extends scala.annotation.Annotation +final class HttpBody() extends scala.annotation.Annotation +final class HttpPath() extends scala.annotation.Annotation + +sealed trait HttpError + +object HttpService: + opaque type Endpoint[I <: Tuple, O, E] = Route + + extension [I <: Tuple, O, E](e: Endpoint[I, O, E]) + def describe: String = ??? // some thing that looks inside the Route to debug it + + type ToEndpoints[Ops <: Tuple] <: Tuple = Ops match + case EmptyTuple => EmptyTuple + case op *: ops => (op, op, op) match + case (OpsMirror.Operation_I[i]) *: (OpsMirror.Operation_O[o]) *: (OpsMirror.Operation_E[e]) *: _ => + Endpoint[i, o, e] *: ToEndpoints[ops] + + trait Handler + + class Endpoints[T](val model: HttpService[T]) extends Selectable: + type Fields <: AnyNamedTuple + def selectDynamic(name: String): Route = model.route(name) + + def serverLogic(funcs: NamedTuple[Names[Fields], Tuple.Map[DropNames[Fields], ToFunc]]): List[Handler] = ??? + + def derived[T](using OpsMirror.Of[T]): HttpService[T] = ??? // inline method to create routes + + def endpoints[T](using model: HttpService[T], m: OpsMirror.Of[T]): Endpoints[T] { + type Fields = NamedTuple[m.OperationLabels, ToEndpoints[m.Operations]] + } = + new Endpoints(model) { type Fields = NamedTuple[m.OperationLabels, ToEndpoints[m.Operations]] } + +class ServerBuilder(): + def handleAll(hs: List[HttpService.Handler]): this.type = this + def create(port: Int): Server = Server() + +class Server(): + def close(): Unit = () diff --git a/tests/run/named-tuples.check b/tests/run/named-tuples.check index 6485aefafa9a..ab1817255336 100644 --- a/tests/run/named-tuples.check +++ b/tests/run/named-tuples.check @@ -8,3 +8,4 @@ Bob is younger than Bill Bob is younger than Lucy Bill is younger than Lucy (((Lausanne,Pully),Preverenges),((1003,1009),1028)) +118 diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index 676c21a0e434..32c634188d52 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -100,6 +100,16 @@ val _: CombinedInfo = bob ++ addr val addr4 = addr3.zip("Preverenges", 1028) println(addr4) + val reducer: (map: Person => Int, reduce: (Int, Int) => Int) = + (map = _.age, reduce = _ + _) + + extension [T](xs: List[T]) + def mapReduce[U](reducer: (map: T => U, reduce: (U, U) => U)): U = + xs.map(reducer.map).reduce(reducer.reduce) + + val totalAge = persons.mapReduce(reducer) + println(totalAge) + // testing conversions object Conv: @@ -107,8 +117,3 @@ object Conv: def f22(x: (String, Int)) = x._1 def f22(x: String) = x f22(bob) - - - - -