在列表理解中拥有许多相同生成器的惯用方式

Posted

技术标签:

【中文标题】在列表理解中拥有许多相同生成器的惯用方式【英文标题】:Idiomatic way to have many of the same generators in a list comprehension 【发布时间】:2018-03-30 14:08:18 【问题描述】:

在统计学课上,我的老师向我们展示了一个概率模型,其中包含两个骰子加到 4 的所有可能掷骰结果。记住 Haskell 列表推导式非常棒,我决定将其带到下一步并编写这段代码找出所有可能的 4 个骰子加到 10

[(d1,d2,d3,d4) | d1 <- [1..6], d2 <- [1..6], d3 <- [1..6], d4 <- [1..6], (d1 + d2 + d3 + d4) == 10]

这按预期工作,给了我

的输出

[(1,1,2,6),(1,1,3,5),(1,1,4,4),(1,1,5,3),(1,1,6 ,2),(1,2,1,6),(1,2,2,5),(1,2,3,4),(1,2,4,3),(1,2,5 ,2),(1,2,6,1),(1,3,1,5),(1,3,2,4),(1,3,3,3),(1,3,4 ,2),(1,3,5,1),(1,4,1,4),(1,4,2,3),(1,4,3,2),(1,4,4 ,1),(1,5,1,3),(1,5,2,2),(1,5,3,1),(1,6,1,2),(1,6,2 ,1),(2,1,1,6),(2,1,2,5),(2,1,3,4),(2,1,4,3),(2,1,5 ,2),(2,1,6,1),(2,2,1,5),(2,2,2,4),(2,2,3,3),(2,2,4 ,2),(2,2,5,1),(2,3,1,4),(2,3,2,3),(2,3,3,2),(2,3,4 ,1),(2,4,1,3),(2,4,2,2),(2,4,3,1),(2,5,1,2),(2,5,2 ,1),(2,6,1,1),(3,1,1,5),(3,1,2,4),(3,1,3,3),(3,1,4 ,2),(3,1,5,1),(3,2,1,4),(3,2,2,3),(3,2,3,2),(3,2,4 ,1),(3,3,1,3),(3,3,2,2),(3,3,3,1),(3,4,1,2),(3,4,2 ,1),(3,5,1,1),(4,1,1,4),(4,1,2,3),(4,1,3,2),(4,1,4 ,1),(4,2,1,3),(4,2,2,2),(4,2,3,1),(4,3,1,2),(4,3,2 ,1),(4,4,1,1),(5,1,1,3),(5,1,2,2),(5,1,3,1),(5,2,1 ,2),(5,2,2,1),(5,3,1,1),(6,1,1,2),(6,1,2,1),(6,2,1 ,1)]

这就是我的问题所在。Ruby 是我背景的重要组成部分,所以我非常重视DRY 原则。在我的代码中包含d1 <- [1..6], d2 <- [1..6], d3 <- [1..6], d4 <- [1..6] 似乎没有必要,我相信有更好的方法来做到这一点。

据我了解,我当前的方法通过在后台运行 4 个嵌套循环来发挥作用——每个生成器一个。有没有办法让一个 <- [1..6] 生成器为所有变量工作,有效地创建 4 个嵌套循环?如果没有,是否有一种更少冗余或惯用的方式来编写此代码以实现相同的结果?

注意:我对这门语言还很陌生,所以如果这是显而易见的事情,我深表歉意。如果您使用了新手命令式/面向对象程序员不熟悉的任何单词/概念,请尝试为我解释一下。

【问题讨论】:

【参考方案1】:

如果您担心[1..6] 重复(范围独立变化的能力),您可以使用:

let die = [1..6] in [ (d1,d2,d3,d4) | d1 <- die, d2 <- die
                                    , d3 <- die, d4 <- die
                                    , (d1 + d2 + d3 + d4) == 10 ]

总体而言,要删除显式的骰子命名,虽然这并不完全相同,因为它将是列表而不是元组:

let die = [1..6] in [dice | dice <- sequence (replicate 4 die), sum dice == 10]

要恢复元组,您可以进行模式匹配,但是如果输入表达式发生更改,则可能会引入难以跟踪的错误,因为模式匹配失败将简单地排除元素:

let die = [1..6] in
  [ (d1,d2,d3,d4) | dice@[d1,d2,d3,d4] <- sequence (replicate 4 die)
                  , sum dice == 10 ]

【讨论】:

sequence (replicate 4 die)replicateM 4 die【参考方案2】:

如果你想坚持4-Tuples,即(1,1,3,5),这不是很优雅,但如果你愿意使用列表代替你可以相当优雅地管理

import Control.Monad
listSum10 = filter ((==10) . sum) $ replicateM 4 [1..6]
          = [dice | dice <- replicateM 4 [1..6], sum dice == 10]

或使用do-notation

listSum10 = do x <- replicateM 4 [1..6]
               guard $ sum x == 10
               return x

【讨论】:

【参考方案3】:

edit2:惯用的方法是在执行搜索时尽早进行测试——尽可能早——以减少尽可能搜索空间:

  let die = [1..6] in [ (d1,d2,d3,d4) | d1 <- die, d2 <- die, 
                         s2 <- [d1 + d2], s2 <= 8, d3 <- die, 
                         s3 <- [s2 + d3], s3 <= 9, s3 >= 4, d4 <- [10 - s3]]

以下是解决此问题的不同方法。

这里的想法是创建某种数据处理器和乘法器网络来实现目标,同时抓住机会提高效率。使用重复数据来避免重复工作,我们安排处理器链/数据路径图的对数高度,而不是线性高度。

import qualified Data.List.Ordered as O          -- from the data-ordlist package
import Data.Ord 

lim = 10                                                       -- the target score

(⊗) :: [(Int, a)] -> [(Int, b)] -> [(Int, (a, b))]            -- cross-product in
xs ⊗ ys = takeWhile ((<= lim).fst) $                          -- ascending order:
            O.foldt' (O.mergeBy $ comparing fst) []            --   merge all via a
              [ [(p+q, (a,b)) | (q,b) <- ys] | (p,a) <- xs]    --   balanced tree
                                       -- combine the points while tracking the score
g2 ys = ys ⊗ ys                -- the doubling combinator
ys = [ (x,x) | x <- [1..6]]     -- six sides to a die

r2 = g2 ys           -- results from rolling the dice twice,
r3 = ys ⊗ r2        -- three times, 
r4 = g2 r2           -- 4,      /less efficiently: (ys ⊗ (ys ⊗ (ys ⊗ ys)))/
r5 = ys ⊗ r4        -- 5,
r6 = g2 r3           -- 6       /less efficiently: (ys ⊗ (ys ⊗ ... (ys ⊗ ys) ...))/

foo rn = map snd $ dropWhile ((< lim).fst) rn

要从所有这些点点滴滴中获得适当的功能,剩下的就是找出从n 中创建rn 的一般方法——通过使用它的二进制表示,或者通过重复减半或其他方式.

目前,

~> foo r2 [(4,6),(5,5),(6,4)] ~> 拿 10 $ foo r4 [((1,1),(2,6)),((1,1),(3,5)),((1,1),(4,4)),((1,1), (5,3)),((1,1),(6,2)),((1,2),(1 ,6)),((1,2),(2,5)),((1,2),(3,4)),((1,2),(4,3)),((1 ,2),(5,2))] ~> 拿 10 $ foo r6 [((1,(1,1)),(1,(1,5))),((1,(1,1)),(1,(2,4))),((1,( 1,1)),(1,(3,3))),((1,(1,1)),(1 ,(4,2))),((1,(1,1)),(1,(5,1))),((1,(1,1)),(2,(1,4) )),((1,(1,1)),(2,(2,3))),((1,( 1,1)),(2,(3,2))),((1,(1,1)),(2,(4,1))),((1,(1,1)), (3,(1,3)))]

【讨论】:

那么,这种方法是更有效……还是更难理解? 它可能有效,但我真的不认为这有什么惯用的。 上述 cmets 指的是这篇文章的早期版本,该版本经过重新设计,使其更加清晰 IMO。此外,在那个之后添加了另一个惯用代码。 我喜欢这种方法。同时,我想知道您对下面我的解决方案的看法。使用 n 叉树不是很合理而且很有效吗?【参考方案4】:

我相信在 Haskell 中,可以通过为掷骰子过程找到最合适的数据类型来实现惯用方式。

那是什么..?我们将掷n 骰子m 次。该算法需要玫瑰树(N-ary tree)数据类型。比如

data Rolls = Dice :< [Rolls] deriving Show

你已经在Data.Tree 包中拥有了这个,但是为了用更简单的术语表达逻辑,我将尝试使用上述数据非常简化的 N 叉树类型。通过展开使用标准 Data.Tree 包来实现这一点很简单。

这也是非常有效的,因为我们在不可能的路线发生之前消除了它们。所以这里没有低效的组合和过滤。

type Count = Int
type Total = Int
type Value = Int
type Dice  = (Count, Total, Value)
data Rolls = Dice :< [Rolls] deriving Show

rollDices :: Dice -> Int -> Int -> Rolls
rollDices (c,t,v) tc tt | c <= tc-2 = let bl = (\x -> if x < 1 then 1 else x) ((tt-t)-(tc-c-1)*6)  -- bottom limit
                                          tl = (tt-t)-(tc-c)+1                                     -- top limit
                                          bs = if bl <= 6 then if tl <= 6 then [bl..tl]            -- branches
                                                                          else [bl..6]
                                                          else []
                                      in  (c,t,v) :< map (\n -> rollDices (c+1, t+n, n) tc tt) bs
                        | otherwise = (c,t,v) :< [(c+1,tt, tt-t) :< []]

main :: IO String
main = do
  putStr "How many dice are to be rolled..?   :"
  tc <- (read :: String -> Int) <$> getLine
  putStr "To what sum do you want to reach..? :"
  tt <- (read :: String -> Int) <$> getLine
  return . show $ rollDices (0,0,0) tc tt

这将计算一个具有 m 级的 N 叉树,并且每个路径都将保存除根(种子)为 0 的骰子值。所以让我们看看 4 个骰子的目标总和为 10。

*Main> main
How many dice are to be rolled..?   :4
To what sum do you want to reach..? :10
"(0,0,0) :< [(1,1,1) :< [(2,2,1) :< [(3,4,2) :< [(4,10,6) :< []],(3,5,3) :< [(4,10,5) :< []],(3,6,4) :< [(4,10,4) :< []],(3,7,5) :< [(4,10,3) :< []],(3,8,6) :< [(4,10,2) :< []]],(2,3,2) :< [(3,4,1) :< [(4,10,6) :< []],(3,5,2) :< [(4,10,5) :< []],(3,6,3) :< [(4,10,4) :< []],(3,7,4) :< [(4,10,3) :< []],(3,8,5) :< [(4,10,2) :< []],(3,9,6) :< [(4,10,1) :< []]],(2,4,3) :< [(3,5,1) :< [(4,10,5) :< []],(3,6,2) :< [(4,10,4) :< []],(3,7,3) :< [(4,10,3) :< []],(3,8,4) :< [(4,10,2) :< []],(3,9,5) :< [(4,10,1) :< []]],(2,5,4) :< [(3,6,1) :< [(4,10,4) :< []],(3,7,2) :< [(4,10,3) :< []],(3,8,3) :< [(4,10,2) :< []],(3,9,4) :< [(4,10,1) :< []]],(2,6,5) :< [(3,7,1) :< [(4,10,3) :< []],(3,8,2) :< [(4,10,2) :< []],(3,9,3) :< [(4,10,1) :< []]],(2,7,6) :< [(3,8,1) :< [(4,10,2) :< []],(3,9,2) :< [(4,10,1) :< []]]],(1,2,2) :< [(2,3,1) :< [(3,4,1) :< [(4,10,6) :< []],(3,5,2) :< [(4,10,5) :< []],(3,6,3) :< [(4,10,4) :< []],(3,7,4) :< [(4,10,3) :< []],(3,8,5) :< [(4,10,2) :< []],(3,9,6) :< [(4,10,1) :< []]],(2,4,2) :< [(3,5,1) :< [(4,10,5) :< []],(3,6,2) :< [(4,10,4) :< []],(3,7,3) :< [(4,10,3) :< []],(3,8,4) :< [(4,10,2) :< []],(3,9,5) :< [(4,10,1) :< []]],(2,5,3) :< [(3,6,1) :< [(4,10,4) :< []],(3,7,2) :< [(4,10,3) :< []],(3,8,3) :< [(4,10,2) :< []],(3,9,4) :< [(4,10,1) :< []]],(2,6,4) :< [(3,7,1) :< [(4,10,3) :< []],(3,8,2) :< [(4,10,2) :< []],(3,9,3) :< [(4,10,1) :< []]],(2,7,5) :< [(3,8,1) :< [(4,10,2) :< []],(3,9,2) :< [(4,10,1) :< []]],(2,8,6) :< [(3,9,1) :< [(4,10,1) :< []]]],(1,3,3) :< [(2,4,1) :< [(3,5,1) :< [(4,10,5) :< []],(3,6,2) :< [(4,10,4) :< []],(3,7,3) :< [(4,10,3) :< []],(3,8,4) :< [(4,10,2) :< []],(3,9,5) :< [(4,10,1) :< []]],(2,5,2) :< [(3,6,1) :< [(4,10,4) :< []],(3,7,2) :< [(4,10,3) :< []],(3,8,3) :< [(4,10,2) :< []],(3,9,4) :< [(4,10,1) :< []]],(2,6,3) :< [(3,7,1) :< [(4,10,3) :< []],(3,8,2) :< [(4,10,2) :< []],(3,9,3) :< [(4,10,1) :< []]],(2,7,4) :< [(3,8,1) :< [(4,10,2) :< []],(3,9,2) :< [(4,10,1) :< []]],(2,8,5) :< [(3,9,1) :< [(4,10,1) :< []]]],(1,4,4) :< [(2,5,1) :< [(3,6,1) :< [(4,10,4) :< []],(3,7,2) :< [(4,10,3) :< []],(3,8,3) :< [(4,10,2) :< []],(3,9,4) :< [(4,10,1) :< []]],(2,6,2) :< [(3,7,1) :< [(4,10,3) :< []],(3,8,2) :< [(4,10,2) :< []],(3,9,3) :< [(4,10,1) :< []]],(2,7,3) :< [(3,8,1) :< [(4,10,2) :< []],(3,9,2) :< [(4,10,1) :< []]],(2,8,4) :< [(3,9,1) :< [(4,10,1) :< []]]],(1,5,5) :< [(2,6,1) :< [(3,7,1) :< [(4,10,3) :< []],(3,8,2) :< [(4,10,2) :< []],(3,9,3) :< [(4,10,1) :< []]],(2,7,2) :< [(3,8,1) :< [(4,10,2) :< []],(3,9,2) :< [(4,10,1) :< []]],(2,8,3) :< [(3,9,1) :< [(4,10,1) :< []]]],(1,6,6) :< [(2,7,1) :< [(3,8,1) :< [(4,10,2) :< []],(3,9,2) :< [(4,10,1) :< []]],(2,8,2) :< [(3,9,1) :< [(4,10,1) :< []]]]]"

所以上面我们有树表示。有时间我会写一个toList 函数。

【讨论】:

好主意! “掷 n 个骰子 m 次” 不清楚你的意思。我们掷 1 次骰子 n 次,是吗? rollDices (0,0,0) (n+1) n 应该是 [] 但不是。干杯! 当然树本身是解决方案的最小表示,但是在我们跟踪它以将结果呈现给用户之后,它变成了相同的整体工作量与列表理解一样,当在阶段之间注入更多测试时,与我在s3 测试中的测试相当。 @Will Ness 首先感谢编辑结果显示列表解释。然而,它也表明上述解决方案是错误的。 (哪里是[1,1,2,6])这是由于下限bl值计算错误造成的。我相信我现在已经纠正了。不过,根据您的评论......可能我们不必以列表形式向用户展示结果。 Data.Tree 包有漂亮的 2D 树表示。 是的,同意,只是想指出这一事实。看起来结果的总数比树中的结果多出大约 0.5n 的因子,其中 n 是掷骰子的数量。因此,4 卷再多两次; 6 次滚动 3 倍,等等。但是 OTOH 这意味着树必须全部保存在内存中,而列表可以是完全短暂的,在生成时进行垃圾收集(仅保留 n-1 值在每个时间点的内存中)。所以如果我们对结果做任何事情而不是打印它们,树会更好。否则,两者是等价的。 一个平衡的、完全填充的二叉树 n 层深度有 k=2^(n-1) 个底部节点和 2^ n - 1 ~= 2k 个节点。如果我们打印从顶部到每个底部节点的 k 路径,我们已经打印了 nk 个数字。是的,这忽略了所有跳过的数字,但是跳过的值越多,树的填充越少,节省的空间就越少。要打印树的路径,我们需要将整棵树保存在内存中。对于打印这些路径的列表comprhns(在阶段之间具有适当的测试/约束),它只需要在内存中保存当前路径上n-1个节点的值。

以上是关于在列表理解中拥有许多相同生成器的惯用方式的主要内容,如果未能解决你的问题,请参考以下文章

生成和管理后台线程的惯用 Clojure 方式

我应该如何以惯用的 React 方式访问自定义 HTML 组件的生成子项?

当我尝试插入由列表理解生成的元组列表时,executemany 抛出错误;如果它是硬编码的,则相同的列表有效

Python 嵌套惰性列表

比较列表理解和显式循环(3 个数组生成器比 1 个 for 循环更快)

在 Kotlin 中处理可为空或空列表的惯用方式