0
# Method Chaining
1
2
Utility methods for functional programming patterns, debugging, and creating fluent APIs through method chaining operations.
3
4
## ChainingOps Class
5
6
```scala { .api }
7
final class ChainingOps[A](private val self: A) extends AnyVal {
8
def tap[U](f: A => U): A
9
def pipe[B](f: A => B): B
10
}
11
```
12
13
Extension methods that enable fluent method chaining for any type.
14
15
### tap Method
16
17
```scala { .api }
18
def tap[U](f: A => U): A
19
```
20
21
Applies a function to the value for its side effects and returns the original value unchanged. This is useful for debugging, logging, or performing side effects in the middle of a method chain.
22
23
**Usage:**
24
```scala
25
import scala.util.chaining._
26
27
val result = List(1, 2, 3, 4, 5)
28
.tap(xs => println(s"Original list: $xs"))
29
.filter(_ % 2 == 0)
30
.tap(evens => println(s"Even numbers: $evens"))
31
.map(_ * 2)
32
.tap(doubled => println(s"Doubled: $doubled"))
33
34
// Output:
35
// Original list: List(1, 2, 3, 4, 5)
36
// Even numbers: List(2, 4)
37
// Doubled: List(4, 8)
38
// result: List(4, 8)
39
```
40
41
**Common use cases:**
42
- Debugging intermediate values in method chains
43
- Logging operations
44
- Performing validation or assertions
45
- Side effects like caching or metrics collection
46
47
### pipe Method
48
49
```scala { .api }
50
def pipe[B](f: A => B): B
51
```
52
53
Transforms the value by applying a function to it. This enables forward function application and can make code more readable by avoiding nested function calls.
54
55
**Usage:**
56
```scala
57
import scala.util.chaining._
58
59
// Instead of nested function calls
60
val result1 = math.abs(math.pow(2.5, 3) - 10)
61
62
// Use pipe for forward flow
63
val result2 = 2.5
64
.pipe(math.pow(_, 3))
65
.pipe(_ - 10)
66
.pipe(math.abs)
67
68
// Both give the same result: 5.375
69
```
70
71
**Advanced usage:**
72
```scala
73
case class Person(name: String, age: Int)
74
case class Adult(person: Person)
75
case class Senior(adult: Adult)
76
77
def toAdult(p: Person): Option[Adult] =
78
if (p.age >= 18) Some(Adult(p)) else None
79
80
def toSenior(a: Adult): Option[Senior] =
81
if (a.person.age >= 65) Some(Senior(a)) else None
82
83
val person = Person("Alice", 70)
84
val result = person
85
.pipe(toAdult)
86
.flatMap(_.pipe(toSenior))
87
.tap(_.foreach(s => println(s"Senior: ${s.adult.person.name}")))
88
89
// Output: Senior: Alice
90
// result: Some(Senior(Adult(Person(Alice,70))))
91
```
92
93
## ChainingSyntax Trait
94
95
```scala { .api }
96
trait ChainingSyntax {
97
@`inline` implicit final def scalaUtilChainingOps[A](a: A): ChainingOps[A] =
98
new ChainingOps(a)
99
}
100
```
101
102
Provides the implicit conversion that adds chaining methods to all types.
103
104
## chaining Object
105
106
```scala { .api }
107
object chaining extends ChainingSyntax
108
```
109
110
The main entry point for chaining operations. Import this to get access to `tap` and `pipe` methods on all types.
111
112
## Usage Examples
113
114
### Debugging and Logging
115
116
```scala
117
import scala.util.chaining._
118
119
def processUserData(rawData: String): Option[Int] = {
120
rawData
121
.tap(data => println(s"Processing: $data"))
122
.trim
123
.tap(trimmed => println(s"After trim: '$trimmed'"))
124
.pipe(_.toIntOption)
125
.tap {
126
case Some(num) => println(s"Parsed number: $num")
127
case None => println("Failed to parse number")
128
}
129
.filter(_ > 0)
130
.tap(filtered => println(s"Final result: $filtered"))
131
}
132
133
processUserData(" 42 ")
134
// Output:
135
// Processing: 42
136
// After trim: '42'
137
// Parsed number: 42
138
// Final result: Some(42)
139
```
140
141
### Configuration and Validation
142
143
```scala
144
import scala.util.chaining._
145
146
case class DatabaseConfig(
147
host: String,
148
port: Int,
149
database: String,
150
maxConnections: Int
151
)
152
153
def createDatabaseConfig(properties: Map[String, String]): Either[String, DatabaseConfig] = {
154
properties
155
.tap(props => println(s"Loading config from ${props.size} properties"))
156
.pipe { props =>
157
for {
158
host <- props.get("db.host").toRight("Missing db.host")
159
port <- props.get("db.port").flatMap(_.toIntOption).toRight("Invalid db.port")
160
database <- props.get("db.name").toRight("Missing db.name")
161
maxConn <- props.get("db.maxConnections").flatMap(_.toIntOption).toRight("Invalid db.maxConnections")
162
} yield DatabaseConfig(host, port, database, maxConn)
163
}
164
.tap {
165
case Right(config) => println(s"Successfully created config: $config")
166
case Left(error) => println(s"Configuration error: $error")
167
}
168
}
169
```
170
171
### Data Processing Pipeline
172
173
```scala
174
import scala.util.chaining._
175
176
case class SalesRecord(product: String, amount: Double, region: String)
177
178
def processSalesData(records: List[SalesRecord]): Map[String, Double] = {
179
records
180
.tap(data => println(s"Processing ${data.length} sales records"))
181
.filter(_.amount > 0)
182
.tap(filtered => println(s"${filtered.length} records after filtering"))
183
.groupBy(_.region)
184
.tap(grouped => println(s"Grouped into ${grouped.size} regions"))
185
.view
186
.mapValues(_.map(_.amount).sum)
187
.toMap
188
.tap(totals => println(s"Region totals: $totals"))
189
}
190
191
val sales = List(
192
SalesRecord("laptop", 999.99, "north"),
193
SalesRecord("mouse", 29.99, "south"),
194
SalesRecord("laptop", -50.0, "north"), // negative - will be filtered
195
SalesRecord("keyboard", 79.99, "south")
196
)
197
198
val result = processSalesData(sales)
199
// Output:
200
// Processing 4 sales records
201
// 3 records after filtering
202
// Grouped into 2 regions
203
// Region totals: Map(north -> 999.99, south -> 109.98)
204
```
205
206
### API Response Processing
207
208
```scala
209
import scala.util.chaining._
210
import scala.util.{Try, Success, Failure}
211
212
case class ApiResponse(status: Int, body: String)
213
case class User(id: Int, name: String, email: String)
214
215
def parseUser(response: ApiResponse): Option[User] = {
216
response
217
.tap(resp => println(s"Received response: ${resp.status}"))
218
.pipe { resp =>
219
if (resp.status == 200) Some(resp.body) else None
220
}
221
.tap {
222
case Some(body) => println(s"Parsing body: $body")
223
case None => println("Non-200 response, skipping parse")
224
}
225
.flatMap { body =>
226
Try {
227
// Simplified JSON parsing
228
val parts = body.split(",")
229
User(
230
id = parts(0).toInt,
231
name = parts(1),
232
email = parts(2)
233
)
234
}.toOption
235
}
236
.tap {
237
case Some(user) => println(s"Successfully parsed user: $user")
238
case None => println("Failed to parse user")
239
}
240
}
241
```
242
243
### Functional Error Handling
244
245
```scala
246
import scala.util.chaining._
247
248
sealed trait ProcessingError
249
case class ValidationError(message: String) extends ProcessingError
250
case class NetworkError(message: String) extends ProcessingError
251
case class ParseError(message: String) extends ProcessingError
252
253
def processRequest(input: String): Either[ProcessingError, String] = {
254
input
255
.pipe(validateInput)
256
.tap {
257
case Right(valid) => println(s"Input validated: $valid")
258
case Left(error) => println(s"Validation failed: $error")
259
}
260
.flatMap(fetchData)
261
.tap {
262
case Right(data) => println(s"Data fetched: ${data.length} chars")
263
case Left(error) => println(s"Fetch failed: $error")
264
}
265
.flatMap(parseData)
266
.tap {
267
case Right(result) => println(s"Parse successful: $result")
268
case Left(error) => println(s"Parse failed: $error")
269
}
270
}
271
272
def validateInput(input: String): Either[ValidationError, String] = {
273
if (input.nonEmpty) Right(input.trim)
274
else Left(ValidationError("Input cannot be empty"))
275
}
276
277
def fetchData(input: String): Either[NetworkError, String] = {
278
// Simulate network call
279
if (input.startsWith("valid")) Right(s"data_for_$input")
280
else Left(NetworkError("Network request failed"))
281
}
282
283
def parseData(data: String): Either[ParseError, String] = {
284
if (data.contains("data_for_")) Right(data.replace("data_for_", "result_"))
285
else Left(ParseError("Invalid data format"))
286
}
287
```
288
289
### Builder Pattern Alternative
290
291
```scala
292
import scala.util.chaining._
293
294
case class HttpRequest(
295
url: String = "",
296
method: String = "GET",
297
headers: Map[String, String] = Map.empty,
298
body: Option[String] = None,
299
timeout: Int = 30000
300
)
301
302
def buildRequest(baseUrl: String): HttpRequest = {
303
HttpRequest()
304
.pipe(_.copy(url = s"$baseUrl/api/users"))
305
.pipe(_.copy(method = "POST"))
306
.pipe(req => req.copy(headers = req.headers + ("Content-Type" -> "application/json")))
307
.pipe(req => req.copy(headers = req.headers + ("Authorization" -> "Bearer token123")))
308
.pipe(_.copy(body = Some("""{"name": "John", "email": "john@example.com"}""")))
309
.pipe(_.copy(timeout = 60000))
310
.tap(request => println(s"Built request: $request"))
311
}
312
```
313
314
## Performance Notes
315
316
- `tap` and `pipe` methods are implemented as inline methods on a value class, so there's minimal runtime overhead
317
- The `@inline` annotation ensures the method calls are inlined at compile time
318
- `ChainingOps` extends `AnyVal` to avoid object allocation in most cases
319
320
## Comparison with Other Approaches
321
322
### Before (nested calls)
323
```scala
324
val result = process(transform(validate(input)))
325
```
326
327
### After (with pipe)
328
```scala
329
val result = input
330
.pipe(validate)
331
.pipe(transform)
332
.pipe(process)
333
```
334
335
### Before (intermediate variables)
336
```scala
337
val validated = validate(input)
338
println(s"Validated: $validated")
339
val transformed = transform(validated)
340
println(s"Transformed: $transformed")
341
val result = process(transformed)
342
```
343
344
### After (with tap and pipe)
345
```scala
346
val result = input
347
.pipe(validate)
348
.tap(validated => println(s"Validated: $validated"))
349
.pipe(transform)
350
.tap(transformed => println(s"Transformed: $transformed"))
351
.pipe(process)
352
```