【问题标题】:Haskell: Handling deadlocked self-referential listsHaskell:处理死锁的自引用列表
【发布时间】:2018-03-10 17:24:17
【问题描述】:

GHC 允许以下内容永久阻塞是否有任何有用的理由:

list = 1 : tail list

似乎在列表迭代器/生成器中稍微复杂一点,我们应该能够做一些更有用的事情:

  1. 返回error "Infinitely blocking list"
  2. 返回[1,1]

解释2:当进入生成器获取元素N时,我们可以将生成器内的所有自引用限制在列表中,但以N-1结尾(我们注意到read N在范围generate N 并返回列表末尾)。这是一种使用范围的简单死锁检测。

显然,这对于上面的玩具示例没有那么有用,但它可能允许更有用/优雅的有限、自引用列表定义,例如:

primes = filter (\x -> none ((==0).mod x) primes) [2..]

请注意,任何一种更改都应该只影响当前会导致无限块的列表生成器,因此它们似乎是向后兼容的语言更改。

暂时忽略进行此类更改所需的 GHC 复杂性,这种行为会破坏我遗漏的任何现有语言行为吗?对这种变化的“优雅”有何其他想法?

另请参阅下面可能受益的另一个 BFS 示例。对我来说,这似乎比其他一些解决方案更实用/更优雅,因为我只需要定义 bfsList ,而不是 如何 来生成它(即指定终止条件):

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

【问题讨论】:

  • 虽然在这种情况下这可能很简单,但通常您要求编译器解决停止问题。但无论如何,为什么编译器不代表代码描述的列表呢?在该示例中返回 [1,1] 完全是虚假的,因为例如 [1,32132132] 也同样有效。
  • 给定list = 1 : tail list,即list = 1 : t where t = tail list,什么是t?这是t = tail list = tail (1 : t) = t。即使从等式推理t = tt 的任何值都是微不足道的,所以它实际上根本没有说明t,所以没有理由给它任何特定值。尝试打印 list 可能会导致 [1,***ERROR: black hole detected***[1, 并陷入无限循环。将列表关闭为[1] 将意味着t = [],这也是不合理的(因此,从等式推理的角度来看,这是错误的做法)。
  • list = 1 : tail list 不会阻止 haskell 中的任何内容,也不会阻止 ghc haskell..它是一个完全有效的值定义,其上有明确定义的操作,可以工作并且有用。
  • 求解递归方程意味着在某个域中为合适的函数取最小不动点。在这里,“最少”的意思是“最少定义”:任何比这更定义的东西本质上都涉及从这种空气中创造价值。例如。解决let (a,b,c,d) = (b,1,d,c)必须选择a=b=1,但c,d有无限多的选择。由于我们通常无法解决停机问题,因此唯一明智的选择是c=d=bottom,其中bottom 是非终止/无限递归/无限循环。

标签: list haskell recursion deadlock self-reference


【解决方案1】:

即使list 在 GHCi 下永远循环,使用 GHC 编译的正确二进制文件确实会检测到循环并发出错误信号。如果编译运行:

list = 1 : tail list
main = print list

它以错误消息终止:

Loop: <<loop>>

它对您的 primes 示例执行相同的操作。

正如其他人所指出的,GHC 不会检测到所有可能的循环。如果确实如此,那么它将解决停机问题,这可能会使 Haskell 更受欢迎。

它返回错误(或“卡住”)而不是返回[1,1] 的原因是因为表达式:

list = 1 : tail list

在 Haskell 语言中具有明确定义的语义。这些语义为其分配了一个值,该值是“底部”(或“错误”或符号_|_),就像head [1,2,3] 的值是1 一样肯定。

(嗯,从技术上讲,list 的值是1 : _|_,这是“几乎底部”。这就是@Justin Li 在他的评论中所说的。我试图解释为什么它有这个值在下面。)

尽管您可能看不到返回底部的程序或表达式的使用,也看不到基于“向后兼容”为此类表达式分配非底部语义的危害,但 Haskell 社区中的大多数人(语言设计者、编译器开发者和有经验的用户)会不同意你的观点,所以不要指望与他们取得太大进展。

至于您提出的具体新语义,尚不清楚。为什么list 的值不等于[1]?在我看来,当我进入“生成器”以获取元素 n=1(零索引,因此是第二个元素)并评估 tail list 时,以元素 n-1=0 结尾的 list 是 @987654336 @ 的尾部等于[],所以我想我应该得到以下内容,对吧?

list = 1 : tail list
     = 1 : tail [1]   -- use list-so-far
     = 1 : []
     = [1]

为什么值是(几乎)底部

这就是为什么list 的值(几乎)是底部的原因,根据标准 Haskell 的语义(但请参阅末尾的注释)。

作为参考,tail 的定义实际上是:

tail l = case l of _:xs -> xs
                   [] -> error "ack, you dummy!"

让我们尝试使用 Haskell 语义“完全”评估 list

-- evaluating `list` using definition of `list`
list = 1 : tail list

-- evaluating `tail list` using definition of `tail`
list = 1 : case list of _:xs -> xs
                        ...
-- evaluating case construct requires matching `list` to
-- a pattern, this requires evaluation of `list` using its defn
list = 1 : case (1 : tail list) of _:xs -> xs
                                   ...
-- case pattern match succeeds
list = 1 : let xs = tail list in xs    -- just to be clear
     = 1 : tail list

-- awesome, now all we need to do is evaluate:
list = 1 : tail list
-- ummm, Houston, we have a problem

最后的无限循环是表达式“几乎底部”的原因。

注意:实际上有几组不同的 Haskell 语义,计算 Haskell 表达式值的不同方法。黄金标准是@luqui 的回答中描述的指称语义。我上面使用的那些,充其量只是 Haskell 报告中描述的“非正式语义”的一种形式,但它们足以得到正确的答案。

【讨论】:

    【解决方案2】:

    这是关于list = 1 : ⊥ 的指称视角。

    首先,有一点背景。在 Haskell 中,值按“定义性”部分排序,其中值涉及 &bot; ("bottom") 的定义比没有的少。所以

    • 的定义比 1 : ⊥
    • 1 : ⊥ 的定义少于 1 : 2 : 3 : []

    但这是一个偏序,所以

    • 1 : ⊥没有2 : 3 : ⊥定义少,也没有更多定义。

    即使第二个列表更长。 1 : ⊥ 的定义仅比以 1 开头的列表少。我强烈建议阅读 Haskell 的 denotational semantics

    现在回答你的问题。看看

    list = 1 : tail list
    

    作为要求解的方程而不是“函数声明”。我们这样重写它:

    list = ((1 :) . tail) list
    

    这样看,我们看到list是一个不动点

    list = f list
    

    在哪里f = (1 :) . tail。在 Haskell 语义中,递归值是通过根据上述顺序找到最小不动点来解决的。

    找到它的方法非常简单。如果你从 ⊥ 开始,然后一遍又一遍地应用这个函数,你会发现一个递增的值链。链条停止变化的点will be the least fixed point(从技术上讲,这将是链条的极限,因为它可能永远不会停止变化)。

    以⊥开头,

    f ⊥ = ((1 :) . tail) ⊥ = 1 : tail ⊥
    

    我们看到 ⊥ 已经不是一个固定点,因为我们没有从另一端得到 ⊥。所以让我们用我们得到的东西再试一次:

    f (1 : tail ⊥) = ((1 :) . tail) (1 : tail ⊥)
                   = 1 : tail (1 : tail ⊥)
                   = 1 : tail ⊥
    

    哦,看,这是一个固定点,我们得到的东西和我们输入的一样。

    这里的重点是它是最少的。你的解[1,1] = 1:1:[]也是一个不动点,所以它解方程:

    f (1:1:[]) = ((1 :) . tail) (1:1:[]) 
               = 1 : tail (1:1:[])
               = 1:1:[]
    

    当然,每个以 1 开头的列表都是一个解决方案,我们还不清楚我们应该如何在它们之间进行选择。但是,我们通过递归找到的 1:⊥ 的定义比所有这些都少,它提供的信息不超过方程式所需的信息,这就是语言指定的信息。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-07-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多