scalapuzzlers / scalapuzzlers.github.com

Github Pages behind scalapuzzlers.com
www.scalapuzzlers.com
161 stars 53 forks source link

Puzzler candidate about value definitions in for expressions #132

Closed demobox closed 7 years ago

demobox commented 9 years ago

Based on a suggestion by @marconilanna

demobox commented 9 years ago

@marconilanna: What do you think? I've added some open questions to the draft as review comments...

demobox commented 9 years ago

@nermin: Ping..?

marconilanna commented 9 years ago

Hmmm... maybe something got lost in translation, but the spirit of the puzzle I initially discussed was that some types have lazy semantics while others are eager:

def f(x: TraversableOnce[Int]) = for {
  i <- x
  _ = println("ping")
} println(i)

def g(x: Range) = for {
  i <- x
  _ = println("pong")
} println(i)

f(1 to 2)
// ping
// 1
// ping
// 2

g(1 to 2)
// pong
// pong
// 1
// 2

Not sure which puzzler is the most interesting one, but the overall lesson here is that side effects are evil and make reasoning about code a lot harder, specially in the presence of laziness.

demobox commented 9 years ago

I initially discussed was that some types have lazy flatMap semantics while others are eager

Do you mean map or flatMap here? The value def becomes a call to map, from what I recall:

        def f(x: TraversableOnce[Int]) = x.map(((i) => {
  val x$1 = println("ping");
  scala.Tuple2(i, x$1)
})).foreach(((x$2) => x$2: @scala.unchecked match {
          case scala.Tuple2((i @ _), _) => println(i)
        }))
      }

And yes, we have an interesting choice here between what we consider "more puzzling": the fact that value defs result in a map that is executed before the next generator (as opposed to adding a statement to a generated, which is called in a nested fashion), or the fact that value definitions combined with lazy vs. non-lazy collections can result in unexpected behaviour.

For me, the "value def + lazy/non-lazy" option requires more complexity to "be puzzling", but I'd be curious to hear what @nermin has to say. I think it also depends on how "natural" it is to put side-effecting expressions in a value def vs. including it in a generator.

I can also live with this quite happily:

def sumsTo1Traversable(numbers: TraversableOnce[Int]) = 
  for {
      x <- numbers
      _ = println("DEBUG 1: x: " + x)
      y <- x to 1
  } println(x + y)

def sumsTo1Iterable(numbers: Iterable[Int]) = 
  for {
      x <- numbers
      _ = println("DEBUG 1: x: " + x)
      y <- x to 1
  } println(x + y)

scala> sumsTo1Traversable(1 to 2)
DEBUG 1: x: 1
2
DEBUG 1: x: 2

scala> sumsTo1Iterable(1 to 2)
DEBUG 1: x: 1
DEBUG 1: x: 2
2

There are plenty of possible variants here - using Range, having Iterable as the signature but calling numbers.iterator in one of the generators etc.

The one thing to note is that the lazy behaviour of TraversableOnce.map seems to be implementation-dependent rather than required by the documentation.

marconilanna commented 9 years ago

The one thing to note is that the lazy behaviour of TraversableOnce.map seems to be implementation-dependent rather than required by the documentation.

Which makes it even more "puzzlier"

demobox commented 9 years ago

@nermin: Ping..?

Discussed this with Nermin today, who likes both versions. Perhaps we can simply turn this into two puzzlers? Or combine them by having three variants of the loop?

demobox commented 7 years ago

Merged to master: ae382d3. See http://scalapuzzlers.com/#pzzlr-068