or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-utilities.mdcoproduct-unions.mdgeneric-derivation.mdhlist-collections.mdindex.mdoptics-lenses.mdpoly-typelevel.mdrecords-fields.md
tile.json

records-fields.mddocs/

Records and Named Field Access

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.

Capabilities

Core Record Types

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[_, _]

Field Construction

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 :: HNil

Dynamic Record Construction

Dynamic 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
}

Record Field Operations

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)

Record Structure Operations

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 :: HNil

Labelled Infrastructure

Infrastructure 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]

Usage Examples

Basic Record Operations

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

Dynamic Record Construction

// 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"

Record Merging and Transformation

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)

Integration with Case Classes

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)

Record Validation and Constraints

// 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}")
}

Record Renaming

// 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)  // 30

Working with Optional Fields

val 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"