CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-scala-lang-modules--scala-collection-compat

A library that makes some Scala 2.13 APIs available on Scala 2.11 and 2.12, facilitating cross-building Scala 2.13 and 3.0 code on older versions

Pending
Overview
Eval results
Files

method-chaining.mddocs/

Method Chaining

Utility methods for functional programming patterns, debugging, and creating fluent APIs through method chaining operations.

ChainingOps Class

final class ChainingOps[A](private val self: A) extends AnyVal {
  def tap[U](f: A => U): A
  def pipe[B](f: A => B): B
}

Extension methods that enable fluent method chaining for any type.

tap Method

def tap[U](f: A => U): A

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.

Usage:

import scala.util.chaining._

val result = List(1, 2, 3, 4, 5)
  .tap(xs => println(s"Original list: $xs"))
  .filter(_ % 2 == 0)
  .tap(evens => println(s"Even numbers: $evens"))
  .map(_ * 2)
  .tap(doubled => println(s"Doubled: $doubled"))

// Output:
// Original list: List(1, 2, 3, 4, 5)
// Even numbers: List(2, 4)
// Doubled: List(4, 8)
// result: List(4, 8)

Common use cases:

  • Debugging intermediate values in method chains
  • Logging operations
  • Performing validation or assertions
  • Side effects like caching or metrics collection

pipe Method

def pipe[B](f: A => B): B

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.

Usage:

import scala.util.chaining._

// Instead of nested function calls
val result1 = math.abs(math.pow(2.5, 3) - 10)

// Use pipe for forward flow
val result2 = 2.5
  .pipe(math.pow(_, 3))
  .pipe(_ - 10)
  .pipe(math.abs)

// Both give the same result: 5.375

Advanced usage:

case class Person(name: String, age: Int)
case class Adult(person: Person)
case class Senior(adult: Adult)

def toAdult(p: Person): Option[Adult] = 
  if (p.age >= 18) Some(Adult(p)) else None

def toSenior(a: Adult): Option[Senior] = 
  if (a.person.age >= 65) Some(Senior(a)) else None

val person = Person("Alice", 70)
val result = person
  .pipe(toAdult)
  .flatMap(_.pipe(toSenior))
  .tap(_.foreach(s => println(s"Senior: ${s.adult.person.name}")))

// Output: Senior: Alice
// result: Some(Senior(Adult(Person(Alice,70))))

ChainingSyntax Trait

trait ChainingSyntax {
  @`inline` implicit final def scalaUtilChainingOps[A](a: A): ChainingOps[A] = 
    new ChainingOps(a)
}

Provides the implicit conversion that adds chaining methods to all types.

chaining Object

object chaining extends ChainingSyntax

The main entry point for chaining operations. Import this to get access to tap and pipe methods on all types.

Usage Examples

Debugging and Logging

import scala.util.chaining._

def processUserData(rawData: String): Option[Int] = {
  rawData
    .tap(data => println(s"Processing: $data"))
    .trim
    .tap(trimmed => println(s"After trim: '$trimmed'"))
    .pipe(_.toIntOption)
    .tap {
      case Some(num) => println(s"Parsed number: $num")
      case None => println("Failed to parse number")
    }
    .filter(_ > 0)
    .tap(filtered => println(s"Final result: $filtered"))
}

processUserData("  42  ")
// Output:
// Processing:   42  
// After trim: '42'
// Parsed number: 42
// Final result: Some(42)

Configuration and Validation

import scala.util.chaining._

case class DatabaseConfig(
  host: String,
  port: Int, 
  database: String,
  maxConnections: Int
)

def createDatabaseConfig(properties: Map[String, String]): Either[String, DatabaseConfig] = {
  properties
    .tap(props => println(s"Loading config from ${props.size} properties"))
    .pipe { props =>
      for {
        host <- props.get("db.host").toRight("Missing db.host")
        port <- props.get("db.port").flatMap(_.toIntOption).toRight("Invalid db.port")
        database <- props.get("db.name").toRight("Missing db.name")
        maxConn <- props.get("db.maxConnections").flatMap(_.toIntOption).toRight("Invalid db.maxConnections")
      } yield DatabaseConfig(host, port, database, maxConn)
    }
    .tap {
      case Right(config) => println(s"Successfully created config: $config")
      case Left(error) => println(s"Configuration error: $error")
    }
}

Data Processing Pipeline

import scala.util.chaining._

case class SalesRecord(product: String, amount: Double, region: String)

def processSalesData(records: List[SalesRecord]): Map[String, Double] = {
  records
    .tap(data => println(s"Processing ${data.length} sales records"))
    .filter(_.amount > 0)
    .tap(filtered => println(s"${filtered.length} records after filtering"))
    .groupBy(_.region)
    .tap(grouped => println(s"Grouped into ${grouped.size} regions"))
    .view
    .mapValues(_.map(_.amount).sum)
    .toMap
    .tap(totals => println(s"Region totals: $totals"))
}

val sales = List(
  SalesRecord("laptop", 999.99, "north"),
  SalesRecord("mouse", 29.99, "south"),
  SalesRecord("laptop", -50.0, "north"),  // negative - will be filtered
  SalesRecord("keyboard", 79.99, "south")
)

val result = processSalesData(sales)
// Output:
// Processing 4 sales records
// 3 records after filtering
// Grouped into 2 regions
// Region totals: Map(north -> 999.99, south -> 109.98)

API Response Processing

import scala.util.chaining._
import scala.util.{Try, Success, Failure}

case class ApiResponse(status: Int, body: String)
case class User(id: Int, name: String, email: String)

def parseUser(response: ApiResponse): Option[User] = {
  response
    .tap(resp => println(s"Received response: ${resp.status}"))
    .pipe { resp =>
      if (resp.status == 200) Some(resp.body) else None
    }
    .tap {
      case Some(body) => println(s"Parsing body: $body")
      case None => println("Non-200 response, skipping parse")
    }
    .flatMap { body =>
      Try {
        // Simplified JSON parsing
        val parts = body.split(",")
        User(
          id = parts(0).toInt,
          name = parts(1),
          email = parts(2)
        )
      }.toOption
    }
    .tap {
      case Some(user) => println(s"Successfully parsed user: $user")
      case None => println("Failed to parse user")
    }
}

Functional Error Handling

import scala.util.chaining._

sealed trait ProcessingError
case class ValidationError(message: String) extends ProcessingError
case class NetworkError(message: String) extends ProcessingError
case class ParseError(message: String) extends ProcessingError

def processRequest(input: String): Either[ProcessingError, String] = {
  input
    .pipe(validateInput)
    .tap {
      case Right(valid) => println(s"Input validated: $valid")
      case Left(error) => println(s"Validation failed: $error")
    }
    .flatMap(fetchData)
    .tap {
      case Right(data) => println(s"Data fetched: ${data.length} chars")
      case Left(error) => println(s"Fetch failed: $error")
    }
    .flatMap(parseData)
    .tap {
      case Right(result) => println(s"Parse successful: $result")
      case Left(error) => println(s"Parse failed: $error")
    }
}

def validateInput(input: String): Either[ValidationError, String] = {
  if (input.nonEmpty) Right(input.trim)
  else Left(ValidationError("Input cannot be empty"))
}

def fetchData(input: String): Either[NetworkError, String] = {
  // Simulate network call
  if (input.startsWith("valid")) Right(s"data_for_$input")
  else Left(NetworkError("Network request failed"))
}

def parseData(data: String): Either[ParseError, String] = {
  if (data.contains("data_for_")) Right(data.replace("data_for_", "result_"))
  else Left(ParseError("Invalid data format"))
}

Builder Pattern Alternative

import scala.util.chaining._

case class HttpRequest(
  url: String = "",
  method: String = "GET", 
  headers: Map[String, String] = Map.empty,
  body: Option[String] = None,
  timeout: Int = 30000
)

def buildRequest(baseUrl: String): HttpRequest = {
  HttpRequest()
    .pipe(_.copy(url = s"$baseUrl/api/users"))
    .pipe(_.copy(method = "POST"))
    .pipe(req => req.copy(headers = req.headers + ("Content-Type" -> "application/json")))
    .pipe(req => req.copy(headers = req.headers + ("Authorization" -> "Bearer token123")))
    .pipe(_.copy(body = Some("""{"name": "John", "email": "john@example.com"}""")))
    .pipe(_.copy(timeout = 60000))
    .tap(request => println(s"Built request: $request"))
}

Performance Notes

  • tap and pipe methods are implemented as inline methods on a value class, so there's minimal runtime overhead
  • The @inline annotation ensures the method calls are inlined at compile time
  • ChainingOps extends AnyVal to avoid object allocation in most cases

Comparison with Other Approaches

Before (nested calls)

val result = process(transform(validate(input)))

After (with pipe)

val result = input
  .pipe(validate)
  .pipe(transform) 
  .pipe(process)

Before (intermediate variables)

val validated = validate(input)
println(s"Validated: $validated")
val transformed = transform(validated)
println(s"Transformed: $transformed")
val result = process(transformed)

After (with tap and pipe)

val result = input
  .pipe(validate)
  .tap(validated => println(s"Validated: $validated"))
  .pipe(transform)
  .tap(transformed => println(s"Transformed: $transformed"))
  .pipe(process)

Install with Tessl CLI

npx tessl i tessl/maven-org-scala-lang-modules--scala-collection-compat

docs

annotation-backports.md

backported-collections.md

collection-extensions.md

collection-factories.md

index.md

iterator-size-ops.md

java-interop.md

map-extensions.md

method-chaining.md

option-converters.md

resource-management.md

string-parsing.md

tile.json