0
# Scrap Your Boilerplate
1
2
Shapeless provides an implementation of "Scrap Your Boilerplate with Class" that enables generic queries and transformations over arbitrarily nested data structures. This allows you to write functions that work uniformly across different data types without explicit recursion.
3
4
## Core Type Classes
5
6
### Data - Generic Queries
7
8
```scala { .api }
9
/**
10
* Type class for generic queries over data structures
11
* F - query function type, T - data type, R - result type
12
*/
13
trait Data[F, T, R] {
14
def gmapQ(f: F, t: T): R
15
}
16
```
17
18
### DataT - Generic Transformations
19
20
```scala { .api }
21
/**
22
* Type class for generic transformations over data structures
23
* F - transformation function type, T - data type
24
*/
25
trait DataT[F, T] {
26
def gmapT(f: F, t: T): T
27
}
28
```
29
30
## Generic Combinators
31
32
### Everything - Generic Queries
33
34
The `everything` combinator applies a query function everywhere in a data structure and combines results:
35
36
```scala { .api }
37
/**
38
* Apply query function everywhere and combine results
39
*/
40
def everything[F <: Poly](f: F)(implicit ...): ...
41
```
42
43
**Usage Examples:**
44
45
```scala
46
import shapeless._
47
48
case class Person(name: String, age: Int, address: Address)
49
case class Address(street: String, city: String, zip: String)
50
case class Company(name: String, employees: List[Person])
51
52
val person = Person("Alice", 30, Address("123 Main St", "Boston", "02101"))
53
val company = Company("Acme Corp", List(
54
Person("Bob", 25, Address("456 Oak Ave", "Cambridge", "02139")),
55
Person("Carol", 35, Address("789 Pine St", "Somerville", "02144"))
56
))
57
58
// Query to count all strings in a data structure
59
object countStrings extends Poly1 {
60
implicit def caseString = at[String](_ => 1)
61
implicit def default[T] = at[T](_ => 0)
62
}
63
64
// Count strings in person record
65
val personStringCount = person.everything(countStrings)
66
// Result: counts "Alice", "123 Main St", "Boston", "02101" = 4
67
68
// Count strings in company structure
69
val companyStringCount = company.everything(countStrings)
70
// Result: counts all string fields across the entire structure
71
```
72
73
### Everywhere - Generic Transformations
74
75
The `everywhere` combinator applies a transformation function everywhere in a data structure:
76
77
```scala { .api }
78
/**
79
* Apply transformation function everywhere in data structure
80
*/
81
def everywhere[F <: Poly](f: F)(implicit ...): ...
82
```
83
84
**Usage Examples:**
85
86
```scala
87
import shapeless._
88
89
// Transformation to uppercase all strings
90
object uppercaseStrings extends Poly1 {
91
implicit def caseString = at[String](_.toUpperCase)
92
implicit def default[T] = at[T](identity)
93
}
94
95
val person = Person("Alice", 30, Address("123 Main St", "Boston", "02101"))
96
97
val uppercasedPerson = person.everywhere(uppercaseStrings)
98
// Result: Person("ALICE", 30, Address("123 MAIN ST", "BOSTON", "02101"))
99
100
// Transformation to increment all integers
101
object incrementInts extends Poly1 {
102
implicit def caseInt = at[Int](_ + 1)
103
implicit def default[T] = at[T](identity)
104
}
105
106
val incrementedPerson = person.everywhere(incrementInts)
107
// Result: Person("Alice", 31, Address("123 Main St", "Boston", "02101"))
108
```
109
110
## Advanced Query Patterns
111
112
### Selective Queries
113
114
```scala
115
import shapeless._
116
117
case class Product(id: Int, name: String, price: Double, categories: List[String])
118
case class Order(id: Int, products: List[Product], total: Double)
119
120
val order = Order(1001, List(
121
Product(1, "Widget", 19.99, List("tools", "hardware")),
122
Product(2, "Gadget", 29.99, List("electronics", "gadgets"))
123
), 49.98)
124
125
// Query to find all prices
126
object findPrices extends Poly1 {
127
implicit def caseDouble = at[Double](d => List(d))
128
implicit def default[T] = at[T](_ => List.empty[Double])
129
}
130
131
val allPrices = order.everything(findPrices)
132
// Result: List(19.99, 29.99, 49.98) - all Double values
133
134
// Query to collect all strings longer than 5 characters
135
object findLongStrings extends Poly1 {
136
implicit def caseString = at[String](s => if (s.length > 5) List(s) else List.empty)
137
implicit def default[T] = at[T](_ => List.empty[String])
138
}
139
140
val longStrings = order.everything(findLongStrings)
141
// Result: List("Widget", "Gadget", "electronics", "gadgets", "hardware")
142
```
143
144
### Statistical Queries
145
146
```scala
147
import shapeless._
148
149
// Query to compute statistics
150
object computeStats extends Poly1 {
151
case class Stats(count: Int, sum: Double, min: Double, max: Double)
152
153
implicit def caseDouble = at[Double](d => Stats(1, d, d, d))
154
implicit def caseInt = at[Int](i => Stats(1, i.toDouble, i.toDouble, i.toDouble))
155
implicit def default[T] = at[T](_ => Stats(0, 0.0, Double.MaxValue, Double.MinValue))
156
157
// Monoid for combining stats
158
implicit val statsMonoid = new Monoid[Stats] {
159
def zero = Stats(0, 0.0, Double.MaxValue, Double.MinValue)
160
def append(a: Stats, b: Stats) = Stats(
161
a.count + b.count,
162
a.sum + b.sum,
163
math.min(a.min, b.min),
164
math.max(a.max, b.max)
165
)
166
}
167
}
168
169
val stats = order.everything(computeStats)
170
// Result: Stats with count, sum, min, max of all numeric values
171
```
172
173
## Advanced Transformation Patterns
174
175
### Conditional Transformations
176
177
```scala
178
import shapeless._
179
180
case class User(id: Int, name: String, email: String, active: Boolean)
181
case class Department(name: String, users: List[User])
182
183
val dept = Department("Engineering", List(
184
User(1, "alice", "alice@example.com", true),
185
User(2, "bob", "bob@example.com", false),
186
User(3, "carol", "carol@example.com", true)
187
))
188
189
// Transform to normalize names and emails
190
object normalizeData extends Poly1 {
191
implicit def caseString = at[String] { s =>
192
if (s.contains("@")) s.toLowerCase // Email addresses
193
else s.split(" ").map(_.capitalize).mkString(" ") // Names
194
}
195
implicit def default[T] = at[T](identity)
196
}
197
198
val normalizedDept = dept.everywhere(normalizeData)
199
// Result: names capitalized, emails lowercased
200
201
// Transform to anonymize sensitive data in inactive users
202
object anonymizeInactive extends Poly1 {
203
implicit def caseUser = at[User] { user =>
204
if (!user.active) user.copy(name = "REDACTED", email = "REDACTED")
205
else user
206
}
207
implicit def default[T] = at[T](identity)
208
}
209
210
val anonymizedDept = dept.everywhere(anonymizeInactive)
211
// Result: inactive users have name and email redacted
212
```
213
214
### Type-specific Transformations
215
216
```scala
217
import shapeless._
218
219
case class Document(title: String, content: String, tags: List[String], wordCount: Int)
220
case class Section(heading: String, documents: List[Document])
221
case class Library(name: String, sections: List[Section])
222
223
val library = Library("Technical Library", List(
224
Section("Programming", List(
225
Document("Scala Guide", "Scala is...", List("scala", "programming"), 1500),
226
Document("Haskell Intro", "Haskell is...", List("haskell", "functional"), 2000)
227
)),
228
Section("Mathematics", List(
229
Document("Linear Algebra", "Vectors and...", List("math", "algebra"), 3000)
230
))
231
))
232
233
// Transform to update word counts and normalize tags
234
object updateLibrary extends Poly1 {
235
implicit def caseDocument = at[Document] { doc =>
236
val actualWordCount = doc.content.split("\\s+").length
237
val normalizedTags = doc.tags.map(_.toLowerCase.trim)
238
doc.copy(wordCount = actualWordCount, tags = normalizedTags)
239
}
240
241
implicit def caseString = at[String](_.trim) // Trim whitespace from strings
242
implicit def default[T] = at[T](identity)
243
}
244
245
val updatedLibrary = library.everywhere(updateLibrary)
246
// Result: word counts updated, tags normalized, strings trimmed
247
```
248
249
## Custom Data Type Support
250
251
To use SYB with custom data types, you need to provide appropriate type class instances:
252
253
```scala
254
import shapeless._
255
256
case class Tree[T](value: T, children: List[Tree[T]])
257
258
// Provide Data instance for Tree
259
implicit def treeData[F <: Poly, T, R](implicit
260
fT: F.Case1.Aux[T, R],
261
dataList: Data[F, List[Tree[T]], R],
262
monoid: Monoid[R]
263
): Data[F, Tree[T], R] = new Data[F, Tree[T], R] {
264
def gmapQ(f: F, tree: Tree[T]): R = {
265
val valueResult = f(tree.value)
266
val childrenResult = dataList.gmapQ(f, tree.children)
267
monoid.append(valueResult, childrenResult)
268
}
269
}
270
271
// Provide DataT instance for Tree
272
implicit def treeDataT[F <: Poly, T](implicit
273
fT: F.Case1.Aux[T, T],
274
dataListT: DataT[F, List[Tree[T]]]
275
): DataT[F, Tree[T]] = new DataT[F, Tree[T]] {
276
def gmapT(f: F, tree: Tree[T]): Tree[T] = {
277
val newValue = f(tree.value)
278
val newChildren = dataListT.gmapT(f, tree.children)
279
Tree(newValue, newChildren)
280
}
281
}
282
283
val tree = Tree("root", List(
284
Tree("child1", List(Tree("leaf1", Nil), Tree("leaf2", Nil))),
285
Tree("child2", List(Tree("leaf3", Nil)))
286
))
287
288
// Now can use everything/everywhere with Tree
289
val stringCount = tree.everything(countStrings)
290
val uppercased = tree.everywhere(uppercaseStrings)
291
```
292
293
## Integration with Other Shapeless Features
294
295
SYB works well with other shapeless features:
296
297
```scala
298
import shapeless._
299
300
// Use with HLists
301
val data = 42 :: "hello" :: true :: 3.14 :: HNil
302
val stringCount = data.everything(countStrings) // 1 (for "hello")
303
val uppercased = data.everywhere(uppercaseStrings) // 42 :: "HELLO" :: true :: 3.14 :: HNil
304
305
// Use with Records
306
val record = ("name" ->> "Alice") :: ("age" ->> 30) :: ("active" ->> true) :: HNil
307
val recordStringCount = record.everything(countStrings)
308
val recordUppercased = record.everywhere(uppercaseStrings)
309
```
310
311
Scrap Your Boilerplate provides powerful generic programming capabilities that eliminate the need for writing repetitive traversal code, enabling concise and type-safe operations over complex nested data structures.