简体   繁体   中英

Is it a library bug in a functional language when a function with the same name but for different collections produces different side effects?

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM