diff --git a/build.sbt b/build.sbt index e203eb8..ad74189 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ import ProjectKeys._ import Implicits._ -ThisBuild / tlBaseVersion := "0.11" +ThisBuild / tlBaseVersion := "0.12" ThisBuild / projectName := "record4s" ThisBuild / groupId := "com.github.tarao" diff --git a/docs/advanced/generic.md b/docs/advanced/generic.md index d640322..6398d4f 100644 --- a/docs/advanced/generic.md +++ b/docs/advanced/generic.md @@ -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") @@ -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 @@ -40,7 +37,7 @@ 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 @@ -48,7 +45,7 @@ by `email` field of E-mail address, whose local part is filled by the first segm ```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 { @@ -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}") } } @@ -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]) { @@ -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}") } } @@ -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 ``` @@ -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)) diff --git a/modules/core/src/main/scala/com/github/tarao/record4s/ArrayRecord.scala b/modules/core/src/main/scala/com/github/tarao/record4s/ArrayRecord.scala index 52d3772..db1ac5f 100644 --- a/modules/core/src/main/scala/com/github/tarao/record4s/ArrayRecord.scala +++ b/modules/core/src/main/scala/com/github/tarao/record4s/ArrayRecord.scala @@ -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`. * diff --git a/modules/core/src/main/scala/com/github/tarao/record4s/Macros.scala b/modules/core/src/main/scala/com/github/tarao/record4s/Macros.scala index 610bd93..c5f371e 100644 --- a/modules/core/src/main/scala/com/github/tarao/record4s/Macros.scala +++ b/modules/core/src/main/scala/com/github/tarao/record4s/Macros.scala @@ -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.* diff --git a/modules/core/src/main/scala/com/github/tarao/record4s/Record.scala b/modules/core/src/main/scala/com/github/tarao/record4s/Record.scala index 33dc02c..188b41d 100644 --- a/modules/core/src/main/scala/com/github/tarao/record4s/Record.scala +++ b/modules/core/src/main/scala/com/github/tarao/record4s/Record.scala @@ -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)) } diff --git a/modules/core/src/main/scala/com/github/tarao/record4s/typing/ArrayRecord.scala b/modules/core/src/main/scala/com/github/tarao/record4s/typing/ArrayRecord.scala index ec000a8..cbfd61b 100644 --- a/modules/core/src/main/scala/com/github/tarao/record4s/typing/ArrayRecord.scala +++ b/modules/core/src/main/scala/com/github/tarao/record4s/typing/ArrayRecord.scala @@ -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 diff --git a/modules/core/src/main/scala/com/github/tarao/record4s/typing/Record.scala b/modules/core/src/main/scala/com/github/tarao/record4s/typing/Record.scala index 77bfd75..eaaabb5 100644 --- a/modules/core/src/main/scala/com/github/tarao/record4s/typing/Record.scala +++ b/modules/core/src/main/scala/com/github/tarao/record4s/typing/Record.scala @@ -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 @@ -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 } } } diff --git a/modules/core/src/main/scala/com/github/tarao/record4s/typing/syntax.scala b/modules/core/src/main/scala/com/github/tarao/record4s/typing/syntax.scala new file mode 100644 index 0000000..4a44316 --- /dev/null +++ b/modules/core/src/main/scala/com/github/tarao/record4s/typing/syntax.scala @@ -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] + } +} diff --git a/modules/core/src/test/scala/external/UseCaseSpec.scala b/modules/core/src/test/scala/external/UseCaseSpec.scala index 0abaf76..0e9717e 100644 --- a/modules/core/src/test/scala/external/UseCaseSpec.scala +++ b/modules/core/src/test/scala/external/UseCaseSpec.scala @@ -19,12 +19,12 @@ package external class UseCaseSpec extends helper.UnitSpec { describe("Generic record lookup") { import com.github.tarao.record4s.{%, Record} - import com.github.tarao.record4s.typing.Record.Lookup + import com.github.tarao.record4s.typing.syntax.{:=, in} - it("can be done by using Lookup") { - def getEmail[R <: %](record: R)(using - hasEmail: Lookup[R, "email"], - ): hasEmail.Out = Record.lookup(record, "email") + it("can be done by using typing.syntax") { + def getEmail[R <: %, V](record: R)(using + V := ("email" in R), + ): V = Record.lookup(record, "email") val r0 = %(name = "tarao", age = 3) val r1 = r0 + (email = "tarao@example.com") @@ -35,26 +35,11 @@ class UseCaseSpec extends helper.UnitSpec { describe("Generic record extension with ++") { import com.github.tarao.record4s.{%, Tag} - import com.github.tarao.record4s.typing.Record.Concat - - it("can be done by using Concat") { - def addEmail[R <: %](record: R, email: String)(using - concat: Concat[R, % { val email: String }], - ): concat.Out = record ++ %(email = email) + import com.github.tarao.record4s.typing.syntax.{++, :=} - val r0 = %(name = "tarao", age = 3) - val r1 = addEmail(r0, "tarao@example.com") - r1 shouldStaticallyBe a[ - % { val name: String; val age: Int; val email: String }, - ] - r1.name shouldBe "tarao" - r1.age shouldBe 3 - r1.email shouldBe "tarao@example.com" - } - - it("can be done by using Concat.Aux") { + it("can be done by using typing.syntax") { def addEmail[R <: %, RR <: %](record: R, email: String)(using - Concat.Aux[R, % { val email: String }, RR], + RR := R ++ % { val email: String }, ): RR = record ++ %(email = email) val r0 = %(name = "tarao", age = 3) @@ -69,7 +54,7 @@ class UseCaseSpec extends helper.UnitSpec { it("can replace existing field") { def addEmail[R <: %, T, RR <: %](record: R, email: T)(using - Concat.Aux[R, % { val email: T }, RR], + RR := R ++ % { val email: T }, ): RR = record ++ %(email = email) val r0 = %(name = "tarao", age = 3, email = "tarao@example.com") @@ -93,13 +78,13 @@ class UseCaseSpec extends helper.UnitSpec { def firstName: String = p.name.split(" ").head def withEmail[RR <: %](email: String)(using - Concat.Aux[R & Tag[Person], % { val email: String }, RR], + RR := (R & Tag[Person]) ++ % { val email: String }, ): RR = p ++ %(email = email) } } def addEmail[R <: %, RR <: %](record: R, email: String)(using - Concat.Aux[R, % { val email: String }, RR], + RR := R ++ % { val email: String }, ): RR = record ++ %(email = email) val r0 = %(name = "tarao fuguta", age = 3).tag[Person] @@ -122,7 +107,7 @@ class UseCaseSpec extends helper.UnitSpec { r2.email shouldBe "tarao@example.com" } - it("should reject generic record concatenation without using Concat") { + it("should reject generic record concatenation without using ++ type") { """ def addEmail[R <: %](record: R, email: String): Any = record ++ %(email = email) @@ -132,28 +117,13 @@ class UseCaseSpec extends helper.UnitSpec { describe("Generic record extension with +") { import com.github.tarao.record4s.{%, Tag} - import com.github.tarao.record4s.typing.Record.Append + import com.github.tarao.record4s.typing.syntax.{+, ++, :=} - it("can be done by using Append") { + it("can be done by using typing.syntax") { locally { - def addEmail[R <: %](record: R, email: String)(using - append: Append[R, % { val email: String }], - ): append.Out = record + (email = email) - - val r0 = %(name = "tarao", age = 3) - val r1 = addEmail(r0, "tarao@example.com") - r1 shouldStaticallyBe a[ - % { val name: String; val age: Int; val email: String }, - ] - r1.name shouldBe "tarao" - r1.age shouldBe 3 - r1.email shouldBe "tarao@example.com" - } - - locally { - def addEmail[R <: %](record: R, email: String)(using - append: Append[R, ("email", String) *: EmptyTuple], - ): append.Out = record + (email = email) + def addEmail[R <: %, RR <: %](record: R, email: String)(using + RR := R ++ % { val email: String }, + ): RR = record + (email = email) val r0 = %(name = "tarao", age = 3) val r1 = addEmail(r0, "tarao@example.com") @@ -164,12 +134,10 @@ class UseCaseSpec extends helper.UnitSpec { r1.age shouldBe 3 r1.email shouldBe "tarao@example.com" } - } - it("can be done by using Append.Aux") { locally { def addEmail[R <: %, RR <: %](record: R, email: String)(using - Append.Aux[R, % { val email: String }, RR], + RR := R ++ ("email", String) *: EmptyTuple, ): RR = record + (email = email) val r0 = %(name = "tarao", age = 3) @@ -184,7 +152,7 @@ class UseCaseSpec extends helper.UnitSpec { locally { def addEmail[R <: %, RR <: %](record: R, email: String)(using - Append.Aux[R, ("email", String) *: EmptyTuple, RR], + RR := R + ("email", String), ): RR = record + (email = email) val r0 = %(name = "tarao", age = 3) @@ -200,7 +168,7 @@ class UseCaseSpec extends helper.UnitSpec { it("can replace existing field") { def addEmail[R <: %, T, RR <: %](record: R, email: T)(using - Append.Aux[R, % { val email: T }, RR], + RR := R ++ % { val email: T }, ): RR = record + (email = email) val r0 = %(name = "tarao", age = 3, email = "tarao@example.com") @@ -224,13 +192,13 @@ class UseCaseSpec extends helper.UnitSpec { def firstName: String = p.name.split(" ").head def withEmail[RR <: %](email: String)(using - Append.Aux[R & Tag[Person], % { val email: String }, RR], + RR := (R & Tag[Person]) ++ % { val email: String }, ): RR = p + (email = email) } } def addEmail[R <: %, RR <: %](record: R, email: String)(using - Append.Aux[R, % { val email: String }, RR], + RR := R ++ % { val email: String }, ): RR = record + (email = email) val r0 = %(name = "tarao fuguta", age = 3).tag[Person] @@ -253,7 +221,7 @@ class UseCaseSpec extends helper.UnitSpec { r2.email shouldBe "tarao@example.com" } - it("should reject generic record extension without using Append") { + it("should reject generic record extension without using ++ type") { """ def addEmail[R <: %](record: R, email: String): Any = record + (email = email) @@ -261,14 +229,59 @@ class UseCaseSpec extends helper.UnitSpec { } } + describe("Generic record upcast") { + import com.github.tarao.record4s.{%, Tag} + import com.github.tarao.record4s.typing.syntax.{-, --, :=} + + it("can be done by using --") { + def withoutAge[R <: %, RR >: R <: %](record: R)(using + RR := R -- "age" *: EmptyTuple, + ): RR = record + + val r0 = %(name = "tarao", age = 3, email = "tarao@example.com") + val r1 = withoutAge(r0) + r1.name shouldBe "tarao" + r1.email shouldBe "tarao@example.com" + "r1.age" shouldNot typeCheck + } + + it("can be done by using -") { + def withoutAge[R <: %, RR >: R <: %](record: R)(using + RR := R - "age", + ): RR = record + + val r0 = %(name = "tarao", age = 3, email = "tarao@example.com") + val r1 = withoutAge(r0) + r1.name shouldBe "tarao" + r1.email shouldBe "tarao@example.com" + "r1.age" shouldNot typeCheck + } + + it("preserves a tag") { + def withoutAge[R <: %, RR >: R <: %](record: R)(using + RR := R -- "age" *: EmptyTuple, + ): RR = record + + trait Person + + val r0 = + %(name = "tarao", age = 3, email = "tarao@example.com").tag[Person] + val r1 = withoutAge(r0) + r1.name shouldBe "tarao" + r1.email shouldBe "tarao@example.com" + "r1.age" shouldNot typeCheck + r1 shouldStaticallyBe a[Tag[Person]] + } + } + describe("Generic array record lookup") { import com.github.tarao.record4s.ArrayRecord - import com.github.tarao.record4s.typing.ArrayRecord.Lookup + import com.github.tarao.record4s.typing.syntax.{:=, in} it("can be done by using Lookup") { - inline def getEmail[R](record: ArrayRecord[R])(using - hasEmail: Lookup[R, "email"], - ): hasEmail.Out = ArrayRecord.lookup(record, "email") + inline def getEmail[R <: Tuple, V, I <: Int](record: ArrayRecord[R])(using + (V, I) := ("email" in R), + ): V = ArrayRecord.lookup(record, "email") val r0 = ArrayRecord(name = "tarao", age = 3) val r1 = r0 + (email = "tarao@example.com") @@ -279,30 +292,14 @@ class UseCaseSpec extends helper.UnitSpec { describe("Generic array record extension with ++") { import com.github.tarao.record4s.{ArrayRecord, ProductRecord, Tag} - import com.github.tarao.record4s.typing.ArrayRecord.Concat - - it("can be done by using Concat") { - def addEmail[R](record: ArrayRecord[R], email: String)(using - concat: Concat[R, ArrayRecord[("email", String) *: EmptyTuple]], - ): concat.Out = record ++ ArrayRecord(email = email) + import com.github.tarao.record4s.typing.syntax.{++, :=} - val r0 = ArrayRecord(name = "tarao", age = 3) - val r1 = addEmail(r0, "tarao@example.com") - r1 shouldStaticallyBe an[ArrayRecord[ - (("name", String), ("age", Int), ("email", String)), - ]] - r1.name shouldBe "tarao" - r1.age shouldBe 3 - r1.email shouldBe "tarao@example.com" - } - - it("can be done by using Concat.Aux") { - def addEmail[R, RR <: ProductRecord]( + it("can be done by using typing.syntax") { + def addEmail[R <: Tuple, RR <: ProductRecord]( record: ArrayRecord[R], email: String, - )(using - Concat.Aux[R, ArrayRecord[("email", String) *: EmptyTuple], RR], - ): RR = record ++ ArrayRecord(email = email) + )(using RR := R ++ ArrayRecord[("email", String) *: EmptyTuple]): RR = + record ++ ArrayRecord(email = email) val r0 = ArrayRecord(name = "tarao", age = 3) val r1 = addEmail(r0, "tarao@example.com") @@ -316,12 +313,11 @@ class UseCaseSpec extends helper.UnitSpec { it("can replace existing field") { locally { - def addEmail[R, T, RR <: ProductRecord]( + def addEmail[R <: Tuple, T, RR <: ProductRecord]( record: ArrayRecord[R], email: T, - )(using - Concat.Aux[R, ArrayRecord[("email", T) *: EmptyTuple], RR], - ): RR = record ++ ArrayRecord(email = email) + )(using RR := R ++ ArrayRecord[("email", T) *: EmptyTuple]): RR = + record ++ ArrayRecord(email = email) val r0 = ArrayRecord(name = "tarao", age = 3, email = "tarao@example.com") @@ -341,8 +337,11 @@ class UseCaseSpec extends helper.UnitSpec { } locally { - def rename[R, T, RR <: ProductRecord](record: ArrayRecord[R], value: T)( - using Concat.Aux[R, ArrayRecord[("name", T) *: EmptyTuple], RR], + def rename[R <: Tuple, T, RR <: ProductRecord]( + record: ArrayRecord[R], + value: T, + )(using + RR := R ++ ArrayRecord[("name", T) *: EmptyTuple], ): RR = record ++ ArrayRecord(name = value) val r0 = @@ -371,17 +370,17 @@ class UseCaseSpec extends helper.UnitSpec { def firstName: String = p.name.split(" ").head def withEmail[RR <: ProductRecord](email: String)(using - Concat.Aux[ - (("name", String) *: T) & Tag[Person], + RR := ((("name", String) *: T) & Tag[Person]) ++ ArrayRecord[("email", String) *: EmptyTuple], - RR, - ], ): RR = p ++ ArrayRecord(email = email) } } - def addEmail[R, T, RR <: ProductRecord](record: ArrayRecord[R], email: T)( - using Concat.Aux[R, ArrayRecord[("email", T) *: EmptyTuple], RR], + def addEmail[R <: Tuple, T, RR <: ProductRecord]( + record: ArrayRecord[R], + email: T, + )(using + RR := R ++ ArrayRecord[("email", T) *: EmptyTuple], ): RR = record ++ ArrayRecord(email = email) val r0 = ArrayRecord(name = "tarao fuguta", age = 3).tag[Person] @@ -414,28 +413,15 @@ class UseCaseSpec extends helper.UnitSpec { describe("Generic array record extension with +") { import com.github.tarao.record4s.{%, ArrayRecord, ProductRecord, Tag} - import com.github.tarao.record4s.typing.ArrayRecord.Append + import com.github.tarao.record4s.typing.syntax.{+, ++, :=} - it("can be done by using Append") { + it("can be done by using typing.syntax") { locally { - def addEmail[R](record: ArrayRecord[R], email: String)(using - append: Append[R, ("email", String) *: EmptyTuple], - ): append.Out = record + (email = email) - - val r0 = ArrayRecord(name = "tarao", age = 3) - val r1 = addEmail(r0, "tarao@example.com") - r1 shouldStaticallyBe a[ArrayRecord[ - (("name", String), ("age", Int), ("email", String)), - ]] - r1.name shouldBe "tarao" - r1.age shouldBe 3 - r1.email shouldBe "tarao@example.com" - } - - locally { - def addEmail[R](record: ArrayRecord[R], email: String)(using - append: Append[R, % { val email: String }], - ): append.Out = record + (email = email) + def addEmail[R <: Tuple, RR <: ProductRecord]( + record: ArrayRecord[R], + email: String, + )(using RR := R ++ ("email", String) *: EmptyTuple): RR = + record + (email = email) val r0 = ArrayRecord(name = "tarao", age = 3) val r1 = addEmail(r0, "tarao@example.com") @@ -446,16 +432,13 @@ class UseCaseSpec extends helper.UnitSpec { r1.age shouldBe 3 r1.email shouldBe "tarao@example.com" } - } - it("can be done by using Append.Aux") { locally { - def addEmail[R, RR <: ProductRecord]( + def addEmail[R <: Tuple, RR <: ProductRecord]( record: ArrayRecord[R], email: String, - )(using - Append.Aux[R, ("email", String) *: EmptyTuple, RR], - ): RR = record + (email = email) + )(using RR := R ++ % { val email: String }): RR = + record + (email = email) val r0 = ArrayRecord(name = "tarao", age = 3) val r1 = addEmail(r0, "tarao@example.com") @@ -468,12 +451,11 @@ class UseCaseSpec extends helper.UnitSpec { } locally { - def addEmail[R, RR <: ProductRecord]( + def addEmail[R <: Tuple, RR <: ProductRecord]( record: ArrayRecord[R], email: String, - )(using - Append.Aux[R, % { val email: String }, RR], - ): RR = record + (email = email) + )(using RR := R + ("email", String)): RR = + record + (email = email) val r0 = ArrayRecord(name = "tarao", age = 3) val r1 = addEmail(r0, "tarao@example.com") @@ -488,12 +470,11 @@ class UseCaseSpec extends helper.UnitSpec { it("can replace existing field") { locally { - def addEmail[R, T, RR <: ProductRecord]( + def addEmail[R <: Tuple, T, RR <: ProductRecord]( record: ArrayRecord[R], email: T, - )(using - Append.Aux[R, ("email", T) *: EmptyTuple, RR], - ): RR = record + (email = email) + )(using RR := R ++ ("email", T) *: EmptyTuple): RR = + record + (email = email) val r0 = ArrayRecord(name = "tarao", age = 3, email = "tarao@example.com") @@ -513,8 +494,11 @@ class UseCaseSpec extends helper.UnitSpec { } locally { - def rename[R, T, RR <: ProductRecord](record: ArrayRecord[R], value: T)( - using Append.Aux[R, ("name", T) *: EmptyTuple, RR], + def rename[R <: Tuple, T, RR <: ProductRecord]( + record: ArrayRecord[R], + value: T, + )(using + RR := R ++ ("name", T) *: EmptyTuple, ): RR = record + (name = value) val r0 = @@ -543,17 +527,17 @@ class UseCaseSpec extends helper.UnitSpec { def firstName: String = p.name.split(" ").head def withEmail[RR <: ProductRecord](email: String)(using - Append.Aux[ - (("name", String) *: T) & Tag[Person], + RR := ((("name", String) *: T) & Tag[Person]) ++ ("email", String) *: EmptyTuple, - RR, - ], ): RR = p + (email = email) } } - def addEmail[R, T, RR <: ProductRecord](record: ArrayRecord[R], email: T)( - using Append.Aux[R, ("email", T) *: EmptyTuple, RR], + def addEmail[R <: Tuple, T, RR <: ProductRecord]( + record: ArrayRecord[R], + email: T, + )(using + RR := R ++ ("email", T) *: EmptyTuple, ): RR = record + (email = email) val r0 = ArrayRecord(name = "tarao fuguta", age = 3).tag[Person]