0
votes

I'm using Scala 2.13.1 and evaluate my examples in a worksheet.

At first, I define two functions that return the range of a to (z-1) as a stream or respectively a lazy list.

def streamRange(a: Int, z: Int): Stream[Int] = {
  print(a + " ")
  if (a >= z) Stream.empty else a #:: streamRange(a + 1, z)
}

def lazyListRange(a: Int, z: Int): LazyList[Int] = {
  print(a + " ")
  if (a >= z) LazyList.empty else a #:: lazyListRange(a + 1, z)
}

Then I call both functions, take a Stream/LazyList of 3 elements and convert them to List:

streamRange(1, 10).take(3).toList    // prints 1 2 3
lazyListRange(1, 10).take(3).toList  // prints 1 2 3 4

Here I do the same again:

val stream1 = streamRange(1, 10)     // prints 1
val stream2 = stream1.take(3)
stream2.toList                       // prints 2 3

val lazyList1 = lazyListRange(1,10)  // prints 1
val lazyList2 = lazyList1.take(3)
lazyList2.toList                     // prints 2 3 4

The 1 is printed because the function is visited and the print statement is at the start. No surprise.

But I don't understand why the additional 4 is printed for the lazy list and not for the stream.

My assumption is that at the point where 3 is to be concatenated with the next function call, the LazyList version visits the function, whereas in the Stream version the function is not visited. Otherwise the 4 would not have been printed.

It seems like unintended behaviour, at least it is unexpected. But would this difference in side effects be considered a bug or just a detailed difference in the evaluation of Stream and LazyList.

1
LazyList evaluates both its head an tail, so computing the third element evaluated that 4 tail. In any case, neither of those types should be mixed with side effects. Consider using fs2, zstream, monix or akka streams.Luis Miguel Mejía Suárez
Thank you for pointing out the difference in evaluation and also for the comment on side effects. My thoughts now about the example: The tail you mention is an expression and has a value. This expression is in this case strictly evaluated and during this evaluation side effects like the print call can occur. Now, what about the value of tail, the result of evaluation of tail. We arrive at (a #:: nextTail). And #:: returns a LazyList where evaluation of both head and tail is deferred. This is the value of tail, a LazyList the contents of which have not been computed yet.colorblind
And if the head of this LazyList is accessed, then again both head and tail will be evaluated. Different from the Stream, where the head can be accessed without evaluating the tail.colorblind

1 Answers

4
votes

Stream implements #:: using Deferer:

  implicit def toDeferrer[A](l: => Stream[A]): Deferrer[A] = new Deferrer[A](() => l)

  final class Deferrer[A] private[Stream] (private val l: () => Stream[A]) extends AnyVal {
    /** Construct a Stream consisting of a given first element followed by elements
      *  from another Stream.
      */
    def #:: [B >: A](elem: B): Stream[B] = new Cons(elem, l())
    /** Construct a Stream consisting of the concatenation of the given Stream and
      *  another Stream.
      */
    def #:::[B >: A](prefix: Stream[B]): Stream[B] = prefix lazyAppendedAll l()
  }

where Cons:

final class Cons[A](override val head: A, tl: => Stream[A]) extends Stream[A] {

Whereas LazyList implements #:: with its own Deferer:

  implicit def toDeferrer[A](l: => LazyList[A]): Deferrer[A] = new Deferrer[A](() => l)

  final class Deferrer[A] private[LazyList] (private val l: () => LazyList[A]) extends AnyVal {
    /** Construct a LazyList consisting of a given first element followed by elements
      *  from another LazyList.
      */
    def #:: [B >: A](elem: => B): LazyList[B] = newLL(sCons(elem, l()))
    /** Construct a LazyList consisting of the concatenation of the given LazyList and
      *  another LazyList.
      */
    def #:::[B >: A](prefix: LazyList[B]): LazyList[B] = prefix lazyAppendedAll l()
  }

where sCons:

@inline private def sCons[A](hd: A, tl: LazyList[A]): State[A] = new State.Cons[A](hd, tl)

and Cons:

final class Cons[A](val head: A, val tail: LazyList[A]) extends State[A]

It means that on the very definition level:

  • Steam lazily evaluates it tail's creation
  • LazyList lazily evaluates its tail's content

Difference is noticeable among other in side-effects... which neither of these if made for.

If you want to handle potentially infinite sequences of impore computations, use a proper streaming library: Akka Streams, FS2, ZIO Streams. Build-in streams/lazy list are made for pure computations and if you step into impure directory you should assume that no guarantees regarding side effects are provided.