Haskell自我引用列表终止

时间:2017-09-27 10:13:44

标签: list haskell recursion

编辑: 请参阅 this followup question ,以简化我在此处尝试识别的问题,并要求输入关于GHC修改提案。

所以我试图编写一个通用的广度优先搜索功能,并提出以下内容:

bfs :: (a -> Bool) -> (a -> [a]) -> [a] -> Maybe a
bfs predf expandf xs = find predf bfsList
    where bfsList = xs ++ concatMap expandf bfsList

我觉得它非常优雅,但是在不存在的情况下它会永远阻止。

在所有条款扩展到[]之后,concatMap将永远不会返回另一个项目,因此concatMap阻止等待其他项目? Haskell可以变得足够智能,以实现列表生成被阻止读取自引用并终止列表吗?

我能够提出的最佳替代品并不那么优雅,因为我必须自己处理终止案例:

    where bfsList = concat.takeWhile (not.null) $ iterate (concatMap expandf) xs

对于具体示例,第一次搜索成功终止,第二次搜索阻止:

bfs (==3) (\x -> if x<1 then [] else [x/2, x/5]) [5, 3*2**8]
bfs (==3) (\x -> if x<1 then [] else [x/2, x/5]) [5, 2**8]

3 个答案:

答案 0 :(得分:4)

我们分步生成结果列表(队列)。在每一步中,我们都会消耗上一步产生的内容。当最后一个扩展步骤没有添加任何内容时,我们停止:

bfs :: (a -> Bool) -> (a -> [a]) -> [a] -> Maybe a
bfs predf expandf xs = find predf queue
    where 
    queue = xs ++ gen (length xs) queue                 -- start the queue with `xs`, and
    gen 0 _ = []                                        -- when nothing in queue, stop;
    gen n q = let next = concatMap expandf (take n q)   -- take n elemts from queue,
              in  next ++                               -- process, enqueue the results,
                         gen (length next) (drop n q)   -- advance by `n` and continue

因此我们得到了

~> bfs (==3) (\x -> if x<1 then [] else [x/2, x/5]) [5, 3*2**8]
Just 3.0

~> bfs (==3) (\x -> if x<1 then [] else [x/2, x/5]) [5, 2**8]
Nothing

此解决方案中一个潜在的严重流程是,如果任何expandf步骤产生无限的结果列表,那么它将无法完全计算其length

通常,只需引入一个计数器,然后按每个扩展步骤(length . concatMap expandf或其他)生成的解决方案的长度递增计数器,减去所消耗的数量。当它达到0时,不要再尝试消耗任何东西,因为此时没有任何消耗,你应该终止。

此计数器实际上用作指向正在构造的队列的指针。值 n 表示下一个结果的放置位置是 n ,位于列表中的位置之前输入。 1因此意味着下一个结果直接放在输入值之后。

Wikipedia's article中可以找到以下代码关于corecursion(搜索&#34; corecursive queue&#34;):

data Tree a b = Leaf a  |  Branch b (Tree a b) (Tree a b)

bftrav :: Tree a b -> [Tree a b]
bftrav tree = queue
  where
    queue = tree : gen 1 queue                -- have one value in queue from the start

    gen  0   _                 =         []           
    gen len (Leaf   _     : s) =         gen (len-1) s   -- consumed one, produced none
    gen len (Branch _ l r : s) = l : r : gen (len+1) s   -- consumed one, produced two

这种技术在Prolog中是很自然的,具有自上而下的列表实例化和逻辑变量,这些变量可以明确地处于尚未设置的状态。另请参阅

gen中的{p> bfs可以重写为更多增量,这通常是一件好事:

    gen 0  _     = []
    gen n (y:ys) = let next = expandf y
                   in  next ++ gen (n - 1 + length next) ys

答案 1 :(得分:4)

已编辑添加注释以解释下面的bfs'解决方案。

您的问题的表达方式(“Haskell可以变得足够聪明”),听起来您认为计算的正确值如下:

bfs (\x -> False) (\x -> []) []

鉴于bfs的原始定义应为Nothing,而Haskell无法找到正确的答案。

但是,上述计算的正确值是最低的。替换bfs的定义(并简化[] ++表达式),上述计算等于:

find (\x -> False) bfsList
   where bfsList = concatMap (\x -> []) bfsList

评估find需要确定bfsList是否为空,因此必须将其强制为弱头正常形式。此强制需要评估concatMap表达式,必须确定bfsList是否为空,强制它为WHNF。此强制循环意味着bfsList位于底部,因此find也是如此。

Haskell 可以更聪明地检测循环并给出错误,但返回[]是不正确的。

最终,这与以下情况相同:

foo = case foo of [] -> []

也无限循环。 Haskell的语义暗示这个case构造必须强制foo,并且强制foo需要强制foo,因此结果是最低的。确实,如果我们将这个定义视为一个等式,那么替换foo = []将“满足”它,但这不是Haskell语义的工作原理,原因同样如下:

bar = bar

没有值1"awesome",即使这些值将其视为“等式”。

所以,你的问题的答案是,不,这个行为无法改变,以便在不从根本上改变Haskell语义的情况下返回一个空列表。

此外,作为一种看起来很漂亮的替代方案 - 即使有明确的终止条件 - 也许可以考虑:

bfs' :: (a -> Bool) -> (a -> [a]) -> [a] -> Maybe a
bfs' predf expandf = look
  where look [] = Nothing
        look xs = find predf xs <|> look (concatMap expandf xs)

这使用了Alternative的{​​{1}}实例,这非常简单:

Maybe

所以Just x <|> ... -- yields `Just x` Nothing <|> Just y -- yields `Just y` Nothing <|> Nothing -- yields `Nothing` (doesn't happen above) 会使用look检查当前值xs,如果失败并返回find,则会递归查看其扩展。

作为一个使终止条件看起来不那么明显的愚蠢的例子,这里是使用Nothing作为终结符的双monad(可能在隐式Reader中)版本! (不推荐在实际代码中使用。)

listToMaybe

这是如何工作的?嗯,这是个玩笑。作为提示,bfs'' :: (a -> Bool) -> (a -> [a]) -> [a] -> Maybe a bfs'' predf expandf = look where look = listToMaybe *>* find predf *|* (look . concatMap expandf) (*>*) = liftM2 (>>) (*|*) = liftM2 (<|>) infixl 1 *>* infixl 3 *|* 的定义与:

相同
look

答案 2 :(得分:1)

bfsList是递归定义的,这本身并不是Haskell中的问题。然而,它确实产生了一个无限的列表,再次,它本身并不是一个问题,因为Haskell被懒惰地评估。

只要find最终找到了它所寻找的东西,那么仍然存在无限元素的问题,因为在那时评估停止(或者更确切地说,继续做其他事情)。 / p>

AFAICT,第二种情况下的问题是谓词永远不会匹配,因此bfsList只会继续生成新元素,而find会继续查看。

  

在所有术语扩展到[]后,concatMap将永远不会返回另一个项目

您确定这是正确的诊断吗?据我所知,使用上面提供的lambda表达式,每个输入元素总是扩展为两个新元素 - 从不到[]。但是,该列表是无限的,因此如果谓词不匹配,函数将永远评估。

  

Haskell可以变得足够智能,以实现列表生成被阻止读取自引用并终止列表吗?

如果有一个通用算法来确定计算是否最终完成,那就太好了。唉,正如图灵和教会(彼此独立)在1936年证明的那样,这样的算法不可能存在。这也称为暂停问题。我不是数学家,所以我可能错了,但我认为它也适用于此......

<击>

<击>
  

我能想出的最佳替代品并不是那么优雅

不确定那个...如果我尝试使用它而不是bfsList的其他定义,它就不会编译......但是,我不是认为问题是空列表。