CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-chuusai--shapeless-2-11

An exploration of generic (aka polytypic) programming in Scala derived from implementing scrap your boilerplate and higher rank polymorphism patterns

Pending
Overview
Eval results
Files

records.mddocs/

Type-safe Records

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.

Core Types

Field Definition

/**
 * Field trait representing a typed field key
 */
trait Field[T] extends FieldAux {
  type valueType = T
}

/**
 * Base trait for field keys
 */
trait FieldAux {
  type valueType
}

Field Entry

/**
 * A field entry pairs a field with its value
 */
type FieldEntry[F <: FieldAux] = (F, F#valueType)

Record Type Alias

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

Creating Records

Field Declaration Syntax

/**
 * 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) :: HNil

Record Operations

RecordOps Enhancement

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

Field Access

/**
 * Type-safe field access by field key
 */
def get[F <: FieldAux](f: F)(implicit selector: Selector[L, FieldEntry[F]]): F#valueType

Usage 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

Record Updates

/**
 * 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.Out

Usage 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

Field Removal

/**
 * 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.Out

Usage 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

Type Classes

Updater Type Class

/**
 * 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.

Advanced Record Operations

Record Merging

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"

Record Transformation

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

Record Validation

import 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 fields

Nested Records

import 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)

Record to Case Class Conversion

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 mechanisms

Field Key Strategies

String-based Keys

import 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")

Symbol-based Keys

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)

Typed Keys

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

docs

conversions.md

generic.md

hlist.md

hmap.md

index.md

lift.md

nat.md

poly.md

records.md

sized.md

sybclass.md

typeable.md

typeoperators.md

tile.json