0
# Type-safe Records
1
2
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.
3
4
## Core Types
5
6
### Field Definition
7
8
```scala { .api }
9
/**
10
* Field trait representing a typed field key
11
*/
12
trait Field[T] extends FieldAux {
13
type valueType = T
14
}
15
16
/**
17
* Base trait for field keys
18
*/
19
trait FieldAux {
20
type valueType
21
}
22
```
23
24
### Field Entry
25
26
```scala { .api }
27
/**
28
* A field entry pairs a field with its value
29
*/
30
type FieldEntry[F <: FieldAux] = (F, F#valueType)
31
```
32
33
### Record Type Alias
34
35
Records are HLists of FieldEntry pairs:
36
37
```scala { .api }
38
// A record is an HList of field entries
39
type Record = HList // where elements are FieldEntry[F] for various F
40
```
41
42
## Creating Records
43
44
### Field Declaration Syntax
45
46
```scala { .api }
47
/**
48
* Syntax for creating field keys
49
*/
50
implicit class FieldOps[K](k: K) {
51
def ->>[V](v: V): FieldEntry[Field[V]] = ???
52
}
53
```
54
55
**Usage Examples:**
56
57
```scala
58
import shapeless._
59
import record._
60
61
// Create field keys and records
62
val nameField = "name" ->> "John Doe"
63
val ageField = "age" ->> 30
64
val activeField = "active" ->> true
65
66
// Combine into record
67
val person = nameField :: ageField :: activeField :: HNil
68
// Type: FieldEntry[Field[String]] :: FieldEntry[Field[Int]] :: FieldEntry[Field[Boolean]] :: HNil
69
70
// Directly create records
71
val book = ("title" ->> "Shapeless Guide") :: ("pages" ->> 300) :: ("isbn" ->> "978-0123456789") :: HNil
72
val product = ("name" ->> "Widget") :: ("price" ->> 19.99) :: ("inStock" ->> true) :: HNil
73
```
74
75
## Record Operations
76
77
### RecordOps Enhancement
78
79
Records are enhanced with specialized operations through `RecordOps[L <: HList]`:
80
81
```scala { .api }
82
/**
83
* Enhanced operations for HList records
84
*/
85
class RecordOps[L <: HList](l: L) {
86
def get[F <: FieldAux](f: F)(implicit selector: Selector[L, FieldEntry[F]]): F#valueType
87
def updated[V, F <: Field[V]](f: F, v: V)(implicit updater: Updater[L, F, V]): updater.Out
88
def remove[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): (F#valueType, remove.Out)
89
def +[V, F <: Field[V]](fv: (F, V))(implicit updater: Updater[L, F, V]): updater.Out
90
def -[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): remove.Out
91
}
92
```
93
94
### Field Access
95
96
```scala { .api }
97
/**
98
* Type-safe field access by field key
99
*/
100
def get[F <: FieldAux](f: F)(implicit selector: Selector[L, FieldEntry[F]]): F#valueType
101
```
102
103
**Usage Examples:**
104
105
```scala
106
import shapeless._
107
import record._
108
109
val person = ("name" ->> "Alice") :: ("age" ->> 25) :: ("city" ->> "Boston") :: HNil
110
111
// Type-safe field access
112
val name: String = person("name") // "Alice"
113
val age: Int = person("age") // 25
114
val city: String = person("city") // "Boston"
115
116
// Access by field key
117
val nameValue = person.get("name") // "Alice"
118
119
// This would fail at compile time:
120
// val invalid = person("salary") // Error: field not found
121
```
122
123
### Record Updates
124
125
```scala { .api }
126
/**
127
* Update existing field or add new field
128
*/
129
def updated[V, F <: Field[V]](f: F, v: V)(implicit updater: Updater[L, F, V]): updater.Out
130
131
/**
132
* Add or update field (operator syntax)
133
*/
134
def +[V, F <: Field[V]](fv: (F, V))(implicit updater: Updater[L, F, V]): updater.Out
135
```
136
137
**Usage Examples:**
138
139
```scala
140
import shapeless._
141
import record._
142
143
val person = ("name" ->> "Bob") :: ("age" ->> 30) :: HNil
144
145
// Update existing field
146
val olderPerson = person.updated("age", 31)
147
// Result: ("name" ->> "Bob") :: ("age" ->> 31) :: HNil
148
149
// Add new field
150
val personWithCity = person + ("city" ->> "Seattle")
151
// Result: ("name" ->> "Bob") :: ("age" ->> 30) :: ("city" ->> "Seattle") :: HNil
152
153
// Update with operator syntax
154
val updatedPerson = person + ("age" ->> 35) + ("active" ->> true)
155
// Result: ("name" ->> "Bob") :: ("age" ->> 35) :: ("active" ->> true) :: HNil
156
```
157
158
### Field Removal
159
160
```scala { .api }
161
/**
162
* Remove field, returning both the value and remaining record
163
*/
164
def remove[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): (F#valueType, remove.Out)
165
166
/**
167
* Remove field, returning only the remaining record
168
*/
169
def -[F <: FieldAux](f: F)(implicit remove: Remove[FieldEntry[F], L]): remove.Out
170
```
171
172
**Usage Examples:**
173
174
```scala
175
import shapeless._
176
import record._
177
178
val person = ("name" ->> "Charlie") :: ("age" ->> 28) :: ("city" ->> "Portland") :: HNil
179
180
// Remove field and get both value and remaining record
181
val (removedAge, personWithoutAge) = person.remove("age")
182
// removedAge: Int = 28
183
// personWithoutAge: ("name" ->> "Charlie") :: ("city" ->> "Portland") :: HNil
184
185
// Remove field, keep only remaining record
186
val personNoCity = person - "city"
187
// Result: ("name" ->> "Charlie") :: ("age" ->> 28) :: HNil
188
```
189
190
## Type Classes
191
192
### Updater Type Class
193
194
```scala { .api }
195
/**
196
* Supports record update and extension operations
197
*/
198
trait Updater[L <: HList, F <: FieldAux, V] {
199
type Out <: HList
200
def apply(l: L, f: F, v: V): Out
201
}
202
```
203
204
The `Updater` type class handles both updating existing fields and adding new fields to records.
205
206
## Advanced Record Operations
207
208
### Record Merging
209
210
```scala
211
import shapeless._
212
import record._
213
214
val person = ("name" ->> "David") :: ("age" ->> 35) :: HNil
215
val address = ("street" ->> "123 Main St") :: ("city" ->> "Denver") :: HNil
216
217
// Merge records using HList concatenation
218
val fullRecord = person ++ address
219
// Result: ("name" ->> "David") :: ("age" ->> 35) :: ("street" ->> "123 Main St") :: ("city" ->> "Denver") :: HNil
220
221
// Access merged fields
222
val street: String = fullRecord("street") // "123 Main St"
223
val fullName: String = fullRecord("name") // "David"
224
```
225
226
### Record Transformation
227
228
```scala
229
import shapeless._
230
import record._
231
232
val employee = ("name" ->> "Eve") :: ("salary" ->> 50000) :: ("department" ->> "Engineering") :: HNil
233
234
// Transform record using polymorphic functions
235
object upperCase extends Poly1 {
236
implicit def caseString = at[String](_.toUpperCase)
237
implicit def caseInt = at[Int](identity)
238
}
239
240
// This would require more complex type machinery in practice
241
// val uppercased = employee.map(upperCase) // Not directly supported
242
243
// Instead, use individual field updates
244
val normalized = employee.updated("name", employee("name").toUpperCase)
245
.updated("department", employee("department").toUpperCase)
246
// Result: ("name" ->> "EVE") :: ("salary" ->> 50000) :: ("department" ->> "ENGINEERING") :: HNil
247
```
248
249
### Record Validation
250
251
```scala
252
import shapeless._
253
import record._
254
255
// Type-safe record validation
256
def validatePerson[L <: HList]
257
(person: L)
258
(implicit
259
hasName: Selector[L, FieldEntry[Field[String]]],
260
hasAge: Selector[L, FieldEntry[Field[Int]]]): Boolean = {
261
262
val name = person.get("name")
263
val age = person.get("age")
264
265
name.nonEmpty && age >= 0 && age <= 120
266
}
267
268
val validPerson = ("name" ->> "Frank") :: ("age" ->> 42) :: HNil
269
val isValid = validatePerson(validPerson) // true
270
271
// This would fail at compile time - missing required fields:
272
// val invalidPerson = ("nickname" ->> "Frankie") :: HNil
273
// validatePerson(invalidPerson) // Error: can't find required fields
274
```
275
276
### Nested Records
277
278
```scala
279
import shapeless._
280
import record._
281
282
// Create nested record structures
283
val address = ("street" ->> "456 Oak Ave") :: ("city" ->> "Austin") :: ("zip" ->> "78701") :: HNil
284
val person = ("name" ->> "Grace") :: ("age" ->> 29) :: ("address" ->> address) :: HNil
285
286
// Access nested fields
287
val nestedAddress = person("address")
288
val street: String = nestedAddress("street") // "456 Oak Ave"
289
290
// Update nested records
291
val newAddress = address.updated("street", "789 Pine St")
292
val personWithNewAddress = person.updated("address", newAddress)
293
```
294
295
### Record to Case Class Conversion
296
297
While not directly supported by the record API, records can be converted to case classes using generic programming:
298
299
```scala
300
import shapeless._
301
import record._
302
303
case class Person(name: String, age: Int, city: String)
304
305
val record = ("name" ->> "Henry") :: ("age" ->> 33) :: ("city" ->> "Miami") :: HNil
306
307
// Conversion would require additional machinery (Generic, LabelledGeneric)
308
// This is typically done through shapeless's automatic derivation mechanisms
309
```
310
311
## Field Key Strategies
312
313
### String-based Keys
314
315
```scala
316
import shapeless._
317
import record._
318
319
// Most common approach - string literals as keys
320
val config = ("host" ->> "localhost") :: ("port" ->> 8080) :: ("ssl" ->> false) :: HNil
321
322
val host: String = config("host")
323
val port: Int = config("port")
324
```
325
326
### Symbol-based Keys
327
328
```scala
329
import shapeless._
330
import record._
331
332
// Using symbols as field keys
333
val data = ('timestamp ->> System.currentTimeMillis) ::
334
('level ->> "INFO") ::
335
('message ->> "System started") :: HNil
336
337
val timestamp: Long = data('timestamp)
338
val level: String = data('level)
339
```
340
341
### Typed Keys
342
343
```scala
344
import shapeless._
345
import record._
346
347
// Define custom typed field keys
348
object Keys {
349
case object Username extends Field[String]
350
case object UserId extends Field[Int]
351
case object IsActive extends Field[Boolean]
352
}
353
354
import Keys._
355
356
val user = (Username ->> "admin") :: (UserId ->> 1001) :: (IsActive ->> true) :: HNil
357
358
val username: String = user(Username)
359
val userId: Int = user(UserId)
360
val active: Boolean = user(IsActive)
361
```
362
363
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.