如何解释Scala Cats / fs2中的堆栈安全性?


13

这是fs2文档中的一段代码。该函数go是递归的。问题是我们如何知道它是否是堆栈安全的,以及如何推断任何函数是否是堆栈安全的?

import fs2._
// import fs2._

def tk[F[_],O](n: Long): Pipe[F,O,O] = {
  def go(s: Stream[F,O], n: Long): Pull[F,O,Unit] = {
    s.pull.uncons.flatMap {
      case Some((hd,tl)) =>
        hd.size match {
          case m if m <= n => Pull.output(hd) >> go(tl, n - m)
          case m => Pull.output(hd.take(n.toInt)) >> Pull.done
        }
      case None => Pull.done
    }
  }
  in => go(in,n).stream
}
// tk: [F[_], O](n: Long)fs2.Pipe[F,O,O]

Stream(1,2,3,4).through(tk(2)).toList
// res33: List[Int] = List(1, 2)

如果我们go从另一个方法调用,它也是安全的吗?

def tk[F[_],O](n: Long): Pipe[F,O,O] = {
  def go(s: Stream[F,O], n: Long): Pull[F,O,Unit] = {
    s.pull.uncons.flatMap {
      case Some((hd,tl)) =>
        hd.size match {
          case m if m <= n => otherMethod(...)
          case m => Pull.output(hd.take(n.toInt)) >> Pull.done
        }
      case None => Pull.done
    }
  }

  def otherMethod(...) = {
    Pull.output(hd) >> go(tl, n - m)
  }

  in => go(in,n).stream
}

不,不完全是。虽然如果是尾部递归的情况,请告知,但事实并非如此。据我所知,猫会做一些称为蹦床的魔术来确保烟囱的安全。不幸的是,我无法确定何时对某个函数进行了微调。
列夫·丹尼索夫

您可以重写go以使用例如Monad[F]typeclass-有一种tailRecM方法允许您显式执行蹦床,以确保该函数是安全的堆栈。我可能是错的,但是如果没有它,您将依靠F自身的堆栈安全性(例如,如果它在内部实现蹦床),但是您不知道是谁来定义您的F,因此您不应该这样做。如果不能保证F堆栈安全,请使用提供的类型类,tailRecM因为根据法律它是堆栈安全的。
Mateusz Kubuszok,

1
让编译器通过@tailrec尾部记录功能的注释来证明这一点很容易。对于其他情况,Scala AFAIK中没有正式的保证。即使函数本身是安全的,它调用的其他函数也可能不是:/。
yǝsʞǝla

Answers:


17

我之前的回答在这里提供了一些有用的背景信息。基本思想是某些效果类型具有flatMap直接支持堆栈安全递归的实现-您可以flatMap显式地嵌套调用,也可以根据需要通过递归嵌套调用,而不会嵌套过多。

对于某些效果类型flatMap,由于效果的语义,不可能是堆栈安全的。在其他情况下,可能可以编写堆栈安全的flatMap,但由于性能或其他考虑,实现者可能决定不这样做。

不幸的是,没有标准(甚至是常规的)方法来知道flatMap给定类型的堆栈是否安全。Cats确实包含一项tailRecM操作,该操作应为任何合法的monadic效果类型提供堆栈安全的monadic递归,有时查看tailRecM已知合法的实现可以提供有关a是否flatMap为堆栈安全的提示。在这种情况下,Pull它看起来像这样

def tailRecM[A, B](a: A)(f: A => Pull[F, O, Either[A, B]]) =
  f(a).flatMap {
    case Left(a)  => tailRecM(a)(f)
    case Right(b) => Pull.pure(b)
  }

tailRecM只是通过反复进行flatMap,而且我们知道PullMonad实例是合法的,这是很好的证据证明PullflatMap是堆栈安全的。在这里一个复杂的因素是,该实例Pull有一个ApplicativeError对约束FPullflatMap没有,但在这种情况下不会改变任何东西。

因此,tk此处的实现是堆栈安全的,因为flatMapon Pull是堆栈安全的,并且通过查看其tailRecM实现可以知道这一点。(如果我们挖得更深一些,我们可以弄清楚,flatMap是堆栈安全的,因为Pull本质上是一个包装FreeC,这是trampolined。)

它可能不会是太难改写tk来讲tailRecM,虽然我们不得不添加,否则不必要的ApplicativeError约束。我猜的文件的作者选择不这样做,为了清楚,因为他们知道PullflatMap是罚款。


更新:这是一个相当机械的tailRecM翻译:

import cats.ApplicativeError
import fs2._

def tk[F[_], O](n: Long)(implicit F: ApplicativeError[F, Throwable]): Pipe[F, O, O] =
  in => Pull.syncInstance[F, O].tailRecM((in, n)) {
    case (s, n) => s.pull.uncons.flatMap {
      case Some((hd, tl)) =>
        hd.size match {
          case m if m <= n => Pull.output(hd).as(Left((tl, n - m)))
          case m => Pull.output(hd.take(n.toInt)).as(Right(()))
        }
      case None => Pull.pure(Right(()))
    }
  }.stream

请注意,没有显式递归。


第二个问题的答案取决于其他方法的外观,但是在您的特定示例中,>>只会导致更多的flatMap层次,因此应该没问题。

为了更笼统地解决您的问题,整个主题都是Scala中令人困惑的混乱。您不必像我们上面所做的那样深入研究实现,只需知道类型是否支持堆栈安全的单子递归即可。围绕文档制定更好的约定将对您有所帮助,但不幸的是,我们在此方面做得不好。您始终可以始终tailRecM是“安全的”(F[_]无论如何,只要是通用的,这都是您想要做的),但是即使如此,您仍然相信Monad实现是合法的。

总结:这是一个糟糕的情况,在敏感情况下,您绝对应该编写自己的测试以验证这样的实现是安全的。


谢谢你的解释。关于当我们go从另一个方法调用时的问题,是什么使它不安全堆栈?如果我们在调用之前进行一些非递归计算, Pull.output(hd) >> go(tl, n - m)那还好吗?
列夫·丹尼索夫

是的,应该没问题(当然,假设计算本身不会溢出堆栈)。
特拉维斯·布朗

例如,哪种效果类型对单子递归不是堆栈安全的?延续类型?
鲍勃,

@bob权,但猫的ContTflatMap 实际堆栈安全(通过Defer底层类型约束)。我在想更多类似的东西List,其中递归遍历flatMap不是堆栈安全的(tailRecM尽管它确实是合法的)。
特拉维斯·布朗
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.