Haskell自称列表终止



edit: 请参阅这个后续问题简化了我试图在此处识别的问题,并要求对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]

编辑添加注释以解释我的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)

这使用MaybeAlternative实例,这确实很简单:

Just x  <|> ...     -- yields `Just x`
Nothing <|> Just y  -- yields `Just y`
Nothing <|> Nothing -- yields `Nothing` (doesn't happen above)

因此,look使用find检查当前值xs,如果失败并返回Nothing,它会递归地查看其扩展。

作为使终止条件看起来不太明确的愚蠢示例,这是使用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的定义与:

相同
  where look xs = listToMaybe xs >> 
                  (find predf xs <|> look (concatMap expandf xs))

我们以步骤生成结果列表(队列)。在每个步骤中,我们消耗了上一步上生产的内容。当最后一个扩展步骤什么也没添加时,我们停止:

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 notches,距离获取输入的列表中的位置。因此,1意味着下一个结果直接放置在输入值之后。

可以在Wikipedia的有关Corecursion的文章中找到以下代码(搜索" Corecursive deqeue"):

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

这种技术在序言中是自然的,具有自上而下的列表实例化和逻辑变量,可以在 dot> dot-net-set状态中明确。另请参见tailRecursion-modulo-cons。


bfs中的 gen可以重新编写为更加增量,这通常是一件好事:

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

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

只要find最终找到了它想要的东西,这并不是一个无限元素的问题,因为当时评估停止(或者,更重要的是要去做其他事情)。

>

afaict,第二种情况下的问题是谓词永远不匹配,因此bfsList只是继续生产新元素,而find不断寻找。

在所有条款都扩展到[] concatmap之后,永远不会返回另一个项目

您确定这是正确的诊断吗?据我所知,在上面提供的lambda表达式的情况下,每个输入元素总是扩展到两个新元素 - 永远不要 []。但是,该列表是无限的,因此,如果谓词无与伦比,则该功能将永远评估。

可以使Haskell变得足够聪明,以意识到列表的一代已被阻止阅读自我参考并终止列表?

如果有通用算法来确定计算是否最终将完成,那将是不错的。las,正如图灵和教堂(彼此独立于)在1936年证明的那样,这种算法不可能存在。这也称为停止问题。不过,我不是数学家,所以我可能错了,但是我认为也适用于这里...

我能想到的最好的替代品并不那么优雅

不确定那个...如果我尝试使用它而不是bfsList的另一个定义,它不会编译...尽管如此,我不认为问题是空列表。

相关内容

最新更新