首页 > 解决方案 > 当具有相同名称但用于不同集合的函数产生不同的副作用时,它是否是函数语言中的库错误?

问题描述

我正在使用 Scala 2.13.1 并在工作表中评估我的示例。

首先,我定义了两个函数,它们将a到 ( 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)
}

然后我调用这两个函数,获取 3 个元素的 Stream/LazyList 并将它们转换为 List:

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 是因为访问了函数并且打印语句在开头。没有惊喜。

但我不明白为什么额外的 4 是为惰性列表而不是流打印的。

我的假设是,在将 3 与下一个函数调用连接时,LazyList 版本会访问该函数,而在 Stream 版本中,该函数不会被访问。否则 4 将不会被打印。

这似乎是无意的行为,至少是出乎意料的。但是这种副作用的差异是否会被认为是一个错误,或者只是 Stream 和 LazyList 评估中的一个详细差异。

标签: scalafunctional-programmingevaluationscala-streams

解决方案


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懒惰地评估它尾巴的内容

其他副作用之间的差异是显而易见的......如果这些都不是。

如果您想处理潜在的无限序列计算,请使用适当的流媒体库:Akka Streams、FS2、ZIO Streams。内置流/惰性列表是为纯计算而制作的,如果您进入不纯目录,您应该假设不提供有关副作用的保证。


推荐阅读