9

In Scala, why does the following occur when using the toSet functionality from TraversableOnce?

If you create a worksheet (in IntelliJ) with the following code you will get the following output (NB: using Scala 2.10.2):

val maps = List(List(1,2),List(3,4),List(5,6,7),List(8),List())

maps.flatMap( _.map( _ +  " " ) )
maps.flatMap( _.map( _ +  " " ) ).toSet
maps.flatMap( _.map( _ +  " " ) ).toSet()

i.e. res4 produces a boolean

> maps: List[List[Int]] = List(List(1, 2), List(3, 4), List(5, 6, 7), List(8), List())
> res2: List[String] = List("1 ", "2 ", "3 ", "4 ", "5 ", "6 ", "7 ", "8 ")
> res3: scala.collection.immutable.Set[String] = Set("3 ", "8 ", "4 ", "5 ", "1 ", "6 ", "2 ", "7 ")
> res4: Boolean = false

Needless to say I was a confused for long enough until I noted toSet doesn't use parentheses in the implementation, but why the boolean?

Bee
  • 93
  • 5
  • 1
    Looking at compiled bytecode, what actually happens -- resulting set's apply method got called with Unit argument, but can't say why. Looks like a bug, since it is not reproducible if I try to call `()` on res3 – om-nom-nom Nov 26 '13 at 18:39

1 Answers1

14

As you and others have already noticed, toSet doesn't provide a parameter list. Thus, calling it with parentheses, will always result in a compilation error unless the compiler finds an apply method that expects an argument as it is the case in your example:

scala> List(1).toSet()
res2: Boolean = false

scala> List(1).toSet.apply()
res3: Boolean = false

scalac has a feature called "adapting argument lists", which is visible with -Xlint:

scala> List(1).toSet()
<console>:8: warning: Adapting argument list by inserting (): this is unlikely to be what you want.
        signature: GenSetLike.apply(elem: A): Boolean
  given arguments: <none>
 after adaptation: GenSetLike((): Unit)
              List(1).toSet()
                           ^
res7: Boolean = false

scalac tries to wrap the argument into a tuple, as one can see in the sources (where an empty argument list will be treated as the Unit literal by gen.mkTuple):

      /* Try packing all arguments into a Tuple and apply `fun`
       * to that. This is the last thing which is tried (after
       * default arguments)
       */
      def tryTupleApply: Tree = (
        if (eligibleForTupleConversion(paramTypes, argslen) && !phase.erasedTypes) {
          val tupleArgs = List(atPos(tree.pos.makeTransparent)(gen.mkTuple(args)))
          // expected one argument, but got 0 or >1 ==> try applying to tuple
          // the inner "doTypedApply" does "extractUndetparams" => restore when it fails
          val savedUndetparams = context.undetparams
          silent(_.doTypedApply(tree, fun, tupleArgs, mode, pt)) map { t =>
              // Depending on user options, may warn or error here if
              // a Unit or tuple was inserted.
              val keepTree = (
                   !mode.typingExprNotFun
                || t.symbol == null
                || checkValidAdaptation(t, args)
              )
              if (keepTree) t else EmptyTree
          } orElse { _ => context.undetparams = savedUndetparams ; EmptyTree }
        }
        else EmptyTree
      )

Which btw, is a feature that is not mentioned in the spec. Adding the parentheses explicitly will let the warning go away:

scala> List(1).toSet(())
res8: Boolean = false

One remaining question now is why the code above does not produce a compilation error due to the fact that the list is of type List[Int] and the apply method of Set has type signature apply(A): Boolean, thus expects an Int in our case. The reason for this is a very well known problem and a result of the type signature of toSet which is toSet[B >: A]: Set[B]. The type signature denotes a lower bound, which means that any supertype of Int can be passed as argument.

Because in our case Unit is specified as the type of the argument, the compiler has to search for a common supertype of Unit and Int that matches the type signature of toSet. And because there is such a type, namely AnyVal, the compiler will infer that type and going forward without falling apart with an error:

scala> List(1).toSet[AnyVal](())
res9: Boolean = false
Community
  • 1
  • 1
kiritsuku
  • 52,967
  • 18
  • 114
  • 136
  • Bravo! The beast with ( horns ) is slain! – Ed Staub Nov 26 '13 at 19:30
  • 1
    That _really_ should be in the spec - consider [voting](https://issues.scala-lang.org/browse/SI-3583). – Ed Staub Nov 26 '13 at 19:50
  • 1
    Nice explanation, but no answer is complete without a reference to the puzzler. http://scalapuzzlers.com/#pzzlr-040 and related http://scalapuzzlers.com/#pzzlr-036 and http://scalapuzzlers.com/#pzzlr-032, OK, basically every puzzler. Maybe there is a meta-puzzler that covers every edge case in the language. – som-snytt Nov 28 '13 at 21:59
  • @som-snytt: btw, hearing from you here right now, I remember that you should take a look to [this question](http://stackoverflow.com/questions/19895467/why-cant-be-used-inside-of-string-interpolation) and especially to my answer... – kiritsuku Nov 28 '13 at 22:49