Является ли это ошибкой библиотеки на функциональном языке, когда функция с одним и тем же именем, но для разных коллекций вызывает разные побочные эффекты? - PullRequest
0 голосов
/ 16 апреля 2020

Я использую Scala 2.13.1 и оцениваю свои примеры на листе.

Сначала я определяю две функции, которые возвращают диапазон от a до (* 1005). * z -1) в виде потока или, соответственно, ленивого списка.

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)
}

Затем я вызываю обе функции, беру Stream / LazyList из 3 элементов и преобразовываю их в список:

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

Здесь я снова делаю то же самое:

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

1 печатается, потому что функция посещена, и оператор print находится в начале. Не удивительно.

Но я не понимаю, почему дополнительные 4 печатаются для ленивого списка, а не для потока.

Я предполагаю, что в точке, где 3 объединяется при следующем вызове функции версия LazyList посещает функцию, тогда как в версии Stream функция не посещается. В противном случае 4 не был бы напечатан.

Это кажется непреднамеренным поведением, по крайней мере, неожиданным. Но можно ли считать эту разницу в побочных эффектах ошибкой или просто детальной разницей в оценке Stream и LazyList.

1 Ответ

4 голосов
/ 16 апреля 2020

Stream реализует #:: с использованием 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()
  }

, где Cons:

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

Принимая во внимание LazyList реализует #:: со своими 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()
  }

, где sCons:

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

и Cons:

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

Это означает, что на самом уровне определения:

  • Steam лениво оценивает свой хвост создание
  • LazyList лениво оценивает свой хвост content

Разница заметна среди других побочных эффектов ... которые ни для одного из них, если они предназначены.

Если вы хотите обрабатывать потенциально бесконечные последовательности вычислений impore , используйте подходящую потоковую библиотеку: Akka Streams, FS2, ZIO Streams. Встроенный список streams / lazy создан для чистых вычислений, и если вы зайдете в нечистый каталог, вы должны предположить, что никаких гарантий относительно побочных эффектов не предоставлено.

...