Records in Shapeless provide structured data access using field names rather than positions, offering a more user-friendly interface for working with case classes and other product types. Records enable dynamic field access while maintaining compile-time type safety.
Fundamental types for creating and working with named field access.
// Field with singleton-typed key K and value type V
type FieldType[K, +V] = V @@ KeyTag[K, V]
// Phantom tag for associating keys with values
trait KeyTag[K, +V]
// Type alias for records (HList of FieldType)
type Record = HList // where elements are FieldType[_, _]Building blocks for creating fields with typed keys.
// Field builder for key K
object field[K] extends FieldOf[K]
trait FieldOf[K] {
def apply[V](value: V): FieldType[K, V]
}
// Field builder that applies values to create fields
trait FieldBuilder[K] {
def apply[V](v: V): FieldType[K, V]
}
// Usage:
val nameField = field['name]("Alice")
val ageField = field['age](30)
val record = nameField :: ageField :: HNilDynamic construction of records using method names as field keys.
object Record {
// Dynamic record construction via method calls
def applyDynamic(method: String)(args: Any*): HList
// Named argument construction
def applyDynamicNamed(method: String)(args: (String, Any)*): HList
// Dynamic field type selection
def selectDynamic(field: String): Any
}
// Dynamic argument mapping for records
trait RecordArgs[R <: HList] {
def apply(rec: R): Any
}
// Inverse mapping from records to named arguments
trait FromRecordArgs[A] {
type Out <: HList
def apply(args: A): Out
}Core operations for accessing and manipulating record fields.
// Select field with key K from record L
trait Selector[L <: HList, K] {
type Out
def apply(l: L): Out
}
// Update field F in record L
trait Updater[L <: HList, F] {
type Out <: HList
def apply(l: L, f: F): Out
}
// Remove field with key K from record L
trait Remover[L <: HList, K] {
type Out <: HList
def apply(l: L): (Out, FieldType[K, _])
}
// Usage:
val record = field['name]("Bob") :: field['age](25) :: HNil
val name = record.get('name) // "Bob"
val updated = record.updated('age, 26)
val (remaining, removed) = record.remove('name)Operations for extracting and manipulating record structure.
// Extract all keys from record L as HList
trait Keys[L <: HList] {
type Out <: HList
def apply(l: L): Out
}
// Extract all values from record L as HList
trait Values[L <: HList] {
type Out <: HList
def apply(l: L): Out
}
// Merge two records L and M
trait Merger[L <: HList, M <: HList] {
type Out <: HList
def apply(l: L, m: M): Out
}
// Rename fields according to key mapping
trait Renamer[L <: HList, KS <: HList] {
type Out <: HList
def apply(l: L): Out
}
// Example:
val keys = record.keys // 'name :: 'age :: HNil
val values = record.values // "Bob" :: 25 :: HNilInfrastructure for working with labeled types and symbolic labels.
// Extract symbolic labels from case class/sealed trait
trait DefaultSymbolicLabelling[T] {
type Out
def apply(): Out
}
// Base trait for polymorphic functions preserving field keys
trait FieldPoly extends Poly
// Type class witnessing field value types
trait FieldOf[V] {
type Out <: HList
def apply(): Out
}
// Create field type from key and value
def fieldType[K, V](key: K, value: V): FieldType[K, V]import shapeless._, record._, syntax.singleton._
// Create record using field syntax
val person = ('name ->> "Alice") :: ('age ->> 30) :: ('active ->> true) :: HNil
// Access fields by name
val name: String = person('name)
val age: Int = person('age)
val active: Boolean = person('active)
// Update fields
val older = person.updated('age, 31)
val renamed = person.updated('name, "Alicia")
// Remove fields
val (withoutAge, ageField) = person.remove('age)
// withoutAge: ('name ->> String) :: ('active ->> Boolean) :: HNil// Using dynamic syntax
val dynamicRecord = Record.empty
.add('firstName, "John")
.add('lastName, "Doe")
.add('email, "john.doe@example.com")
// Access dynamically
val firstName = dynamicRecord.get('firstName) // "John"
val email = dynamicRecord.get('email) // "john.doe@example.com"val address = ('street ->> "123 Main St") :: ('city ->> "Anytown") :: HNil
val contact = ('phone ->> "555-1234") :: ('email ->> "test@example.com") :: HNil
// Merge records
val combined = person ++ address ++ contact
// Extract keys and values
val allKeys = combined.keys
val allValues = combined.values
// Transform values while preserving keys
object addPrefix extends FieldPoly {
implicit def stringCase[K] = atField[K, String](s => s"prefix_$s")
implicit def intCase[K] = atField[K, Int](i => i * 100)
}
val transformed = combined.map(addPrefix)case class Employee(name: String, id: Int, department: String, salary: Double)
val emp = Employee("Sarah", 12345, "Engineering", 95000.0)
// Convert to labeled generic (preserves field names)
val lgen = LabelledGeneric[Employee]
val empRecord = lgen.to(emp)
// Result has field names as singleton types
// Access by field name
val empName = empRecord.get('name) // "Sarah"
val empId = empRecord.get('id) // 12345
val empDept = empRecord.get('department) // "Engineering"
// Update and convert back
val promoted = empRecord.updated('salary, 105000.0)
val updatedEmp = lgen.from(promoted)
// Employee("Sarah", 12345, "Engineering", 105000.0)// Define validation rules
object validateRecord extends FieldPoly {
implicit def nameValidation = atField['name, String] { name =>
require(name.nonEmpty, "Name cannot be empty")
name
}
implicit def ageValidation = atField['age, Int] { age =>
require(age >= 0, "Age must be non-negative")
age
}
}
// Apply validations
try {
val validatedPerson = person.map(validateRecord)
println("Validation passed")
} catch {
case e: IllegalArgumentException => println(s"Validation failed: ${e.getMessage}")
}// Define key mapping for renaming
val keyMapping = ('name ->> 'fullName) :: ('age ->> 'yearsOld) :: HNil
// Rename fields
val renamedPerson = person.renameFields(keyMapping)
// Result: ('fullName ->> "Alice") :: ('yearsOld ->> 30) :: ('active ->> true) :: HNil
val fullName = renamedPerson('fullName) // "Alice"
val yearsOld = renamedPerson('yearsOld) // 30val optionalRecord =
('name ->> Some("Bob")): FieldType['name, Option[String]] ::
('age ->> None): FieldType['age, Option[Int]] ::
('email ->> Some("bob@example.com")): FieldType['email, Option[String]] ::
HNil
// Extract present values
object extractPresent extends FieldPoly {
implicit def optionCase[K, V] = atField[K, Option[V]] {
case Some(v) => v
case None => throw new NoSuchElementException(s"Field not present")
}
}
// Get only present values (will throw for None)
val name = optionalRecord.get('name).get // "Bob"
val email = optionalRecord.get('email).get // "bob@example.com"