record4s

Defining Methods on Records

Extension Methods

You can define methods on records by defining extension methods in a usual manner.

object Extension {
  extension (p: % { val name: String; val age: Int }) {
    def firstName: String = p.name.split(" ").head
  }
}
import Extension.firstName

val person = %(name = "tarao fuguta", age = 3)
// person: % {
//   val name: String
//   val age: Int
// } = %(name = tarao fuguta, age = 3)

person.firstName
// res0: String = "tarao"

As you see, you need to explicitly import the method in this case.

Extension Methods with Tags

Records can be tagged and it enables extension methods to be found automatically.

import com.github.tarao.record4s.Tag

trait Person
object Person {
  extension (p: % { val name: String; val age: Int } & Tag[Person]) {
    def firstName: String = p.name.split(" ").head
  }
}
val person0 = %(name = "tarao fuguta", age = 3)
// person0: % {
//   val name: String
//   val age: Int
// } = %(name = tarao fuguta, age = 3)

val person = person0.tag[Person]
// person: % {
//   val name: String
//   val age: Int
// } & Tag[Person] = %(name = tarao fuguta, age = 3)

person.firstName
// res2: String = "tarao"

The mechanism is that the type of person is an intersection type with Tag[Person], which adds extension methods in object Person to the target of implicit search at the method invocation on person. Without the tag, the method invocation statically fails.

person0.firstName
// error: 
// value firstName is not a member of com.github.tarao.record4s.%{val name: String; val age: Int}
// error: 
// Line is indented too far to the left, or a `}` is missing
// error: 
// Line is indented too far to the left, or a `}` is missing

Methods on Generic Record Types

Extension methods can be generic ⸺ i.e., you can specify the least fields that are necessary for the methods.

trait Person
object Person {
  extension [R <: % { val name: String }](p: R & Tag[Person]) {
    def firstName: String = p.name.split(" ").head
  }
}
val person = %(name = "tarao fuguta", age = 3).tag[Person]
// person: % {
//   val name: String
//   val age: Int
// } & Tag[Person] = %(name = tarao fuguta, age = 3)
person.firstName
// res5: String = "tarao"

val personWithEmail = person + (email = "tarao@example.com")
// personWithEmail: % {
//   val name: String
//   val age: Int
//   val email: String
// } & Tag[Person] = %(name = tarao fuguta, age = 3, email = tarao@example.com)
personWithEmail.firstName
// res6: String = "tarao"