String interpolators in Scala

Versions: Scala 2.12.1

I've always found concatenating Strings with "+" a laborious task. Hence when I've discovered Scala and its text construction methods I was immediately convinced. And as you can imagine, this topic merits its own short post in the Scala category.

This post is composed of 3 short sections. Each one describes one of the available string interpolators. The last part gives an example of a custom interpolator.

s String interpolator

The first and maybe the most popular interpolator is s. It lets us to write Strings with embedded variables or expressions. The former ones are added simply with a dollar sign ($myVariable) and the latter with dollar and braces (${1 + 2}). Please notice that such constructed Strings don't escape special characters and the symbols like \n, \t will be translated respectively to a new line and a tabulation:

describe("s interpolator") {
  it("should add a variable in the String") {
    val age = 100

    val interpolatedString = s"My age is $age"

    interpolatedString shouldEqual "My age is 100"
  }
  it("should add an expression in the String") {
    def sentenceTerminationSymbol = "."

    val interpolatedString = s"My age is ${20 + 20 + 20}${sentenceTerminationSymbol}"

    interpolatedString shouldEqual "My age is 60."
  }
  it("should interpolate string with special characters") {
    val tabSymbol = "\t"

    val interpolatedString = s"1${tabSymbol}1"

    interpolatedString shouldEqual "1\t1"
  }
}

f String interpolator

Another interpolator is symbolized with f. It's similar to printf methods present in other languages. The interpolated String can be decorated with format specifiers represented as with %-prefixed symbols. This interpolator is the synonymous of java.lang.String#format(java.lang.String, java.lang.Object...) method. Aside of that the f interpolator is type-safe, i.e. if we try to use a bad type for given formatting symbol, the compilation will simply fail:

describe("f interpolator") {
  it("should format decimals") {
    val amount = 35.998888d

    val interpolatedString = f"Amount to pay ${amount}%.2f"

    interpolatedString shouldEqual "Amount to pay 36.00"
  }
  it("should format number in separate lines") {
    val number = 1

    val interpolatedString = f"${number}%n${number}"

    interpolatedString shouldEqual
      """|1
        |1""".stripMargin
  }

  /**
    * The following method fails at compile time:
    *
    * it("should fail with incorrect type") {
    * val amount = "abc"
    *
    * val interpolatedString = f"Amount to pay ${amount}%.2f"
    *
    * interpolatedString shouldEqual "Amount to pay 36.00"
    * }
    * Error:(48, 50) type mismatch;
    * found   : String
    * required: Double
    * val interpolatedString = f"Amount to pay ${amount}%.2f"
    */
}

raw String interpolator

The last and at the same time the easiest interpolator starts with raw. It's responsible for formatting raw String similarly to s. The diferenece is that all characters with special meaning are escaped as we would write the same text with double anti-slashes (\\):

describe("raw interpolator") {
  it("should escape literals") {
    val interpolatedString = raw"tab\t and new line\n are escaped"

    interpolatedString shouldEqual "tab\\t and new line\\n are escaped"
  }
}

Interpolators under-the-hood

As you can see through above sections, interpolators are a very easy way to deal with Strings in Scala. Internally the interpolators are implemented as simple methods taking as argument a varargs of Any type:

// StringContext.scala
def s(args: Any*): String = standardInterpolator(treatEscapes, args)
def f[A >: Any](args: A*): String = macro ???
def raw(args: Any*): String = standardInterpolator(identity, args)

As you can see, the s and raw interpolators call a standardInterpolator method. Internally it uses Java's StringBuilder represented here as JLSBuilder:

def standardInterpolator(process: String => String, args: Seq[Any]): String = {
  checkLengths(args)
  val pi = parts.iterator
  val ai = args.iterator
  val bldr = new JLSBuilder(process(pi.next()))
  while (ai.hasNext) {
    bldr append ai.next
    bldr append process(pi.next())
  }
  bldr.toString
}

Regarding to the f interpolator, it builds a list of parameters that are next filled to already mentioned String's format via scala.collection.immutable.StringLike#format method.

String interpolators are another great Scala invention to make programmers life easier. Thanks to them we don't need anymore to concatenate Strings with "+"s, StringBuilder or to use a verbose String.format(...) method. The interpolators use them internally but then the details are hidden so well that concatenating Strings is not a big deal.