A Deeper Dive into Strings: Interpolation and Multiline

Introduction

Strings are fundamental to most applications, whether you're building user interfaces, processing text data, or generating reports. Scala provides powerful and elegant string manipulation features that go far beyond basic concatenation.

In this lesson, you'll master Scala's string interpolation mechanisms, learn to work with multiline strings, and discover advanced techniques for text processing that will make your code more readable and maintainable.

String Interpolation Overview

Scala offers three types of string interpolation, each serving different purposes:

  • s-interpolation: Variable substitution and expression evaluation
  • f-interpolation: Printf-style formatting
  • raw-interpolation: Raw strings without escape sequence processing

All interpolators use the same basic syntax: a prefix letter followed by a string literal.

s-Interpolation: The Workhorse

The s-interpolator is the most commonly used. It allows you to embed variables and expressions directly in strings:

Basic Variable Substitution

val name = "Alice"
val age = 30
val city = "San Francisco"

// Basic substitution
val intro = s"My name is $name"
println(intro)  // "My name is Alice"

// Multiple variables
val details = s"$name is $age years old and lives in $city"
println(details)  // "Alice is 30 years old and lives in San Francisco"

Expression Evaluation

You can embed any Scala expression using ${...}:

val a = 10
val b = 20

val calculation = s"$a + $b = ${a + b}"
println(calculation)  // "10 + 20 = 30"

val comparison = s"$a is ${if (a > b) "greater than" else "less than or equal to"} $b"
println(comparison)  // "10 is less than or equal to 20"

// Method calls
val text = "hello world"
val formatted = s"Original: '$text', Capitalized: '${text.capitalize}', Length: ${text.length}"
println(formatted)  // "Original: 'hello world', Capitalized: 'Hello world', Length: 11"

Working with Objects

case class Person(firstName: String, lastName: String, age: Int) {
  def fullName: String = s"$firstName $lastName"
}

val person = Person("John", "Doe", 35)

val greeting = s"Hello, ${person.fullName}! You are ${person.age} years old."
println(greeting)  // "Hello, John Doe! You are 35 years old."

// Accessing fields directly
val info = s"${person.firstName} ${person.lastName} (${person.age})"
println(info)  // "John Doe (35)"

Complex Expressions

val numbers = List(1, 2, 3, 4, 5)
val stats = s"Numbers: ${numbers.mkString(", ")}, Sum: ${numbers.sum}, Average: ${numbers.sum.toDouble / numbers.length}"
println(stats)  // "Numbers: 1, 2, 3, 4, 5, Sum: 15, Average: 3.0"

// Nested function calls
val emails = List("alice@example.com", "bob@test.org", "charlie@demo.net")
val domains = s"Domains: ${emails.map(_.split("@")(1)).distinct.mkString(", ")}"
println(domains)  // "Domains: example.com, test.org, demo.net"

f-Interpolation: Printf-Style Formatting

The f-interpolator provides printf-style formatting capabilities:

Basic Formatting

val name = "Alice"
val score = 87.6666
val rank = 3

// Basic formatting
val result = f"$name scored $score%.2f and ranked #$rank%d"
println(result)  // "Alice scored 87.67 and ranked #3"

Numeric Formatting

val pi = math.Pi
val price = 19.99
val quantity = 42

// Decimal places
val piFormatted = f"π = $pi%.4f"  // "π = 3.1416"

// Padding and alignment
val report = f"Item: $quantity%5d units at $$${price}%8.2f each"
// "Item:    42 units at $   19.99 each"

// Scientific notation
val avogadro = 6.02214076e23
val scientific = f"Avogadro's number: $avogadro%.2e"
// "Avogadro's number: 6.02e+23"

println(piFormatted)
println(report)
println(scientific)

String Formatting

val firstName = "Alice"
val lastName = "Smith"

// Width and alignment
val leftAligned = f"Name: $firstName%-10s $lastName%-10s"
// "Name: Alice      Smith     "

val rightAligned = f"Name: $firstName%10s $lastName%10s"
// "Name:      Alice     Smith"

println(s"'$leftAligned'")
println(s"'$rightAligned'")

Advanced Formatting Examples

case class Product(name: String, price: Double, quantity: Int) {
  def total: Double = price * quantity
}

val products = List(
  Product("Laptop", 999.99, 2),
  Product("Mouse", 25.50, 5),
  Product("Keyboard", 75.00, 3)
)

println("Product Report")
println("=" * 50)
println(f"${"Name"}%-15s ${"Price"}%8s ${"Qty"}%4s ${"Total"}%10s")
println("-" * 50)

products.foreach { product =>
  println(f"${product.name}%-15s $$${product.price}%7.2f ${product.quantity}%4d $$${product.total}%9.2f")
}

val grandTotal = products.map(_.total).sum
println("-" * 50)
println(f"${"Grand Total"}%-28s $$${grandTotal}%9.2f")

raw-Interpolation: Literal Strings

The raw-interpolator treats escape sequences literally:

Basic Raw Strings

// Regular string with escapes
val regularPath = "C:\\Users\\Alice\\Documents"
val regularRegex = "\\d+\\.\\d+"

// Raw strings (escapes not processed)
val rawPath = raw"C:\Users\Alice\Documents"
val rawRegex = raw"\d+\.\d+"

println(regularPath)  // "C:\Users\Alice\Documents"
println(rawPath)      // "C:\Users\Alice\Documents" (same result)
println(regularRegex) // "\d+\.\d+"
println(rawRegex)     // "\d+\.\d+" (same result)

// But raw still allows variable substitution
val username = "Alice"
val userPath = raw"C:\Users\$username\Documents"
println(userPath)     // "C:\Users\Alice\Documents"

Regular Expressions

Raw strings are particularly useful for regex patterns:

val emailPattern = raw"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
val phonePattern = raw"\d{3}-\d{3}-\d{4}"
val urlPattern = raw"https?://[^\s]+"

val text = "Contact John at john@example.com or call 555-123-4567. Visit https://example.com"

val emailRegex = emailPattern.r
val phoneRegex = phonePattern.r
val urlRegex = urlPattern.r

emailRegex.findAllIn(text).foreach(email => println(s"Found email: $email"))
phoneRegex.findAllIn(text).foreach(phone => println(s"Found phone: $phone"))
urlRegex.findAllIn(text).foreach(url => println(s"Found URL: $url"))

Multiline Strings

Triple-Quoted Strings

Scala supports multiline strings using triple quotes:

val poem = """Roses are red,
Violets are blue,
Scala is awesome,
And so are you!"""

println(poem)

stripMargin for Clean Formatting

Use stripMargin to handle indentation in multiline strings:

val sqlQuery = """SELECT u.name, u.email, p.title
                 |FROM users u
                 |JOIN posts p ON u.id = p.author_id
                 |WHERE u.active = true
                 |ORDER BY u.name""".stripMargin

println(sqlQuery)

// Custom margin character
val htmlTemplate = """<html>
                     #  <head>
                     #    <title>My Page</title>
                     #  </head>
                     #  <body>
                     #    <h1>Welcome!</h1>
                     #  </body>
                     #</html>""".stripMargin('#')

println(htmlTemplate)

Multiline with Interpolation

Combine multiline strings with interpolation:

val user = "Alice"
val itemCount = 5
val total = 99.99

val receipt = s"""
  |=====================================
  |           RECEIPT
  |=====================================
  |Customer: $user
  |Items:    $itemCount
  |Total:    $$${total}%.2f
  |
  |Thank you for your purchase!
  |=====================================
  """.stripMargin

println(receipt)

Advanced String Techniques

Building Complex Templates

case class Invoice(
  invoiceNumber: String,
  customerName: String,
  items: List[(String, Int, Double)],
  taxRate: Double
) {
  def subtotal: Double = items.map { case (_, qty, price) => qty * price }.sum
  def tax: Double = subtotal * taxRate
  def total: Double = subtotal + tax

  def toFormattedString: String = {
    val itemLines = items.map { case (name, qty, price) =>
      f"  $name%-20s $qty%3d × $$${price}%6.2f = $$${qty * price}%8.2f"
    }.mkString("\n")

    f"""
      |INVOICE #$invoiceNumber
      |${"=" * 50}
      |Customer: $customerName
      |
      |Items:
      |$itemLines
      |${"-" * 50}
      |Subtotal: $$${subtotal}%8.2f
      |Tax:      $$${tax}%8.2f
      |Total:    $$${total}%8.2f
      |${"=" * 50}
    """.stripMargin
  }
}

val invoice = Invoice(
  "INV-001",
  "Alice Johnson",
  List(
    ("Laptop", 1, 999.99),
    ("Mouse", 2, 25.50),
    ("Keyboard", 1, 75.00)
  ),
  0.08
)

println(invoice.toFormattedString)

String Builder Pattern

For complex string construction, consider using StringBuilder:

import scala.collection.mutable.StringBuilder

def generateReport(data: List[(String, Int)]): String = {
  val sb = new StringBuilder()

  sb.append("Data Report\n")
  sb.append("=" * 20).append("\n")

  data.foreach { case (name, value) =>
    sb.append(f"$name%-15s: $value%5d\n")
  }

  sb.append("-" * 20).append("\n")
  val total = data.map(_._2).sum
  sb.append(f"Total: $total%10d\n")

  sb.toString()
}

val data = List(("Apples", 150), ("Bananas", 200), ("Oranges", 75))
println(generateReport(data))

Practical String Processing Examples

Example 1: Log Entry Parser

case class LogEntry(timestamp: String, level: String, message: String)

object LogParser {
  def parseEntry(line: String): Option[LogEntry] = {
    val pattern = raw"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (.+)".r

    line match {
      case pattern(timestamp, level, message) => 
        Some(LogEntry(timestamp, level, message))
      case _ => None
    }
  }

  def formatEntry(entry: LogEntry): String = {
    f"${entry.timestamp} [${entry.level}%5s] ${entry.message}"
  }

  def generateSummary(entries: List[LogEntry]): String = {
    val levelCounts = entries.groupBy(_.level).view.mapValues(_.size).toMap
    val lines = levelCounts.map { case (level, count) =>
      f"$level%-8s: $count%4d entries"
    }.mkString("\n")

    s"""
      |Log Summary
      |===========
      |$lines
      |Total: ${entries.size} entries
    """.stripMargin
  }
}

val logLines = List(
  "2023-08-31 10:15:30 [INFO] Application started",
  "2023-08-31 10:15:35 [DEBUG] Loading configuration",
  "2023-08-31 10:15:40 [WARN] Deprecated method used",
  "2023-08-31 10:15:45 [ERROR] Database connection failed"
)

val entries = logLines.flatMap(LogParser.parseEntry)
entries.foreach(entry => println(LogParser.formatEntry(entry)))
println(LogParser.generateSummary(entries))

Example 2: Configuration File Generator

case class DatabaseConfig(
  host: String,
  port: Int,
  database: String,
  username: String,
  password: String,
  maxConnections: Int = 10,
  timeout: Int = 30
)

case class AppConfig(
  appName: String,
  version: String,
  debug: Boolean,
  database: DatabaseConfig
)

object ConfigGenerator {
  def generateProperties(config: AppConfig): String = {
    s"""# ${config.appName} Configuration
       |# Version: ${config.version}
       |# Generated on: ${java.time.LocalDateTime.now()}
       |
       |# Application Settings
       |app.name=${config.appName}
       |app.version=${config.version}
       |app.debug=${config.debug}
       |
       |# Database Configuration
       |db.host=${config.database.host}
       |db.port=${config.database.port}
       |db.database=${config.database.database}
       |db.username=${config.database.username}
       |db.password=${config.database.password}
       |db.max-connections=${config.database.maxConnections}
       |db.timeout=${config.database.timeout}
       |""".stripMargin
  }

  def generateYaml(config: AppConfig): String = {
    s"""# ${config.appName} Configuration
       |# Version: ${config.version}
       |
       |app:
       |  name: "${config.appName}"
       |  version: "${config.version}"
       |  debug: ${config.debug}
       |
       |database:
       |  host: "${config.database.host}"
       |  port: ${config.database.port}
       |  database: "${config.database.database}"
       |  username: "${config.database.username}"
       |  password: "${config.database.password}"
       |  max-connections: ${config.database.maxConnections}
       |  timeout: ${config.database.timeout}
       |""".stripMargin
  }

  def generateJson(config: AppConfig): String = {
    s"""{
       |  "app": {
       |    "name": "${config.appName}",
       |    "version": "${config.version}",
       |    "debug": ${config.debug}
       |  },
       |  "database": {
       |    "host": "${config.database.host}",
       |    "port": ${config.database.port},
       |    "database": "${config.database.database}",
       |    "username": "${config.database.username}",
       |    "password": "${config.database.password}",
       |    "maxConnections": ${config.database.maxConnections},
       |    "timeout": ${config.database.timeout}
       |  }
       |}""".stripMargin
  }
}

val config = AppConfig(
  "MyScalaApp",
  "1.0.0",
  true,
  DatabaseConfig(
    "localhost",
    5432,
    "myapp_db",
    "dbuser",
    "secret123"
  )
)

println("=== Properties Format ===")
println(ConfigGenerator.generateProperties(config))

println("\n=== YAML Format ===")
println(ConfigGenerator.generateYaml(config))

println("\n=== JSON Format ===")
println(ConfigGenerator.generateJson(config))

Example 3: Code Generator

case class Field(name: String, fieldType: String, isOptional: Boolean = false)
case class ClassDefinition(name: String, fields: List[Field])

object ScalaCodeGenerator {
  def generateCaseClass(classDef: ClassDefinition): String = {
    val fieldDefs = classDef.fields.map { field =>
      val typeStr = if (field.isOptional) s"Option[${field.fieldType}]" else field.fieldType
      val defaultStr = if (field.isOptional) " = None" else ""
      s"  ${field.name}: $typeStr$defaultStr"
    }.mkString(",\n")

    s"""case class ${classDef.name}(
       |$fieldDefs
       |)""".stripMargin
  }

  def generateCompanionObject(classDef: ClassDefinition): String = {
    val requiredFields = classDef.fields.filterNot(_.isOptional)
    val factoryParams = requiredFields.map(f => s"${f.name}: ${f.fieldType}").mkString(", ")
    val constructorArgs = classDef.fields.map { field =>
      if (field.isOptional) "None" else field.name
    }.mkString(", ")

    s"""object ${classDef.name} {
       |  def apply($factoryParams): ${classDef.name} = 
       |    new ${classDef.name}($constructorArgs)
       |    
       |  def empty: ${classDef.name} = ${classDef.name}(${
         classDef.fields.map { field =>
           if (field.isOptional) "None"
           else field.fieldType match {
             case "String" => "\"\""
             case "Int" => "0"
             case "Double" => "0.0"
             case "Boolean" => "false"
             case _ => "???"
           }
         }.mkString(", ")
       })
       |}""".stripMargin
  }

  def generateCompleteClass(classDef: ClassDefinition): String = {
    s"""${generateCaseClass(classDef)}
       |
       |${generateCompanionObject(classDef)}""".stripMargin
  }
}

val userClass = ClassDefinition(
  "User",
  List(
    Field("id", "Int"),
    Field("name", "String"),
    Field("email", "String"),
    Field("age", "Int", isOptional = true),
    Field("profileUrl", "String", isOptional = true)
  )
)

println(ScalaCodeGenerator.generateCompleteClass(userClass))

Performance Considerations

String Concatenation vs Interpolation

// Avoid: Multiple concatenations (creates many temporary strings)
val inefficient = "Hello" + " " + name + "!" + " You are " + age + " years old."

// Better: Use interpolation
val efficient = s"Hello $name! You are $age years old."

// For many operations: Use StringBuilder
val sb = new StringBuilder()
for (i <- 1 to 1000) {
  sb.append(s"Line $i\n")
}
val result = sb.toString()

Choosing the Right Interpolator

// Use s-interpolation for general purposes
val message = s"User $userName logged in at $timestamp"

// Use f-interpolation when you need formatting
val formatted = f"Score: $score%.2f%%"

// Use raw-interpolation for regex and paths
val regex = raw"\d{3}-\d{2}-\d{4}"
val path = raw"C:\Users\$username\Documents"

Common Pitfalls and Solutions

1. Escaping in Interpolated Strings

val price = 19.99

// Wrong: This will cause a compilation error
// val message = s"Price: $$price"  // Error: $ needs escaping

// Correct ways:
val message1 = s"Price: $$${price}"           // "Price: $19.99"
val message2 = s"Price: ${literal("$")}$price" // Using a helper
val message3 = f"Price: $$${price}%.2f"       // "Price: $19.99"

def literal(s: String): String = s

2. Multiline String Indentation

// Problem: Unwanted indentation
val badHtml = s"""<html>
                    <body>
                      <h1>$title</h1>
                    </body>
                  </html>"""

// Solution: Use stripMargin
val goodHtml = s"""<html>
                  |  <body>
                  |    <h1>$title</h1>
                  |  </body>
                  |</html>""".stripMargin

3. Type Safety with f-interpolation

val name = "Alice"
val score = 95.5

// Wrong: Type mismatch
// val wrong = f"$name%d scored $score%s"  // Error: %d expects Int, %s expects String

// Correct: Match format specifiers with types
val correct = f"$name%s scored $score%.1f"

Summary

In this lesson, you've mastered Scala's powerful string manipulation features:

s-Interpolation: Variable substitution and expression evaluation
f-Interpolation: Printf-style formatting with type safety
raw-Interpolation: Literal strings for regex and file paths
Multiline Strings: Triple quotes and stripMargin for clean formatting
Advanced Techniques: Templates, builders, and code generation
Performance Tips: Choosing the right approach for your use case
Best Practices: Avoiding common pitfalls and writing maintainable code

String interpolation makes your code more readable and maintainable while providing powerful formatting capabilities. These skills will be invaluable as you build real-world applications that process and display text.

What's Next

In the next lesson, we'll explore "Loops in Scala: for and while." You'll learn how to perform iteration in Scala using while loops and the more idiomatic and powerful for-comprehensions.

We'll see how Scala's approach to iteration is more functional and expressive than traditional imperative loops, setting the stage for working with collections in later lessons.

Ready to master iteration in Scala? Let's continue!