Generic Records
Generic Field Lookup
It is possible to retrieve a field value of an arbitrary type from records by using
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.
import com.github.tarao.record4s.{%, Record}
import com.github.tarao.record4s.typing.syntax.{:=, in}
def getValue[R <: %, V](record: R)(using
V := ("value" in R),
): V = Record.lookup(record, "value")
val r1 = %(value = "tarao")
// r1: % {
// val value: String
// } = %(value = tarao)
val r2 = %(value = 3)
// r2: % {
// val value: Int
// } = %(value = 3)
getValue(r1)
// res0: String = "tarao"
getValue(r2)
// res1: Int = 3
Of course, it doesn't compile for a record without value
field.
val r3 = %(name = "tarao")
getValue(r3)
// error:
// Value '("value" : String)' is not a member of com.github.tarao.record4s.%{val name: String}.
// I found:
//
// com.github.tarao.record4s.typing.Record.Lookup.given_Lookup_R_L[
// com.github.tarao.record4s.%{val name: String}, ("value" : String)]
//
// But given instance given_Lookup_R_L in object Lookup does not match type com.github.tarao.record4s.typing.Record.Lookup.Aux[
// com.github.tarao.record4s.%{val name: String}, ("value" : String), Any].
// RR := (R & Tag[Person]) ++ % { val email: String },
//
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.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.
import com.github.tarao.record4s.Tag
import com.github.tarao.record4s.typing.syntax.++
trait Person
object Person {
extension [R <: % { val name: String }](p: R & Tag[Person]) {
def firstName: String = p.name.split(" ").head
def withEmail[RR <: %](
domain: String,
localPart: String = p.firstName,
)(using
RR := (R & Tag[Person]) ++ % { val email: String },
): RR = p + (email = s"${localPart}@${domain}")
}
}
The method can be called on an arbitrary record with name
field and Person
tag.
val person = %(name = "tarao fuguta", age = 3)
.tag[Person]
.withEmail("example.com")
// person: % {
// val name: String
// val age: Int
// val email: String
// } & Tag[Person] = %(name = tarao fuguta, age = 3, email = tarao@example.com)
It is also possible to calculate concatenation of two record types in the same way. The above example can be rewritten as the following.
trait Person
object Person {
extension [R <: % { val name: String }](p: R & Tag[Person]) {
def firstName: String = p.name.split(" ").head
def withEmail[RR <: %](
domain: String,
localPart: String = p.firstName,
)(using
RR := (R & Tag[Person]) ++ % { val email: String },
): RR = p ++ %(email = s"${localPart}@${domain}")
}
}
Concatenating Two Generic Records
You may think that you can define a method to concatenate two generic records by using
typing.syntax.++
but it doesn't work in a simple way.
def concat[R1 <: %, R2 <: %, RR <: %](r1: R1, r2: R2)(using
RR := R1 ++ R2,
): RR = r1 ++ r2
// error:
// A concrete type expected but type variable R2 is given.
// Did you forget to make the method inline?
// ): RR = r1 ++ r2
// ^^^^^^^^
// error:
// Found: <notype>
// Required: com.github.tarao.record4s.typing.Concrete[R2]
The problem is that the right-hand-side argument type of ++
is restricted to be a
concrete type for type safety. In this case, defining an inline method makes it work.
inline def concat[R1 <: %, R2 <: %, RR <: %](r1: R1, r2: R2)(using
RR := R1 ++ R2,
): RR = r1 ++ r2
concat(%(name = "tarao"), %(age = 3))
// res4: % {
// val name: String
// val age: Int
// } = %(name = tarao, age = 3)