【问题标题】:Composition of TailRec and State monadTailRec 和 State monad 的组成
【发布时间】:2017-12-21 06:52:49
【问题描述】:

为了简化我的问题,我将从一个学术示例开始,即阿克曼函数。

我使用以下递归朴素实现:

def a(m: BigInt, n: BigInt): BigInt = {
  if (m == 0) {
    n + 1
  } else if (m > 0 && n == 0) a(m - 1, 1)
  else a(m - 1, a(m, n - 1))
}

这不是最优的,很快就会在堆栈溢出中结束。 所以我构建了一个新的实现,它使用了标准 scala 库中的 TailRec,它给出了:

import scala.util.control.TailCalls._

private[this] def a_impl(m: BigInt, n: BigInt): TailRec[BigInt] = {
  if (m == 0) {
    done(n + 1)
  } else if (m > 0 && n == 0) tailcall(a_impl(m - 1, 1))
  else
    for {
      x <- tailcall(a_impl(m, n - 1))
      y <- tailcall(a_impl(m - 1, x))
    } yield y

}

def a(m: BigInt, n: BigInt): BigInt = {
  a_impl(m, n).result
}

它可以工作,但速度很慢。 所以我构建了一个使用 State monad 的新实现,但我再次失去了终端递归。

type Memo = Map[(BigInt, BigInt), BigInt]

private[this] def a_impl(m: BigInt, n: BigInt): State[Memo, BigInt] = {
  if (m == 0) {
    State.init(n + 1)
  } else {
    for {
      memoed <- State.gets { memo: Memo => memo get (m, n) }
      res <- memoed match {
        case Some(ack) => State.init[Memo, BigInt](ack)
        case None =>
          if (m > 0 && n == 0) for {
            a <- a_impl(m - 1, 1)
            _ <- State.update { memo: Memo => memo + ((m, n) -> a) }
          } yield a
          else for {
            a <- a_impl(m, n - 1)
            b <- a_impl(m - 1, a)
            _ <- State.update { memo: Memo => memo + ((m, n) -> b) }
          } yield b
      }
    } yield res
  }
}

def a(m: BigInt, n: BigInt): BigInt = {
  a_impl(m, n) eval (Map())
}

所以我的问题是,我如何同时使用 State 和 TailRec?

我已经看到 Monad Transformer 的概念,但我真的不知道如何在我的示例中使用它。 我什至不知道该使用哪种类型,我可以在那个和这个之间进行选择:

type TailRecWithState = TailRec[State[Memo, BigInt]] 
// or  
type StateWithTailRec = State[Memo, TailRec[BigInt]]

您能否帮助我并在此示例中指出正确的方向(然后我将处理我的实际案例)?

【问题讨论】:

  • 当你说“真的很慢”时,你是什么意思?你得到什么数字,你期望什么?
  • 真的很慢:在我的笔记本电脑上计算 a(4, 2) 需要 8 分钟
  • 有一点需要注意:你的记忆并没有像你想象的那样做。由于状态是不可变的,因此计算结果不会在计算分支之间共享。
  • 我的 State 实现既高效又快速。 a(3,9) 的计算时间为 8 毫秒,而不是 TailRec 的 2 秒。但是由于堆栈溢出异常,状态一无法计算 a(4, 2),它不是尾递归的。但这不是这里最重要的一点。我的问题是如何同时使用 State 和 TailRec
  • @BobDalgleish,我认为你对记忆的看法是不对的。由于此代码表示严格的顺序计算,因此来自上一个分支的状态被下一个分支“继承”,并且记忆工作正常。您可以通过将一些日志记录添加到 case None =&gt; 分支来验证这一点,并查看它永远不会为相同的 (m, n) 对调用两次

标签: scala monads


【解决方案1】:

我知道至少在猫中,State[S, A]StateT[Eval, S, A] 的类型别名,其中EvalTailRec 完全类似——堆栈安全延迟执行。这对我来说很好用:

import cats._, cats.data._, cats.implicits._

type Memo = Map[(BigInt, BigInt), BigInt]

private[this] def a_impl(m: BigInt, n: BigInt): State[Memo, BigInt] = {
  if (m == 0) {
    State.pure(n + 1)
  } else {
    for {
      memoed <- State.inspect[Memo, Option[BigInt]](s => s.get((m, n)))
      res <- memoed match {
        case Some(x) => State.pure[Memo, BigInt](x)
        case None => {
          if (n == 0) for {
            a <- a_impl(m - 1, 1)
            _ <- State.modify[Memo](s => s + ((m, n) -> a))
          } yield a
          else for {
            a <- a_impl(m, n - 1)
            b <- a_impl(m - 1, a)
            _ <- State.modify[Memo](s => s + ((m, n) -> b))
          } yield b
        }
      }
    } yield res
  }
}

def a(m: BigInt, n: BigInt): BigInt = {
  a_impl(m, n).runA(Map()).value
}

我的猜测是 scalaz 可能也有一些类似的 StateTEval,尽管我不熟悉这个库。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-12-01
    • 2011-05-07
    • 1970-01-01
    • 2014-09-11
    • 2019-05-19
    • 1970-01-01
    • 2019-10-29
    • 1970-01-01
    相关资源
    最近更新 更多