Skip to content

Commit 8c782c2

Browse files
committed
Add embedded queries use case
1 parent e975903 commit 8c782c2

File tree

1 file changed

+199
-2
lines changed

1 file changed

+199
-2
lines changed

content/named-tuples.md

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ Named patterns are compatible with extensible pattern matching simply because
230230

231231
### Operations on Named Tuples
232232

233-
The operations on named tuples are defined in object `scala.NamedTuple`. The current version of this object is listed in the appendix.
233+
The operations on named tuples are defined in object `scala.NamedTuple`. The current version of this object is listed in Appendix A.
234234

235235
### Restrictions
236236

@@ -250,6 +250,13 @@ The following restrictions apply to named tuples and named pattern arguments:
250250
case (age = x) => // error
251251
```
252252

253+
### Use Case
254+
255+
As a a use case showing some advanced capabilities of named tuples (including computed field names and the `From` type),
256+
we show an implementation of embedded queries in Scala. For expressions that look like working with collections are instead
257+
used to directly generate a query AST that can be further optimized and mapped to a variety of query languages. The code
258+
is given in Appendix B.
259+
253260
### Syntax Changes
254261

255262
The syntax of Scala is extended as follows to support named tuples and
@@ -346,7 +353,7 @@ This section should list prior work related to the proposal, notably:
346353

347354
## FAQ
348355

349-
## Appendix: NamedTuple Definition
356+
## Appendix A: NamedTuple Definition
350357

351358
Here is the current definition of `NamedTuple`. This is part of the library and therefore subject to future changes and additions.
352359

@@ -544,4 +551,194 @@ object NamedTupleDecomposition:
544551
/** The value types of a named tuple represented as a regular tuple. */
545552
type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match
546553
case NamedTuple[_, x] => x
554+
```
555+
556+
## Appendix B: Embedded Queries Case Study
557+
558+
```scala
559+
import language.experimental.namedTuples
560+
import NamedTuple.{NamedTuple, AnyNamedTuple}
561+
562+
/* This is a demonstrator that shows how to map regular for expressions to
563+
* internal data that can be optimized by a query engine. It needs NamedTuples
564+
* and type classes but no macros. It's so far very provisional and experimental,
565+
* intended as a basis for further exploration.
566+
*/
567+
568+
/** The type of expressions in the query language */
569+
trait Expr[Result] extends Selectable:
570+
571+
/** This type is used to support selection with any of the field names
572+
* defined by Fields.
573+
*/
574+
type Fields = NamedTuple.Map[NamedTuple.From[Result], Expr]
575+
576+
/** A selection of a field name defined by Fields is implemented by `selectDynamic`.
577+
* The implementation will add a cast to the right Expr type corresponding
578+
* to the field type.
579+
*/
580+
def selectDynamic(fieldName: String) = Expr.Select(this, fieldName)
581+
582+
/** Member methods to implement universal equality on Expr level. */
583+
def == (other: Expr[?]): Expr[Boolean] = Expr.Eq(this, other)
584+
def != (other: Expr[?]): Expr[Boolean] = Expr.Ne(this, other)
585+
586+
object Expr:
587+
588+
/** Sample extension methods for individual types */
589+
extension (x: Expr[Int])
590+
def > (y: Expr[Int]): Expr[Boolean] = Gt(x, y)
591+
def > (y: Int): Expr[Boolean] = Gt(x, IntLit(y))
592+
extension (x: Expr[Boolean])
593+
def &&(y: Expr[Boolean]): Expr[Boolean] = And(x, y)
594+
def || (y: Expr[Boolean]): Expr[Boolean] = Or(x, y)
595+
596+
// Note: All field names of constructors in the query language are prefixed with `$`
597+
// so that we don't accidentally pick a field name of a constructor class where we want
598+
// a name in the domain model instead.
599+
600+
// Some sample constructors for Exprs
601+
case class Gt($x: Expr[Int], $y: Expr[Int]) extends Expr[Boolean]
602+
case class Plus(x: Expr[Int], y: Expr[Int]) extends Expr[Int]
603+
case class And($x: Expr[Boolean], $y: Expr[Boolean]) extends Expr[Boolean]
604+
case class Or($x: Expr[Boolean], $y: Expr[Boolean]) extends Expr[Boolean]
605+
606+
// So far Select is weakly typed, so `selectDynamic` is easy to implement.
607+
// Todo: Make it strongly typed like the other cases
608+
case class Select[A]($x: Expr[A], $name: String) extends Expr
609+
610+
case class Single[S <: String, A]($x: Expr[A])
611+
extends Expr[NamedTuple[S *: EmptyTuple, A *: EmptyTuple]]
612+
613+
case class Concat[A <: AnyNamedTuple, B <: AnyNamedTuple]($x: Expr[A], $y: Expr[B])
614+
extends Expr[NamedTuple.Concat[A, B]]
615+
616+
case class Join[A <: AnyNamedTuple](a: A)
617+
extends Expr[NamedTuple.Map[A, StripExpr]]
618+
619+
type StripExpr[E] = E match
620+
case Expr[b] => b
621+
622+
// Also weakly typed in the arguents since these two classes model universal equality */
623+
case class Eq($x: Expr[?], $y: Expr[?]) extends Expr[Boolean]
624+
case class Ne($x: Expr[?], $y: Expr[?]) extends Expr[Boolean]
625+
626+
/** References are placeholders for parameters */
627+
private var refCount = 0
628+
629+
case class Ref[A]($name: String = "") extends Expr[A]:
630+
val id = refCount
631+
refCount += 1
632+
override def toString = s"ref$id(${$name})"
633+
634+
/** Literals are type-specific, tailored to the types that the DB supports */
635+
case class IntLit($value: Int) extends Expr[Int]
636+
637+
/** Scala values can be lifted into literals by conversions */
638+
given Conversion[Int, IntLit] = IntLit(_)
639+
640+
/** The internal representation of a function `A => B`
641+
* Query languages are ususally first-order, so Fun is not an Expr
642+
*/
643+
case class Fun[A, B](param: Ref[A], f: B)
644+
645+
type Pred[A] = Fun[A, Expr[Boolean]]
646+
647+
/** Explicit conversion from
648+
* (name_1: Expr[T_1], ..., name_n: Expr[T_n])
649+
* to
650+
* Expr[(name_1: T_1, ..., name_n: T_n)]
651+
*/
652+
extension [A <: AnyNamedTuple](x: A) def toRow: Join[A] = Join(x)
653+
654+
/** Same as _.toRow, as an implicit conversion */
655+
given [A <: AnyNamedTuple]: Conversion[A, Expr.Join[A]] = Expr.Join(_)
656+
657+
end Expr
658+
659+
/** The type of database queries. So far, we have queries
660+
* that represent whole DB tables and queries that reify
661+
* for-expressions as data.
662+
*/
663+
trait Query[A]
664+
665+
object Query:
666+
import Expr.{Pred, Fun, Ref}
667+
668+
case class Filter[A]($q: Query[A], $p: Pred[A]) extends Query[A]
669+
case class Map[A, B]($q: Query[A], $f: Fun[A, Expr[B]]) extends Query[B]
670+
case class FlatMap[A, B]($q: Query[A], $f: Fun[A, Query[B]]) extends Query[B]
671+
672+
// Extension methods to support for-expression syntax for queries
673+
extension [R](x: Query[R])
674+
675+
def withFilter(p: Ref[R] => Expr[Boolean]): Query[R] =
676+
val ref = Ref[R]()
677+
Filter(x, Fun(ref, p(ref)))
678+
679+
def map[B](f: Ref[R] => Expr[B]): Query[B] =
680+
val ref = Ref[R]()
681+
Map(x, Fun(ref, f(ref)))
682+
683+
def flatMap[B](f: Ref[R] => Query[B]): Query[B] =
684+
val ref = Ref[R]()
685+
FlatMap(x, Fun(ref, f(ref)))
686+
end Query
687+
688+
/** The type of query references to database tables */
689+
case class Table[R]($name: String) extends Query[R]
690+
691+
// Everything below is code using the model -----------------------------
692+
693+
// Some sample types
694+
case class City(zipCode: Int, name: String, population: Int)
695+
type Address = (city: City, street: String, number: Int)
696+
type Person = (name: String, age: Int, addr: Address)
697+
698+
@main def Test =
699+
700+
val cities = Table[City]("cities")
701+
702+
val q1 = cities.map: c =>
703+
c.zipCode
704+
val q2 = cities.withFilter: city =>
705+
city.population > 10_000
706+
.map: city =>
707+
city.name
708+
709+
val q3 =
710+
for
711+
city <- cities
712+
if city.population > 10_000
713+
yield city.name
714+
715+
val q4 =
716+
for
717+
city <- cities
718+
alt <- cities
719+
if city.name == alt.name && city.zipCode != alt.zipCode
720+
yield
721+
city
722+
723+
val addresses = Table[Address]("addresses")
724+
val q5 =
725+
for
726+
city <- cities
727+
addr <- addresses
728+
if addr.street == city.name
729+
yield
730+
(name = city.name, num = addr.number)
731+
732+
val q6 =
733+
cities.map: city =>
734+
(name = city.name, zipCode = city.zipCode)
735+
736+
def run[T](q: Query[T]): Iterator[T] = ???
737+
738+
def x1: Iterator[Int] = run(q1)
739+
def x2: Iterator[String] = run(q2)
740+
def x3: Iterator[String] = run(q3)
741+
def x4: Iterator[City] = run(q4)
742+
def x5: Iterator[(name: String, num: Int)] = run(q5)
743+
def x6: Iterator[(name: String, zipCode: Int)] = run(q6)
547744
```

0 commit comments

Comments
 (0)