Scala - String Interpolation (s, f, raw)

The `s` interpolator is the most commonly used string interpolator in Scala. It allows you to embed variables and expressions directly into strings using the `$` prefix.

Key Insights

  • Scala provides three built-in string interpolators: s for simple variable substitution, f for formatted output with type safety, and raw for literal strings without escape sequence processing
  • The s interpolator supports arbitrary expressions in ${} blocks and evaluates them at runtime, while f performs compile-time type checking against format specifiers
  • Custom interpolators can be implemented by defining implicit classes with methods matching the interpolator prefix, enabling domain-specific string processing

The s Interpolator: Expression Evaluation

The s interpolator is the most commonly used string interpolator in Scala. It allows you to embed variables and expressions directly into strings using the $ prefix.

val name = "Alice"
val age = 30
val greeting = s"Hello, $name. You are $age years old."
// Result: "Hello, Alice. You are 30 years old."

For complex expressions, wrap them in curly braces:

val price = 29.99
val quantity = 3
val total = s"Total cost: ${price * quantity}"
// Result: "Total cost: 89.97"

case class User(firstName: String, lastName: String)
val user = User("John", "Doe")
val message = s"Welcome, ${user.firstName} ${user.lastName}!"
// Result: "Welcome, John Doe!"

The s interpolator evaluates expressions at runtime and calls toString on the results. This makes it flexible but means you lose compile-time formatting guarantees:

val numbers = List(1, 2, 3)
val output = s"Numbers: ${numbers.mkString(", ")}"
// Result: "Numbers: 1, 2, 3"

val condition = true
val status = s"Status: ${if (condition) "active" else "inactive"}"
// Result: "Status: active"

The f Interpolator: Type-Safe Formatting

The f interpolator provides printf-style formatting with compile-time type checking. It ensures that format specifiers match the types of the interpolated values.

val pi = 3.14159
val formatted = f"Pi to 2 decimal places: $pi%.2f"
// Result: "Pi to 2 decimal places: 3.14"

val count = 42
val hex = f"Decimal: $count%d, Hex: $count%x"
// Result: "Decimal: 42, Hex: 2a"

The format specifier follows the variable name after a % symbol. Common specifiers include:

val num = 123.456
val examples = List(
  f"$num%f",           // 123.456000 (default float)
  f"$num%.2f",         // 123.46 (2 decimal places)
  f"$num%10.2f",       // "    123.46" (width 10, right-aligned)
  f"$num%-10.2f",      // "123.46    " (width 10, left-aligned)
  f"$num%e"            // 1.234560e+02 (scientific notation)
)

val integer = 255
val intFormats = List(
  f"$integer%d",       // 255 (decimal)
  f"$integer%x",       // ff (hexadecimal)
  f"$integer%o",       // 377 (octal)
  f"$integer%05d"      // 00255 (zero-padded to width 5)
)

Type safety is enforced at compile time:

val text = "hello"
// f"$text%d"  // Compile error: type mismatch

val number = 42
val safe = f"$number%d"  // Compiles successfully

For complex formatting scenarios:

case class Product(name: String, price: Double, quantity: Int)
val product = Product("Laptop", 999.99, 5)

val invoice = f"""
  |Product: ${product.name}
  |Price: $$${product.price}%.2f
  |Quantity: ${product.quantity}%d
  |Total: $$${product.price * product.quantity}%.2f
  """.stripMargin

Note the escaped $$ to produce a literal dollar sign in the output.

The raw Interpolator: Preserving Escape Sequences

The raw interpolator works like s but does not process escape sequences in the string literal itself. This is useful when you need to preserve backslashes and other special characters.

val path = raw"C:\Users\Documents\file.txt"
// Result: "C:\Users\Documents\file.txt"

// Compare with s interpolator:
val pathS = s"C:\Users\Documents\file.txt"
// Result: "C:UsersDocumentsfile.txt" (escape sequences processed)

The distinction is important for regular expressions and file paths:

val regex = raw"\d{3}-\d{2}-\d{4}"
// Result: "\d{3}-\d{2}-\d{4}" (literal backslashes preserved)

val regexS = s"\d{3}-\d{2}-\d{4}"
// Result: "d{3}-d{2}-d{4}" (backslashes interpreted as escapes)

However, raw still evaluates interpolated expressions:

val username = "admin"
val windowsPath = raw"C:\Users\$username\Desktop"
// Result: "C:\Users\admin\Desktop"

val newline = "\n"
val mixed = raw"Line1${newline}Line2"
// Result: "Line1
//         Line2"
// The interpolated variable still contains a processed newline

This behavior makes raw suitable for scenarios where the string literal contains backslashes but you still need variable substitution:

val tableName = "users"
val sqlQuery = raw"SELECT * FROM $tableName WHERE name LIKE '%\_%'"
// Result: "SELECT * FROM users WHERE name LIKE '%\_%'"
// The \_ is preserved for SQL LIKE pattern matching

Multiline Strings with Interpolation

All three interpolators work with triple-quoted strings for multiline content:

val name = "Bob"
val age = 25
val multiline = s"""
  |Name: $name
  |Age: $age
  |Status: ${if (age >= 18) "Adult" else "Minor"}
  """.stripMargin

val table = f"""
  |${"Name"}%-10s | ${"Age"}%3s | ${"Score"}%5s
  |${"-" * 10}%-10s | ${"-" * 3}%3s | ${"-" * 5}%5s
  |${"Alice"}%-10s | ${30}%3d | ${95.5}%5.1f
  |${"Bob"}%-10s | ${25}%3d | ${87.3}%5.1f
  """.stripMargin

Custom Interpolators

You can create custom interpolators by defining an implicit class with a method matching the interpolator name:

implicit class JsonInterpolator(val sc: StringContext) extends AnyVal {
  def json(args: Any*): String = {
    val strings = sc.parts.iterator
    val expressions = args.iterator
    val buf = new StringBuilder(strings.next())
    
    while (strings.hasNext) {
      val arg = expressions.next()
      val jsonValue = arg match {
        case s: String => s""""$s""""
        case n: Int => n.toString
        case n: Double => n.toString
        case b: Boolean => b.toString
        case null => "null"
        case _ => s""""${arg.toString}""""
      }
      buf.append(jsonValue)
      buf.append(strings.next())
    }
    buf.toString
  }
}

val name = "Alice"
val age = 30
val active = true
val result = json"""{"name": $name, "age": $age, "active": $active}"""
// Result: """{"name": "Alice", "age": 30, "active": true}"""

Custom interpolators enable domain-specific languages and specialized formatting:

implicit class SqlInterpolator(val sc: StringContext) extends AnyVal {
  def sql(args: Any*): String = {
    // Implement SQL escaping and parameterization
    sc.parts.zip(args).map { case (part, arg) =>
      part + escapeSQL(arg)
    }.mkString + sc.parts.last
  }
  
  private def escapeSQL(arg: Any): String = arg match {
    case s: String => s"'${s.replace("'", "''")}'"
    case other => other.toString
  }
}

String interpolation in Scala provides a powerful, type-safe mechanism for constructing strings. Choose s for general-purpose interpolation, f when you need formatting control with type safety, and raw when working with strings containing literal backslashes. For specialized use cases, custom interpolators extend this functionality to match your domain requirements.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.