编辑: 请参阅 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]
答案 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中是很自然的,具有自上而下的列表实例化和逻辑变量,这些变量可以明确地处于尚未设置的状态。另请参阅tailrecursion-modulo-cons。
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
的其他定义,它就不会编译......但是,我不是认为问题是空列表。