是否有任何直接的方法可以广度优先地评估递归函数?

时间:2019-02-07 19:50:31

标签: haskell

注意以下程序:

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中这样做很尴尬。我想知道是否有任何一种自动化的方法,或者至少是一种不那么引人注目的方式来评估广度优先的递归函数?

4 个答案:

答案 0 :(得分:5)

您可以使用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

这可能是一个错误,尽管我现在不觉得超级追逐...

P.S。如果您决定使用unamb,我可能更喜欢foo的这种拼写,只是因为它在语法上比使用卫兵更紧凑:

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

答案 1 :(得分:3)

我不知道如何(或什至有可能)对此进行概括,但是您可以通过显式维护要检查的参数队列来模拟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))

请注意,在这种情况下,队列永远不会被清空,因为(由于不能保证终止原始功能)您正在有效地搜索无限树。如果原始的递归得到了保护,则仅在有条件的情况下将新的参数添加到队列中,从而有可能最终将其清空。

答案 2 :(得分: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...

答案 3 :(得分:2)

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个步骤后停止。

这种方法遵循 expression 的原理。就像命令式编程的隐式状态更改在函数式编程中是明确的一样,评估步骤也可以在列表中加以说明,可以说是将折叠折叠成扫描(或展开)。


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