从 Prolog 到 Haskell 的思考——生成真值组合列表

Posted

技术标签:

【中文标题】从 Prolog 到 Haskell 的思考——生成真值组合列表【英文标题】:Thinking out of Prolog and into Haskell - Generating Lists of Truth-Value Combinations 【发布时间】:2014-06-24 01:11:07 【问题描述】:

我知道一点 Prolog,并且刚刚开始在 Haskell 中进行一些自学。我一直在通过99 Problems for Haskell 工作,一路上学到了很多东西,并且非常喜欢 Haskell。有时我试图通过在 Prolog 中编写一些代码来阐明我对问题空间的理解,然后思考该解决方案如何与 Haskell 中的函数式方法相关联。今晚,在处理有关逻辑的问题(问题 47 和 48)时,我分心了,试图完成一个简单的任务,即建立一个包含 n 个值的所有真值组合列表的列表。我想要一个函数tValues :: Int -> [[Bool]]。这个问题可以在 Prolog 中以非常直接和声明式的方式解决,例如:

combinations_of_n_truth_values(N, AllValues) :-
    findall(Values, n_truth_values(N, Values), AllValues).

n_truth_values(N, TruthValues) :-
    length(TruthValues, N),
    maplist(truth_value, TruthValues).

truth_value(true).
truth_value(false).

使用时,变量AllValues 将与所需的真值列表列表统一。我想知道有经验的 Haskell 程序员将如何解决同样的问题。我希望有一个同样直接和声明式的解决方案,但我似乎无法让我的 Haskell 大脑以正确的方式运作。

我已经使用列表推导来摆弄一些半模拟,例如:

tValues:: Int -> [[Bool]]
tValues 0 = []
tValues n = [v:vs | v  <- tValue
                  , vs <- tValues (n-1) ] 

tValue :: [Bool]
tValue = [True, False]

tValues 只返回[]。我想我只需要一些人与人之间的接触来帮助我清醒地摇头,也许能让我有更深入的洞察力。

非常感谢。

【问题讨论】:

【参考方案1】:

在伪代码中,您的列表理解

[v:vs | v  <- tValue
      , vs <- tValues (n-1) ] 

等于

for any combination of two elements in `tValue`  and `tValues (n-1)`
    cons the first onto the latter

但是,tValues 没有以任何元素开头,它是一个空列表。让我们模拟一下n = 1

tValues 1 = [v:vs | v  <- tValue
                  , vs <- tValues 0 ] 
          = [v:vs | v  <- [True, False]
                  , vs <- [] ] 
            -- since there is no vs
          = []

这会在整个递归中传播。解决方案非常简单:将基本情况更改为包含一个空组合:

tValues 0 = [[]] -- or: return []

现在模拟结果:

tValues 1 = [v:vs | v  <- tValue
                  , vs <- tValues 0 ] 
          = [v:vs | v  <- [True, False]
                  , vs <- [[]]] 
            -- vs is []
          = [True:[],False:[]]
          = [[True],[False]]

这正是我们想要的。

【讨论】:

感谢您的回答!我认为这澄清了我在尝试使用列表推导时遇到的几个问题。得知我忽略了一个小细节,而不是从根本上误解了这个问题,这也令人鼓舞。我也很欣赏自然语言翻译,因为我在 Prolog 的学习使我习惯于用简单的声明性陈述来制定答案并在此基础上构建算法。 您是否有兴趣权衡这种方法与此处提供的其他方法之间的关系?例如,与坚持使用 list-builder 语法相比,是否存在激发显式使用 list monad 的意识形态差异,或者仅仅是风格/方便的问题?无论如何,再次感谢! @aBathologist:我仍然称自己为 Haskell 初学者,所以我的观点可能不是真正的专业观点。对我来说,这或多或少是一个方便的问题:列表解析语法易于阅读,并且可以被 Haskell 初学者理解。它甚至可以立即转换为一元形式:do v &lt;- [True, False]; vs &lt;- tValues (n-1); return (v:vs);(谓词可以通过guard 来自Control.Monad)。这个中间单子版本可以帮助你找到像 m09 的flip replicateM [True, False] 这样优雅的版本。最后由你决定。【参考方案2】:

使用列表单子,

g n = mapM (\_-> [True, False]) [1..n]

关于您的函数的基本情况。由于该函数返回一个包含所有长度为n的真值列表的列表,因此对于n=0,它应该返回一个包含所有长度为0的真值列表,即包含一个空列表的列表。

【讨论】:

为了明确“使用monad”的部分,我们可以写成sequence . replicate n $ [True, False] 深入研究一元库函数,您的sequence . replicate n 已经作为Control.Monad.replicateM n 存在。 所以replicateM n [True, False] 肯定会赢得短距离比赛! 实际上,这正是 your Prolog 代码maplist(truth_value, N_long_list) 的翻译,因为 Haskell 的 list monad 通过收集谓词的所有解决方案的列表来表达不确定性,就好像通过隐含的findall。所以它是完全相同的代码,一个映射一个映射。 :) 不客气。 :) “替代计算策略”正是 monad 通常被查看的方式:列表类型 [] 表示不确定性,Maybe 表示失败的可能性,Either 允许失败携带自己的信息等。 【参考方案3】:
truths :: [[Bool]]
truths = [True] : [False] : concatMap (\bs -> [True:bs, False:bs]) truths

truthValues :: Int -> [[Bool]]
truthValues n = dropWhile ((< n) . length) . takeWhile ((<= n) . length) $ truths

truths 是所有真值组合的无限列表,因为长度单调增加,我们可以只取列表的前面部分,而长度小于或等于我们正在寻找的集合,然后丢弃长度较短的前面。

你只得到空列表的原因是最终tValues (n-1) 评估为tValues 0,这当然是空列表。尝试从列表推导中的空列表中进行绘制会导致迭代失败。这会向上传播理解链。将基本情况更改为tValues 1 = [[True], [False]] 即可。

【讨论】:

非常感谢您提供有趣的解决方案,并就我的基本情况给出了这样一个可以理解的诊断。其他答案没有指出v &lt;- [] 是一个失败的条件,理解这一点确实有助于为我澄清这个问题。我真的很喜欢这种方法,它生成一个包含所有可能的真值系列的抽象对象。不过,我确实对从无限列表中间切出所需集合的开销有点犹豫。但这可能更像是一个心理问题而不是计算问题?非常感谢! 很高兴您发现它的价值!不过你是对的,这绝对不是这里给出的最有效的解决方案。这主要是一个聪明的新奇事物,使用与我最喜欢的斐波那契数列定义相同的模式fibs = 1 : 1 : zipWith (+) fibs (tail fibs)【参考方案4】:

本身不是一个完整的答案,但如果您感兴趣,请使用 list monad:

truthValues ∷ Int → [[Bool]]
truthValues 0 = return []
truthValues n = truthValues (n - 1) >>= (\l → [True:l, False:l])

truthValues n = foldM (\l _ -> [True:l, False:l]) [] [1..n]

truthValues = flip replicateM [True, False]

另请参阅 Will Ness 的回答和随附的 cmets :)

【讨论】:

非常感谢您的回答。我下午的计划围绕着研究你的解决方案,以清楚地了解正在发生的事情。您的 list-monad 答案确实让我感兴趣:我特别想听听一些关于直接使用 list monad 而不是仅使用 list-builder 语法的动机。是否有特定的用例会引导您以某种方式或其他方式引导您,还是更多的是原则问题? 从表面上看,使用 monad 似乎会导致一种更加命令式的风格,而使用绑定和returnflip 等)。这是直接使用 monad 的一个共同特征,还是我在这里误读了?非常感谢! return 本质上不是必须的。它与命令式语言中的 return 不同:它只是将它给出的值放入 monad。这里return [] 也可以写成[[]]flip 只是在这里用来实现无点样式,我们可以改写为 truthValues n = replicateM n [True, False]。 list monad 的优点在于它的核心——它的bind 操作——正是你想要的:即它以非确定性的方式组合了两个值(a la Prolog)。这就是为什么最后一个解决方案如此简短而切题的原因。 是的,replicateM n [True,False] 实际上用英语说,“n 非确定性布尔值”。的确很好。 :) --- 关于return,更好的名字可能是inject - 甚至是yield

以上是关于从 Prolog 到 Haskell 的思考——生成真值组合列表的主要内容,如果未能解决你的问题,请参考以下文章

[Haskell] 为什么列表操作++很昂贵?

如何在Haskell中产生无穷大?

关于语言的思考

Prolog 逻辑拼图不起作用?

编程语言

是否可以从 C# 运行 prolog?