0
# Records and Named Field Access
1
2
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.
3
4
## Capabilities
5
6
### Core Record Types
7
8
Fundamental types for creating and working with named field access.
9
10
```scala { .api }
11
// Field with singleton-typed key K and value type V
12
type FieldType[K, +V] = V @@ KeyTag[K, V]
13
14
// Phantom tag for associating keys with values
15
trait KeyTag[K, +V]
16
17
// Type alias for records (HList of FieldType)
18
type Record = HList // where elements are FieldType[_, _]
19
```
20
21
### Field Construction
22
23
Building blocks for creating fields with typed keys.
24
25
```scala { .api }
26
// Field builder for key K
27
object field[K] extends FieldOf[K]
28
29
trait FieldOf[K] {
30
def apply[V](value: V): FieldType[K, V]
31
}
32
33
// Field builder that applies values to create fields
34
trait FieldBuilder[K] {
35
def apply[V](v: V): FieldType[K, V]
36
}
37
38
// Usage:
39
val nameField = field['name]("Alice")
40
val ageField = field['age](30)
41
val record = nameField :: ageField :: HNil
42
```
43
44
### Dynamic Record Construction
45
46
Dynamic construction of records using method names as field keys.
47
48
```scala { .api }
49
object Record {
50
// Dynamic record construction via method calls
51
def applyDynamic(method: String)(args: Any*): HList
52
53
// Named argument construction
54
def applyDynamicNamed(method: String)(args: (String, Any)*): HList
55
56
// Dynamic field type selection
57
def selectDynamic(field: String): Any
58
}
59
60
// Dynamic argument mapping for records
61
trait RecordArgs[R <: HList] {
62
def apply(rec: R): Any
63
}
64
65
// Inverse mapping from records to named arguments
66
trait FromRecordArgs[A] {
67
type Out <: HList
68
def apply(args: A): Out
69
}
70
```
71
72
### Record Field Operations
73
74
Core operations for accessing and manipulating record fields.
75
76
```scala { .api }
77
// Select field with key K from record L
78
trait Selector[L <: HList, K] {
79
type Out
80
def apply(l: L): Out
81
}
82
83
// Update field F in record L
84
trait Updater[L <: HList, F] {
85
type Out <: HList
86
def apply(l: L, f: F): Out
87
}
88
89
// Remove field with key K from record L
90
trait Remover[L <: HList, K] {
91
type Out <: HList
92
def apply(l: L): (Out, FieldType[K, _])
93
}
94
95
// Usage:
96
val record = field['name]("Bob") :: field['age](25) :: HNil
97
val name = record.get('name) // "Bob"
98
val updated = record.updated('age, 26)
99
val (remaining, removed) = record.remove('name)
100
```
101
102
### Record Structure Operations
103
104
Operations for extracting and manipulating record structure.
105
106
```scala { .api }
107
// Extract all keys from record L as HList
108
trait Keys[L <: HList] {
109
type Out <: HList
110
def apply(l: L): Out
111
}
112
113
// Extract all values from record L as HList
114
trait Values[L <: HList] {
115
type Out <: HList
116
def apply(l: L): Out
117
}
118
119
// Merge two records L and M
120
trait Merger[L <: HList, M <: HList] {
121
type Out <: HList
122
def apply(l: L, m: M): Out
123
}
124
125
// Rename fields according to key mapping
126
trait Renamer[L <: HList, KS <: HList] {
127
type Out <: HList
128
def apply(l: L): Out
129
}
130
131
// Example:
132
val keys = record.keys // 'name :: 'age :: HNil
133
val values = record.values // "Bob" :: 25 :: HNil
134
```
135
136
### Labelled Infrastructure
137
138
Infrastructure for working with labeled types and symbolic labels.
139
140
```scala { .api }
141
// Extract symbolic labels from case class/sealed trait
142
trait DefaultSymbolicLabelling[T] {
143
type Out
144
def apply(): Out
145
}
146
147
// Base trait for polymorphic functions preserving field keys
148
trait FieldPoly extends Poly
149
150
// Type class witnessing field value types
151
trait FieldOf[V] {
152
type Out <: HList
153
def apply(): Out
154
}
155
156
// Create field type from key and value
157
def fieldType[K, V](key: K, value: V): FieldType[K, V]
158
```
159
160
## Usage Examples
161
162
### Basic Record Operations
163
164
```scala
165
import shapeless._, record._, syntax.singleton._
166
167
// Create record using field syntax
168
val person = ('name ->> "Alice") :: ('age ->> 30) :: ('active ->> true) :: HNil
169
170
// Access fields by name
171
val name: String = person('name)
172
val age: Int = person('age)
173
val active: Boolean = person('active)
174
175
// Update fields
176
val older = person.updated('age, 31)
177
val renamed = person.updated('name, "Alicia")
178
179
// Remove fields
180
val (withoutAge, ageField) = person.remove('age)
181
// withoutAge: ('name ->> String) :: ('active ->> Boolean) :: HNil
182
```
183
184
### Dynamic Record Construction
185
186
```scala
187
// Using dynamic syntax
188
val dynamicRecord = Record.empty
189
.add('firstName, "John")
190
.add('lastName, "Doe")
191
.add('email, "john.doe@example.com")
192
193
// Access dynamically
194
val firstName = dynamicRecord.get('firstName) // "John"
195
val email = dynamicRecord.get('email) // "john.doe@example.com"
196
```
197
198
### Record Merging and Transformation
199
200
```scala
201
val address = ('street ->> "123 Main St") :: ('city ->> "Anytown") :: HNil
202
val contact = ('phone ->> "555-1234") :: ('email ->> "test@example.com") :: HNil
203
204
// Merge records
205
val combined = person ++ address ++ contact
206
207
// Extract keys and values
208
val allKeys = combined.keys
209
val allValues = combined.values
210
211
// Transform values while preserving keys
212
object addPrefix extends FieldPoly {
213
implicit def stringCase[K] = atField[K, String](s => s"prefix_$s")
214
implicit def intCase[K] = atField[K, Int](i => i * 100)
215
}
216
217
val transformed = combined.map(addPrefix)
218
```
219
220
### Integration with Case Classes
221
222
```scala
223
case class Employee(name: String, id: Int, department: String, salary: Double)
224
225
val emp = Employee("Sarah", 12345, "Engineering", 95000.0)
226
227
// Convert to labeled generic (preserves field names)
228
val lgen = LabelledGeneric[Employee]
229
val empRecord = lgen.to(emp)
230
// Result has field names as singleton types
231
232
// Access by field name
233
val empName = empRecord.get('name) // "Sarah"
234
val empId = empRecord.get('id) // 12345
235
val empDept = empRecord.get('department) // "Engineering"
236
237
// Update and convert back
238
val promoted = empRecord.updated('salary, 105000.0)
239
val updatedEmp = lgen.from(promoted)
240
// Employee("Sarah", 12345, "Engineering", 105000.0)
241
```
242
243
### Record Validation and Constraints
244
245
```scala
246
// Define validation rules
247
object validateRecord extends FieldPoly {
248
implicit def nameValidation = atField['name, String] { name =>
249
require(name.nonEmpty, "Name cannot be empty")
250
name
251
}
252
253
implicit def ageValidation = atField['age, Int] { age =>
254
require(age >= 0, "Age must be non-negative")
255
age
256
}
257
}
258
259
// Apply validations
260
try {
261
val validatedPerson = person.map(validateRecord)
262
println("Validation passed")
263
} catch {
264
case e: IllegalArgumentException => println(s"Validation failed: ${e.getMessage}")
265
}
266
```
267
268
### Record Renaming
269
270
```scala
271
// Define key mapping for renaming
272
val keyMapping = ('name ->> 'fullName) :: ('age ->> 'yearsOld) :: HNil
273
274
// Rename fields
275
val renamedPerson = person.renameFields(keyMapping)
276
// Result: ('fullName ->> "Alice") :: ('yearsOld ->> 30) :: ('active ->> true) :: HNil
277
278
val fullName = renamedPerson('fullName) // "Alice"
279
val yearsOld = renamedPerson('yearsOld) // 30
280
```
281
282
### Working with Optional Fields
283
284
```scala
285
val optionalRecord =
286
('name ->> Some("Bob")): FieldType['name, Option[String]] ::
287
('age ->> None): FieldType['age, Option[Int]] ::
288
('email ->> Some("bob@example.com")): FieldType['email, Option[String]] ::
289
HNil
290
291
// Extract present values
292
object extractPresent extends FieldPoly {
293
implicit def optionCase[K, V] = atField[K, Option[V]] {
294
case Some(v) => v
295
case None => throw new NoSuchElementException(s"Field not present")
296
}
297
}
298
299
// Get only present values (will throw for None)
300
val name = optionalRecord.get('name).get // "Bob"
301
val email = optionalRecord.get('email).get // "bob@example.com"
302
```