Puzzler 36

Size It Up

One of the goals of the Scala collections is to make common operations, such as concatenating two collections, concise and easy to read. To that end, the collection types support a number of operators, such as ++ and +:, allowing you to write expressions "naturally" and avoid method names such as concat or prepend.

The following code example uses one of these operations to determine how many items are in a young Scala enthusiast's lunchbox. In order to ensure an at least moderately healthy diet, we sneak an apple into the lunchbox as well.

Our Scala enthusiast has the peculiar habit of never taking more than one of each item, so the code starts out by assuming that lunchboxes are sets. Realizing that this is something of a special case, we update the code to allow arbitrary selections of items and count the number of items in the "apple-reinforced" lunchbox, using the original function that assumes lunchboxes are sets and the updated version that allows for arbitrary collections. What is the result of executing the following code?

  import collection.mutable
  
def howManyItems(lunchbox: mutable.Set[String],    itemToAdd: String): Int = (lunchbox + itemToAdd).size
def howManyItemsRefac(lunchbox: mutable.Iterable[String],    itemToAdd: String): Int = (lunchbox + itemToAdd).size
val lunchbox =   mutable.Set("chocolate bar""orange juice""sandwich")
println(howManyItems(lunchbox, "apple")) println(howManyItemsRefac(lunchbox, "apple")) println(lunchbox.size)

Possibilities

  1. Prints:
      4
      5
      5
    
  2. Prints:
      4
      4
      3
    
  3. Prints:
      4
      4
      4
    
  4. Prints:
      4
      47
      3
    

Explanation

You may wonder whether treating the lunchbox as an Iterable, as in the case of howManyItemsRefac, somehow allows the addition of a second apple. Or you may suspect that adding an apple creates a new lunchbox, rather than modifying the lunchbox passed to the counting functions, which would result in 4, 4, and 3 being printed.

As it happens, that is close, but not quite the case. The correct answer is number 4:

  scala> println(howManyItems(lunchbox, "apple"))
  4
  
scala> println(howManyItemsRefac(lunchbox, "apple")) 47
scala> println(lunchbox.size) 3

What is happening here? The first point to observe is that the suspicion that the lunchbox passed to the counting function is not actually modified is correct. The + method on both mutable and immutable Sets creates a new set, consisting of the elements of the original set together with the argument passed. The method on a mutable set that actually modifies the set is +=. So howManyItems returns the size of a new lunchbox with an additional apple, but does not actually add the apple to the lunchbox passed in. This explains why the first println statement outputs 4 but the final one prints 3.

Where do the 47 lunchbox items found by howManyItemsRefac come from, then? Well, it so happens that + is not actually a method supported by Iterable, but it is Scala's string concatenation operator. And itemToAdd is indeed a string.

Of course, lunchbox is not a string, so the Scala compiler starts looking for an applicable implicit conversion[1] to help. And it finds one, in the form of Predef.any2stringadd.[2] As its name applies, this default implicit can convert any object to a String so it can be concatenated with another string.

The body of the function howManyItemsRefac is, essentially:

  Predef.any2stringadd(lunchbox).+(itemToAdd).size

This means that howManyItemsRefac does not return the number of items in the lunchbox after adding an apple, but rather the length of the string lunchbox.toString + "apple":

  def howManyItemsDebug(lunchbox: mutable.Iterable[String], 
      itemToAdd: String): Int = {
    val concatenatedStrings = lunchbox.toString + itemToAdd
    println(s"DEBUG: ${concatenatedStrings}")
    concatenatedStrings.size
  }
  
scala> println(howManyItemsDebug(lunchbox, "apple")) DEBUG: Set(orange juice, sandwich, chocolate bar)apple 47

Discussion

The main reason any2stringadd exists is for consistency with Java. Recall that Java also supports concatenation of any object with a string. This is convenient for quick debugging statements:

  case class Pet(name: String, species: String)
  println(Pet("garfield""cat") + " is my pet")

Due to any2stringadd's penchant for causing confusion by appearing in unexpected places in your code, there are frequent calls for it to be removed. This is likely to happen in future, although not necessarily in the short term.

Luckily, you can disable this or indeed any other default implicit in your programs, by using an import selector[3] to "rename" the implicits in question to the wildcard symbol _. This makes them effectively inaccessible. These import statements must be the first lines in your source file.

Disabling implicits in the REPL is possible using the :paste -raw command, which treats the subsequent input as a Scala file, rather than a script:

  scala> :paste -raw
  
// Entering paste mode (ctrl-D to finish)
  import Predef.{any2stringadd => _, _}
  object SizeItUp {
    import collection.mutable
    def howManyItemsRefac(lunchbox: mutable.Iterable[String],
      itemToAdd: String): Int = (lunchbox + itemToAdd).size
  }
  
// Exiting paste mode, now interpreting.
  <pastie>:5: error: value + is not a member of 
    scala.collection.mutable.Iterable[String]
      itemToAdd: String): Int = (lunchbox + itemToAdd).size
                                          ^

Here, the second wildcard (_) in import Predef.{any2stringadd => _, _} ensures that all other members of Predef are still imported.

An easier, if less elegant, way of disabling an implicit conversion is to deliberately cause an "ambiguous implicit values" clash. In order for this to work, you need to introduce not one, but two new conversions. If you define only one implicit, it will be more specific than the conversion in Predef you want to disable and will be chosen instead, rather than producing the intended conflict:

  object NoAny2StringAdd {
    implicit val disableAny2stringadd1 = (_: Any) => ""
    implicit val disableAny2stringadd2 = (_: Any) => ""
  }
  import collection.mutable
  import NoAny2StringAdd._
  
scala> def howManyItemsRefac(            lunchbox: mutable.Iterable[String],            itemToAdd: String): Int =          (lunchbox + itemToAdd).size <console>:13: error: type mismatch;  found   : lunchbox.type (with underlying type     scala.collection.mutable.Iterable[String])  required: ?{def +(x$1: ? >: String): ?} Note that implicit conversions are not applicable    because they are ambiguous:  both value disableAny2stringadd1 in object NoAny2StringAdd    of type => Any => String  and value disableAny2stringadd2 in object NoAny2StringAdd    of type => Any => String  are possible conversion functions from lunchbox.type to     ?{def +(x$1: ? >: String): ?}          (lunchbox + itemToAdd).size           ^

A more straightforward way of preventing unintended string concatenations is to explicitly specify the expected type of collection operations:

  scala> def howManyItemsRefac(
             lunchbox: mutable.Iterable[String],
             itemToAdd: String): Int = {
           val healthierLunchbox: mutable.Iterable[String] = 
             lunchbox + itemToAdd
           healthierLunchbox.size
         }
  <console>:10: error: type mismatch;
   found   : String
   required: scala.collection.mutable.Iterable[String]
             lunchbox + itemToAdd
                      ^

Using an intermediate val in this way should not incur any performance penalty: the compiler will generally be able to optimize away the intermediate value.

image images/moralgraphic117px.png Be aware that the Scala compiler can always fall back to treating the + operator as string concatenation if the argument is a string. If the expression is not intended to return a String, specify the expected result type explicitly. You can disable Predef.any2stringadd in your program to prevent the implicit conversion of any object to a string.

Footnotes for Chapter 36:

[1] Odersky, The Scala Language Specification, Section 6.26. [Ode14]

[2] See Puzzler 30 for a related discussion of implicit resolution.

[3] Odersky, The Scala Language Specification, Section 4.7. [Ode14]

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

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