Skip to content

Commit

Permalink
Merge pull request #68 from tarao/typing-syntax
Browse files Browse the repository at this point in the history
Provide pretty syntax for generic record typing
  • Loading branch information
tarao authored Mar 25, 2024
2 parents 9787f14 + a4f0a10 commit 06bbc14
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 178 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ProjectKeys._
import Implicits._

ThisBuild / tlBaseVersion := "0.11"
ThisBuild / tlBaseVersion := "0.12"

ThisBuild / projectName := "record4s"
ThisBuild / groupId := "com.github.tarao"
Expand Down
31 changes: 13 additions & 18 deletions docs/advanced/generic.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ Generic Field Lookup
--------------------

It is possible to retrieve a field value of an arbitrary type from records by using
`Record.lookup`. To express the type of the field value, which is unknown until the
record type is specified, you can use `typing.Record.Lookup`. In the following example,
`typing.syntax`. To express the type of the field value, which is unknown until the
record type is specified, you can use `typing.syntax.in`. In the following example,
`getValue` method retrieves a field value named `value` from records of any type.

```scala mdoc:mline
import com.github.tarao.record4s.{%, Record}
import com.github.tarao.record4s.typing.Record.Lookup
import com.github.tarao.record4s.typing.syntax.{:=, in}

def getValue[R <: %, V](record: R)(using
Lookup.Aux[R, "value", V],
V := ("value" in R),
): V = Record.lookup(record, "value")

val r1 = %(value = "tarao")
Expand All @@ -25,9 +25,6 @@ getValue(r1)
getValue(r2)
```

Note that `(using Lookup.Aux[R, L, V]): V` is a shorthand for `(using l: Lookup[R, L]):
l.Out`.

Of course, it doesn't compile for a record without `value` field.

```scala mdoc:fail
Expand All @@ -40,15 +37,15 @@ Extending Generic Records with Concrete Fields
----------------------------------------------

To define a method to extend a generic record with some concrete field, we need to somehow
calculate the extended result record type. This can be done by using `typing.Record.Append`.
calculate the extended result record type. This can be done by using `typing.syntax.++`.

For example, `withEmail` method, which expects a domain name and returns a record extended
by `email` field of E-mail address, whose local part is filled by the first segment of
`name` field of the original record, can be defined as the following.

```scala mdoc:mline
import com.github.tarao.record4s.Tag
import com.github.tarao.record4s.typing.Record.Append
import com.github.tarao.record4s.typing.syntax.++

trait Person
object Person {
Expand All @@ -59,7 +56,7 @@ object Person {
domain: String,
localPart: String = p.firstName,
)(using
Append.Aux[R & Tag[Person], % { val email: String }, RR],
RR := (R & Tag[Person]) ++ % { val email: String },
): RR = p + (email = s"${localPart}@${domain}")
}
}
Expand All @@ -73,15 +70,13 @@ val person = %(name = "tarao fuguta", age = 3)
.withEmail("example.com")
```

There is also `typing.Record.Concat` to calculate concatenation of two record types. The
above example can be rewritten with `Concat` as the following.
It is also possible to calculate concatenation of two record types in the same way. The
above example can be rewritten as the following.

```scala mdoc:nest:invisible
```

```scala mdoc:mline
import com.github.tarao.record4s.typing.Record.Concat

trait Person
object Person {
extension [R <: % { val name: String }](p: R & Tag[Person]) {
Expand All @@ -91,7 +86,7 @@ object Person {
domain: String,
localPart: String = p.firstName,
)(using
Concat.Aux[R & Tag[Person], % { val email: String }, RR],
RR := (R & Tag[Person]) ++ % { val email: String },
): RR = p ++ %(email = s"${localPart}@${domain}")
}
}
Expand All @@ -101,11 +96,11 @@ Concatenating Two Generic Records
---------------------------------

You may think that you can define a method to concatenate two generic records by using
`Concat` but it doesn't work in a simple way.
`typing.syntax.++` but it doesn't work in a simple way.

```scala mdoc:fail
def concat[R1 <: %, R2 <: %, RR <: %](r1: R1, r2: R2)(using
Concat.Aux[R1, R2, RR],
RR := R1 ++ R2,
): RR = r1 ++ r2
```

Expand All @@ -114,7 +109,7 @@ concrete type for type safety. In this case, defining an inline method makes it

```scala mdoc:mline
inline def concat[R1 <: %, R2 <: %, RR <: %](r1: R1, r2: R2)(using
Concat.Aux[R1, R2, RR],
RR := R1 ++ R2,
): RR = r1 ++ r2

concat(%(name = "tarao"), %(age = 3))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,13 +454,16 @@ object ArrayRecord
* @return
* a new record without the unselected fields
*/
inline def apply[U <: Tuple, RR <: %](u: Unselector[U])(using
ev: Unselect.Aux[R, U, RR],
inline def apply[U <: Tuple, R2 <: %, RR <: %](u: Unselector[U])(using
r: typing.Record.Aux[ArrayRecord[R], R2],
ev: Unselect.Aux[R2, U, RR],
rr: RecordLike[RR],
): ArrayRecord[Tuple.Zip[rr.ElemLabels, rr.ElemTypes]] =
withPotentialTypingError {
record.shrinkTo[Tuple.Zip[rr.ElemLabels, rr.ElemTypes]]
}
withPotentialTypingError {
record.shrinkTo[Tuple.Zip[rr.ElemLabels, rr.ElemTypes]]
}(using ev)
}(using r)

/** Convert this record to a `To`.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ object Macros {
}
}

def derivedTypingUnselectImpl[R: Type, U <: Tuple: Type](using
def derivedTypingUnselectImpl[R <: `%`: Type, U <: Tuple: Type](using
Quotes,
): Expr[Unselect[R, U]] = withTyping {
import internal.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,9 @@ object Record extends RecordPlatformSpecific {
* @return
* a new record without the unselected fields
*/
inline def apply[U <: Tuple, RR <: %](u: Unselector[U])(using
inline def apply[U <: Tuple, RR >: R <: %](u: Unselector[U])(using
Unselect.Aux[R, U, RR],
RecordLike[RR],
R <:< RR,
): RR = withPotentialTypingError {
newMapRecord[RR](summon[RecordLike[RR]].tidiedIterableOf(record))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,6 @@ object ArrayRecord {
${ ArrayRecordMacros.derivedTypingConcatImpl }
}

type Append[R1, R2] = Concat[R1, R2]

object Append {
type Aux[R1, R2, Out0 <: ProductRecord] = Concat[R1, R2] {
type Out = Out0
}
}

@implicitNotFound("Value '${Label}' is not a member of ${R}")
final class Lookup[R, Label] private () {
type Out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ object Record {
${ Macros.derivedTypingConcatImpl }
}

type Append[R1, R2] = Concat[R1, R2]

object Append {
type Aux[R1, R2, Out0 <: %] = Concat[R1, R2] { type Out = Out0 }
}

@implicitNotFound("Value '${Label}' is not a member of ${R}")
final class Lookup[R, Label] private () {
type Out
Expand Down Expand Up @@ -68,16 +62,16 @@ object Record {
${ Macros.derivedTypingSelectImpl }
}

final class Unselect[R, U] private extends MaybeError {
type Out <: %
final class Unselect[R <: %, U] private extends MaybeError {
type Out >: R <: %
}

object Unselect {
private[record4s] val instance = new Unselect[Nothing, Nothing]

type Aux[R, U, Out0 <: %] = Unselect[R, U] { type Out = Out0 }
type Aux[R <: %, U, Out0 <: %] = Unselect[R, U] { type Out = Out0 }

transparent inline given [R: RecordLike, S <: Tuple]: Unselect[R, S] =
transparent inline given [R <: %, S <: Tuple]: Unselect[R, S] =
${ Macros.derivedTypingUnselectImpl }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2023 record4s authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.tarao.record4s
package typing

object syntax {
type :=[Out0, A] = A match {
case Record.Concat[r1, r2] => Record.Concat.Aux[r1, r2, Out0]
case Record.Unselect[r, u] => Record.Unselect.Aux[r, u, Out0]
case Record.Lookup[r, l] => Record.Lookup.Aux[r, l, Out0]
case ArrayRecord.Concat[r1, r2] => ArrayRecord.Concat.Aux[r1, r2, Out0]
case ArrayRecord.Lookup[r, l] =>
Out0 match {
case (o, i) =>
ArrayRecord.Lookup[r, l] {
type Out = o
type Index = i
}
}
}

type ++[R1, R2] = R1 match {
case % => Record.Concat[R1, R2]
case Tuple => ArrayRecord.Concat[R1, R2]
}

type +[R, F <: Tuple] = R match {
case % => Record.Concat[R, F *: EmptyTuple]
case Tuple => ArrayRecord.Concat[R, F *: EmptyTuple]
}

type --[R <: %, U <: Tuple] = Record.Unselect[R, U]

type -[R <: %, L] = Record.Unselect[R, L *: EmptyTuple]

infix type in[L, R] = R match {
case % => Record.Lookup[R, L]
case Tuple => ArrayRecord.Lookup[R, L]
}
}
Loading

0 comments on commit 06bbc14

Please sign in to comment.