Puzzler 21

I Can Has Padding?

Scala provides an incremental string builder that shares the same name as, and therefore hides, java.lang.StringBuilder. Scala's StringBuilder is similar to Java's for the most part, differing mainly where providing the same methods as the Java class would conflict with the Scala collections library. And like java.lang.StringBuilder, Scala's StringBuilder is not synchronized, leaving you to handle thread safety by other means.

The following program demonstrates a StringBuilder in action. What does it do?

  implicit class Padder(val sb: StringBuilderextends AnyVal {
    def pad2(width: Int) = {
      1 to width - sb.length foreach { sb += '*' }
      sb
     }
  }
  
// length == 14 val greeting = new StringBuilder("Hello, kitteh!")  println(greeting pad2 20)
// length == 9 val farewell = new StringBuilder("U go now.")  println(farewell pad2 20)

Possibilities

  1. Prints:
      Hello, kitteh!******
      U go now.***********
    
  2. The first statement prints:
      Hello, kitteh!*
    

    and the second one throws an exception.

  3. The first statement throws an exception and the second one prints:
      U go now.***********
    
  4. Fails to compile.

Explanation

The code looks simple enough. It consists of an implicit value class[1] that transparently enriches StringBuilder with the pad2 method. The pad2 method pads a string with a specified number of asterisks. The method is then invoked twice.

Although the first answer may appear most plausible, it is, in fact, not the right one. Instead, the correct answer is number 2:

Value Classes

You may have noticed that Padder is a subclass of AnyVal. This makes it a value class. Value classes reduce runtime overhead by avoiding object allocation. Extension methods, such as pad2, are a typical use case for value classes.[2]

  scala> println(greeting pad2 20)
  Hello, kitteh!*
  
scala> println(farewell pad2 20) java.lang.StringIndexOutOfBoundsException:      String index out of range: 10   at j.l.StringBuilder.charAt(StringBuilder.java:55)   at s.c.m.StringBuilder.apply(StringBuilder.scala:114)   at s.c.m.StringBuilder.apply(StringBuilder.scala:28)   at s.c.i.Range.foreach(Range.scala:141)   at Padder$.pad2$extension(<console>:9)     ...

As the saying goes, the truth is in the details, so take a closer look at the implementation of the pad2 method. The expression, 1 to width - sb.length, is a Range, and its foreach method accepts a function as a argument:

  final def foreach(f: (A) => Unit): Unit

It does not seem as if we are passing a function, though, as the += method—essentially an alias for the StringBuilder.append method—returns the StringBuilder itself. The code does compile, however, so that can only mean that StringBuilder is itself a function. A look at the Scaladoc confirms that Scala's StringBuilder inherits from Function1, which brings you to the apply method of StringBuilder:

  def apply(index: Int): Char
Equivalent to charAt.
returns the element of this growable collection at index idx, where 0 indicates the first element.

Now things start to make sense. It is not the expression sb += '*' that is executed on each iteration of foreach. Instead, += is called once when the argument to foreach is being evaluated! In fact, it is the resulting StringBuilder whose apply method is invoked each time, retrieving the character at the given index, but not assigning it to anything. Method pad2 is actually equivalent to the following:

  def pad2(width: Int) = {
    val appendedSb = sb += '*'
    // apply calls charAt
    1 to width - sb.length foreach appendedSb.apply
    sb
  }

In both cases, 20 is passed as width to pad2, which runs without errors for the "Hello, kitteh!" string, as the maximum index passed to charAt is 6. The value of the StringBuilder (the input string plus one padding asterisk) is then printed. The second string, "U go now.", is shorter, so the range stretches to 11, which is greater than the length of the "U go now." string. Method charAt is eventually called with index 10, which results in a StringIndexOutOfBoundsException.

Discussion

To correctly pad the string, as intended, all that's required is to explicitly specify the function literal:

  def pad2(width: Int) = {
    1 to width - sb.length foreach { _ => sb += '*' }
    sb
  }

This happens automatically if you use a for expression, which the compiler translates into a foreach call behind the scenes:

  def pad2(width: Int) = {
    for (_ <- 1 to width - sb.length) { sb += '*' }
    sb
  }

Even better, you can use the existing padTo method on StringBuilder:

  scala> println(new StringBuilder("Hello, kitteh!")
           .padTo(20'*').mkString)
  Hello, kitteh!******
  scala> println(new StringBuilder("U go now.")
           .padTo(20'*').mkString)
  U go now.***********
image images/moralgraphic117px.png Watch out when passing expressions to foreach that return a function. More generally, before writing your own utility code, check if a suitable method already exists in the Scala collections library.

Footnotes for Chapter 21:

[1] Suereth, "Implicit Classes." [Sue]

[2] Harrah, "Value Classes and Universal Traits." [Har]

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.194.123