【问题标题】:Is there any straightforward way to evaluate a recursive function breadth-first?是否有任何直接的方法来评估递归函数广度优先?
【发布时间】:2019-02-09 01:30:39
【问题描述】:

注意以下程序:

foo :: Int -> Int -> Bool
foo n x | x == n    = True
foo n x | otherwise = foo n (x * 2) || foo n (x * 2 + 1)

main :: IO ()
main = print (foo 10 0)

它实现了一个函数foo,它在两个分支中递归地调用自身,当它沿着树递归时增加第二个参数。如果它的第二个参数等于第一个参数,它“应该”返回True,这是这种情况,因为((0 * 2 + 1) * 2 + 1) * 2 == 10。但这并没有发生,因为 Haskell 在尝试评估左分支深度优先时遇到了困难。

通常,这可以通过实现 BFS 来解决,但在 Haskell 中这样做很尴尬。我想知道是否有任何自动化的或至少不那么突兀的方式来评估递归函数广度优先?

【问题讨论】:

  • @EugeneSh。这与停机问题无关。如果 Haskell 总是首先减少最外层的 redexes,而不是最左边的最内层,则此函数将评估为 true。
  • 我不是在问这个问题,我只是问是否有任何方法可以首先评估 Haskell 函数的广度(无论是通过编译器标志还是用户友好的 DSL)。重点是指定递归函数比手动设置 BFS 更容易,因此为此提供库或功能会很方便。
  • 不,据我了解,这与评估顺序无关。如果您正在测试A || B,并且A 产生C || DBE || F,Haskell 将通过“深度优先”通过AC 和无论接下来发生什么——如果这个分支没有终止,它就永远不会评估B。而 OP 相当合理地询问是否可以编写代码以便 AB 都经过测试(顺序无关紧要),然后是 CDEF ,等等。
  • 这与|| 的评估顺序无关,它绝对是关于减少递归函数的广度优先(最外面的redexes 优先)或深度优先(最里面的redexes 优先)。请注意,如果您有一个表达式生成||s 的无限树(就像这个),并且如果true 出现在该树中的任何位置,那么如果 Haskell 首先评估最外层的 redexes,则此类表达式将始终评估为true.
  • 这也与 Collat​​z 猜想无关,因为我不是要求解决问题(即“这个程序是否停止”),我只是在问它是否是可以更改 Haskell 的评估顺序(实际上我在问是否有我可以使用的现有 DSL,因为 GHC 可能无法做到这一点)。它与回答停止问题无关,因为以我提出的方式更改 Haskell 的评估顺序显然并不能保证所有函数都会神奇地停止,尽管这个特殊(和类似)的函数会。

标签: haskell


【解决方案1】:

您可以使用unamb 包以最小的调整使原始代码工作。关键的观察是“柏拉图式”(||) 是对称的,因为它可以在任一方向短路;而 unamb 为您提供了实现这一点的方法。

foo :: Int -> Int -> Bool
foo n x | x == n    = True
foo n x | otherwise = foo n (x * 2) `por` foo n (x * 2 + 1)

有效,但留下一个僵尸以 100% 的 CPU 运行:

> foo 10 1
True

这可能是一个错误,虽然我现在不觉得超级喜欢追它...

附:如果您决定使用 unamb,我可能更喜欢 foo 的这种拼写,因为它在语法上比使用守卫更紧凑:

foo :: Int -> Int -> Bool
foo n x = x == n || por (foo n (2*x)) (foo n (2*x+1))

【讨论】:

  • Unamb 是一个可爱的概念,但它确实会留下僵尸,否则会导致性能不稳定。我一直在思考如何让它更可预测。
【解决方案2】:

我不知道如何(或者是否有可能)概括这一点,但您可以通过显式维护要检查的参数队列来模拟 BFS。

import Data.Sequence

foo :: Int -> Int -> Bool
foo n x = let foo' :: Seq Int -> Bool
              foo' Empty = False
              foo' (x' :<| xs')
                 | n' == x' = True
                 | otherwise = foo' (xs' >< fromList [2 * x', 2*x'+1])
          in foo' (singleton x)

不是立即递归,而是将递归调用的参数简单地附加到队列的末尾。助手以先到先得的顺序检查每个参数。由于n 永远不会改变,我只是简单地关闭了它的助手。更一般地,您可以在队列中存储参数元组。

foo :: Int -> Int -> Bool
foo n x = let foo' :: Seq (Int, Int) -> Bool
              foo' Empty = False
              foo' ((n', x') :<| rest) 
                | n' == x' = True
                | otherwise = foo' (rest >< fromList [(n',(2*x')),(n',(2*x'+1)))
          in foo' (singleton (n, x))

请注意,在这种情况下,队列永远不会被清空,因为(因为不能保证原始函数会终止)您实际上是在搜索无限树。如果原始递归受到保护,则只会有条件地将新参数添加到队列中,从而有可能最终被清空。

【讨论】:

  • 我认为你可以添加一个守卫| y &gt; n = False,这样它就会终止。
  • 可能,但我尽量保持一般性,以便更清楚地了解如何将其应用于其他功能。
【解决方案3】:

当然,您可以直接生成 BFS 的“级别”。合并并行递归调用的级别。所以:

import Data.List

foo :: Int -> Int -> [[Bool]]
foo n x = id
    . ([x == n] :)
    . map concat
    . transpose
    . map (foo n)
    $ [2*x, 2*x+1]

外部列表表示搜索深度——深度 0 在第 0 个元素中,深度 1 在第 1 个元素中,依此类推——而内部列表包含我们在该级别探索的参数的函数调用结果.

总结成一个Bool 只涉及遍历两个列表。

> any or (foo 10 1)
True
> any or (foo 1 10)
-- this might take a while

作为森林砍伐优化,您可以改为只返回每个深度的 or,因此:

foo :: Int -> Int -> [Bool]
foo n x = (x == n) : zipWith (||) (foo n (2*x)) (foo n (2*x+1))

只剩下深度列表;之前的内部列表是预先折叠的。仍然可以正常工作:

> or (foo 10 1)
True
> or (foo 1 10)
-- uh oh...

【讨论】:

  • 我打算建议调整函数以返回所有已测试 ns 的列表,交错流(使用 Omega 或手动 - 我想这就是你的 transpose 正在做的事情) ,然后只取合并结果流中的第一个 True 。或者代替布尔值,制作[x | n==x]s、concat it 和take 1 的流。
  • @WillNess 好主意! (+++) 可以帮忙。
  • 感谢您的链接。啊,这是你的包裹,里面有好东西! ...顺便说一句diagonal 并不是真的“不公平”,它比concat 公平得多! :) 所以称之为“稍微不公平”,也许? (切线,我在 CS.SE 上为我的 one chance answer 写了一些非常相似的东西,我确实称对角线等价物“公平”......好吧,它正在终止!)
【解决方案4】:

foo :: Int -> Int -> Bool
foo n x | x == n    = True
foo n x | otherwise = foo n (x * 2) || foo n (x * 2 + 1)

去..

foo :: Int -> Int -> Bool
foo n x =  x == n  || foo n (x * 2) || foo n (x * 2 + 1)

去...

foo :: Int -> Int -> [Bool]
foo n x = [x == n] ++ foo n (x * 2) ++ foo n (x * 2 + 1)

去....

foo :: Int -> Int -> [Bool]
foo n x = [x == n] ++ concat [foo n (x * 2) , foo n (x * 2 + 1)]

走了

foo :: Int -> Int -> [Bool]
foo n x = [x == n] ++ concat (transpose [foo n (x * 2) , foo n (x * 2 + 1)])

takeWhile not $ foo 10 0 在 20 步后愉快地终止。易于根据需要进行扩充。

transpose 这里模拟了 Omega 包的对角化,实现了结果的两个子流的交错。

如果寻找更接近的句法相似性,定义

xs ||/ ys = concat (transpose [xs, ys])

foo n x = [n==x] ||/ foo n (2*x) ||/ foo n (2*x+1)

takeWhile not $ foo 10 0 在 25 步后停止。

这种方法遵循解释的原则。正如命令式编程的隐式状态变化在函数式编程中被显式化一样,求值的步骤也可以用一个列表来解释,可以说将折叠变成扫描(或展开)。


注意:(||/) 与 Daniel Wagner 的包 universe-base 中的 (+++) 相同,该包具有各种枚举工具。可以看到对角化的效用,例如在 CS.SE 上我的 this answer

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-02-18
    • 1970-01-01
    • 1970-01-01
    • 2020-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-21
    相关资源
    最近更新 更多