【问题标题】:Haskell self-referential List terminationHaskell 自引用列表终止
【发布时间】:2017-09-27 10:13:43
【问题描述】:

编辑: 请参阅 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]

【问题讨论】:

  • 我现在认为添加takeWhile (not.null) 正确和最简单的解决方案,并且希望完全明智的事情concat . iterate (const []) 实际上应该终止(并且等效于列表的id)。

标签: list haskell recursion


【解决方案1】:

已编辑以添加注释来解释我在下面的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)

这使用了AlternativeMaybe 实例,这真的非常简单:

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))

【讨论】:

  • 感谢您的见解。我对您的替代方案很感兴趣,但在我的 Haskell 中还没有达到足够远的程度来掌握它们,稍后将不得不重新访问。我会对你对我编辑的问题的看法非常感兴趣,也许解释为什么 list = 1 : tail list 需要永远阻止会阐明我还不了解的 Haskell 的某些方面。
  • bfsList = concatMap (\x -&gt; []) bfsList 实际上是一个很好的例子,因为通过一点的人类推理我们可以看到concatMap (\x -&gt; []) bfsList无论如何都会产生一个空列表,所以bfsList必须[]。当然,这不是 Haskell 的工作方式。
  • @Erik 你想要的是完整的等式推理一直向下。但这不是 Haskell,不幸的是(或者不是,YMMV)。
  • 我认为foo = case foo of [] -&gt; [] 不一样,因为它并不详尽。 foo = case foo of [] -&gt; []; (_:_) -&gt; [] 是一样的。
  • 你错过了最后一行的xs。大量使用find p (a++b) = find p a &lt;|&gt; find p b BTW,避免使用++length
【解决方案2】:

我们逐步生成结果列表(队列)。在每一步中,我们都会消耗我们在上一步中产生的东西。当最后一个扩展步骤什么都不添加时,我们停止:

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(搜索“corecursive queue”):

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 中是很自然的,它具有自上而下的列表实例化和可以显式处于 尚未设置 状态的逻辑变量。另见


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

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

【讨论】:

  • 在某些情况下,不能让语言为我们执行“计数器增量”步骤吗?至少,GHC 应该能够判断超出当前列表末尾的重入读取是否会永远阻塞,并抛出错误。我将我的问题编辑为一个更简单的示例,以说明我所指的阻止列表生成器类,并且对您的想法非常感兴趣。
  • 希望当您发布新问题时,它会引起一些专家的注意。 :) 我认为 GHCi 有时 能够检测到这些情况,但我不确定具体情况。
【解决方案3】:

bfsList 是递归定义的,这在 Haskell 中本身不是问题。然而,它确实产生了一个无限列表,这本身也不是问题,因为 Haskell 是惰性求值的。

只要find 最终找到它正在寻找的东西,仍然存在无限的元素就不是问题,因为此时评估停止(或者,更确切地说,继续做其他事情)。

AFAICT,第二种情况的问题是谓词永远不会匹配,所以bfsList 只是不断产生新元素,find 继续寻找。

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

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

是否可以让 Haskell 变得足够聪明,以实现列表生成被阻止读取自引用并终止列表?

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

罢工>

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

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

【讨论】:

  • 这里我们知道,因为计算的是 我们。在每个步骤中,我们都会消耗 我们 在上一步中生成的内容。当最后一个扩展步骤没有添加任何内容时,我们停止。我已经用工作代码添加了一个答案。
  • 是的,OP 诊断是正确的。你错过了if x&lt;1 then [] 部分吗?这些不是整数,因为(**) :: Floating a =&gt; a -&gt; a -&gt; a,因此2**8 :: Floating a =&gt; a,所以2**8 /5 /5 /5 /5 = 0.4096
  • @WillNess 是的,我完全错过了。我不知道我是怎么做到的,因为我什至没有特别着急;我真的没有借口。
  • 我责怪x&lt;1 中的(缺少)空格。我曾经也写过密集的代码,比如 OP。我现在什至经常添加无关的空格。
  • @WillNess 谢谢你让我诚实。我考虑删除答案,但后来我认为关于停止问题的部分可能有点用处,所以我最终删除了大部分其他内容。
猜你喜欢
  • 2018-03-10
  • 1970-01-01
  • 2022-10-09
  • 2011-01-08
  • 2011-01-30
  • 1970-01-01
  • 2013-06-05
  • 1970-01-01
  • 2013-04-17
相关资源
最近更新 更多