An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns
—
Shapeless records provide type-safe, compile-time verified record operations built on top of HLists. Records allow you to work with labeled data structures where field access, updates, and manipulations are all verified at compile time.
/**
* Field trait representing a typed field key
*/
trait Field[T] extends FieldAux {
type valueType = T
}
/**
* Base trait for field keys
*/
trait FieldAux {
type valueType
}/**
* A field entry pairs a field with its value
*/
type FieldEntry[F <: FieldAux] = (F, F#valueType)Records are HLists of FieldEntry pairs:
// A record is an HList of field entries
type Record = HList // where elements are FieldEntry[F] for various F/**
* Syntax for creating field keys
*/
implicit class FieldOps[K](k: K) {
def ->>[V](v: V): FieldEntry[Field[V]] = ???
}Usage Examples:
import shapeless._
import record._
// Create field keys and records
val nameField = "name" ->> "John Doe"
val ageField = "age" ->> 30
val activeField = "active" ->> true
// Combine into record
val person = nameField :: ageField :: activeField :: HNil
// Type: FieldEntry[Field[String]] :: FieldEntry[Field[Int]] :: FieldEntry[Field[Boolean]] :: HNil
// Directly create records
val book = ("title" ->> "Shapeless Guide") :: ("pages" ->> 300) :: ("isbn" ->> "978-0123456789") :: HNil
val product = ("name" ->> "Widget") :: ("price" ->> 19.99) :: ("inStock" ->> true) :: HNilRecords are enhanced with specialized operations through RecordOps[L <: HList]:
/**
* Enhanced operations for HList records
*/
class RecordOps[L <: HList](l: L) {
def get[F <: FieldAux](f: F)(implicit selector: Selector[L, FieldEntry[F]]): F#valueType
def updated[V, F <: Field[V]](f: F, v: V)(implicit updater: Updater[L, F, V]): updater.Out
def remove[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): (F#valueType, remove.Out)
def +[V, F <: Field[V]](fv: (F, V))(implicit updater: Updater[L, F, V]): updater.Out
def -[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): remove.Out
}/**
* Type-safe field access by field key
*/
def get[F <: FieldAux](f: F)(implicit selector: Selector[L, FieldEntry[F]]): F#valueTypeUsage Examples:
import shapeless._
import record._
val person = ("name" ->> "Alice") :: ("age" ->> 25) :: ("city" ->> "Boston") :: HNil
// Type-safe field access
val name: String = person("name") // "Alice"
val age: Int = person("age") // 25
val city: String = person("city") // "Boston"
// Access by field key
val nameValue = person.get("name") // "Alice"
// This would fail at compile time:
// val invalid = person("salary") // Error: field not found/**
* Update existing field or add new field
*/
def updated[V, F <: Field[V]](f: F, v: V)(implicit updater: Updater[L, F, V]): updater.Out
/**
* Add or update field (operator syntax)
*/
def +[V, F <: Field[V]](fv: (F, V))(implicit updater: Updater[L, F, V]): updater.OutUsage Examples:
import shapeless._
import record._
val person = ("name" ->> "Bob") :: ("age" ->> 30) :: HNil
// Update existing field
val olderPerson = person.updated("age", 31)
// Result: ("name" ->> "Bob") :: ("age" ->> 31) :: HNil
// Add new field
val personWithCity = person + ("city" ->> "Seattle")
// Result: ("name" ->> "Bob") :: ("age" ->> 30) :: ("city" ->> "Seattle") :: HNil
// Update with operator syntax
val updatedPerson = person + ("age" ->> 35) + ("active" ->> true)
// Result: ("name" ->> "Bob") :: ("age" ->> 35) :: ("active" ->> true) :: HNil/**
* Remove field, returning both the value and remaining record
*/
def remove[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): (F#valueType, remove.Out)
/**
* Remove field, returning only the remaining record
*/
def -[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): remove.OutUsage Examples:
import shapeless._
import record._
val person = ("name" ->> "Charlie") :: ("age" ->> 28) :: ("city" ->> "Portland") :: HNil
// Remove field and get both value and remaining record
val (removedAge, personWithoutAge) = person.remove("age")
// removedAge: Int = 28
// personWithoutAge: ("name" ->> "Charlie") :: ("city" ->> "Portland") :: HNil
// Remove field, keep only remaining record
val personNoCity = person - "city"
// Result: ("name" ->> "Charlie") :: ("age" ->> 28) :: HNil/**
* Supports record update and extension operations
*/
trait Updater[L <: HList, F <: FieldAux, V] {
type Out <: HList
def apply(l: L, f: F, v: V): Out
}The Updater type class handles both updating existing fields and adding new fields to records.
import shapeless._
import record._
val person = ("name" ->> "David") :: ("age" ->> 35) :: HNil
val address = ("street" ->> "123 Main St") :: ("city" ->> "Denver") :: HNil
// Merge records using HList concatenation
val fullRecord = person ++ address
// Result: ("name" ->> "David") :: ("age" ->> 35) :: ("street" ->> "123 Main St") :: ("city" ->> "Denver") :: HNil
// Access merged fields
val street: String = fullRecord("street") // "123 Main St"
val fullName: String = fullRecord("name") // "David"import shapeless._
import record._
val employee = ("name" ->> "Eve") :: ("salary" ->> 50000) :: ("department" ->> "Engineering") :: HNil
// Transform record using polymorphic functions
object upperCase extends Poly1 {
implicit def caseString = at[String](_.toUpperCase)
implicit def caseInt = at[Int](identity)
}
// This would require more complex type machinery in practice
// val uppercased = employee.map(upperCase) // Not directly supported
// Instead, use individual field updates
val normalized = employee.updated("name", employee("name").toUpperCase)
.updated("department", employee("department").toUpperCase)
// Result: ("name" ->> "EVE") :: ("salary" ->> 50000) :: ("department" ->> "ENGINEERING") :: HNilimport shapeless._
import record._
// Type-safe record validation
def validatePerson[L <: HList]
(person: L)
(implicit
hasName: Selector[L, FieldEntry[Field[String]]],
hasAge: Selector[L, FieldEntry[Field[Int]]]): Boolean = {
val name = person.get("name")
val age = person.get("age")
name.nonEmpty && age >= 0 && age <= 120
}
val validPerson = ("name" ->> "Frank") :: ("age" ->> 42) :: HNil
val isValid = validatePerson(validPerson) // true
// This would fail at compile time - missing required fields:
// val invalidPerson = ("nickname" ->> "Frankie") :: HNil
// validatePerson(invalidPerson) // Error: can't find required fieldsimport shapeless._
import record._
// Create nested record structures
val address = ("street" ->> "456 Oak Ave") :: ("city" ->> "Austin") :: ("zip" ->> "78701") :: HNil
val person = ("name" ->> "Grace") :: ("age" ->> 29) :: ("address" ->> address) :: HNil
// Access nested fields
val nestedAddress = person("address")
val street: String = nestedAddress("street") // "456 Oak Ave"
// Update nested records
val newAddress = address.updated("street", "789 Pine St")
val personWithNewAddress = person.updated("address", newAddress)While not directly supported by the record API, records can be converted to case classes using generic programming:
import shapeless._
import record._
case class Person(name: String, age: Int, city: String)
val record = ("name" ->> "Henry") :: ("age" ->> 33) :: ("city" ->> "Miami") :: HNil
// Conversion would require additional machinery (Generic, LabelledGeneric)
// This is typically done through shapeless's automatic derivation mechanismsimport shapeless._
import record._
// Most common approach - string literals as keys
val config = ("host" ->> "localhost") :: ("port" ->> 8080) :: ("ssl" ->> false) :: HNil
val host: String = config("host")
val port: Int = config("port")import shapeless._
import record._
// Using symbols as field keys
val data = ('timestamp ->> System.currentTimeMillis) ::
('level ->> "INFO") ::
('message ->> "System started") :: HNil
val timestamp: Long = data('timestamp)
val level: String = data('level)import shapeless._
import record._
// Define custom typed field keys
object Keys {
case object Username extends Field[String]
case object UserId extends Field[Int]
case object IsActive extends Field[Boolean]
}
import Keys._
val user = (Username ->> "admin") :: (UserId ->> 1001) :: (IsActive ->> true) :: HNil
val username: String = user(Username)
val userId: Int = user(UserId)
val active: Boolean = user(IsActive)Records in shapeless provide a powerful abstraction for working with labeled data structures while maintaining compile-time safety and type inference. They bridge the gap between the flexibility of dynamic field access and the safety of static typing.
Install with Tessl CLI
npx tessl i tessl/maven-com-chuusai--shapeless-2-11