or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

annotation-backports.mdbackported-collections.mdcollection-extensions.mdcollection-factories.mdindex.mditerator-size-ops.mdjava-interop.mdmap-extensions.mdmethod-chaining.mdoption-converters.mdresource-management.mdstring-parsing.md

method-chaining.mddocs/

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

```