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)