Haskell 列出 monad 循环

Haskell list monad looping

我有一个如下所示的列表理解:

cross ps = [  p* pp * ppp | p <- ps, pp <- ps, ppp <- ps, p >= pp , pp >= ppp ]

如何在不直接输入列表名称的情况下使用 monad 实现此目的?

dim ps n = do
    p <- ps
    pp <- ps
    ppp <- ps
    p...p <- ps

    guard (p >= pp && pp >= ppp ... && p...p >=p....p)
    return (p*pp*ppp*p...p)

如何在不显式分配值的情况下执行此操作以使用列表 monad?

这是我的做法

ascending :: Ord a => [a] -> Bool
ascending list = and $ zipWith (>=) (tail list) list

dim ps n = map product $ filter ascending allComb 
    where allComb = replicateM n ps

replicateM 来自 Control.Monad 并且对于列表 monad,它生成给定列表的 n 元素的所有组合。 然后我只过滤掉按升序排列的组合,最后计算剩下的列表的乘积。

近似翻译可以是:

dim :: Num a => [a] -> Int -> [a]
dim ps n = do
  chosen <- replicateM n ps
  guard $ increasing chosen
  return $ product chosen

increasing :: Ord a => [a] -> Bool
increasing []        = True
increasing xs@(_:ys) = and $ zipWith (<=) xs ys

然而,这可以通过更早地放置守卫来改善。我的意思是:

[ ... | p1<-xs, p2<-xs, p3<-xs, p1 <= p2, p2 <= p3 ]

[ ... | p1<-xs, p2<-xs, p1 <= p2, p3<-xs, p2 <= p3 ]

因为后者会在 p1 <= p2 时避免为 p3<-xs 扫描整个列表,所以无论如何我们都不会生成任何东西。

所以,让我们再试一次,用更原始的方法:

dim :: Num a => [a] -> Int -> [a]
dim ps 0 = [1]
dim ps n = do
   x  <- ps
   xs <- dim (filter (>=x) ps) (n-1)
   return (x * xs)

现在我们尽早丢弃不可能的备选方案,在递归调用之前将它们从 ps 中移除。

也许最容易理解的解决方案是“字面上使用循环”:

dim ps n = do
    pz <- forM [1..n] $ \_i -> do
       p <- ps
       return p

    guard $ descending pz
    return $ product pz

但是 do {p <- ps; return p}equivalent to simply ps,对于 forM [1..n] $ \_i -> ps 我们有 shorthand replicateM n ps。所以你得到了 chi 的建议解决方案。不过,我想说 Luka Horvat 的实际上要好一些。

但是话又说回来,正如 chi 所说,您可以通过不选择 所有 可能的组合并丢弃绝大多数,而只选择降序排列来提高效率首先是可能性。为此,我手动编写了一个递归函数:

descendingChoices :: Ord a => Int -> [a] -> [[a]]
descendingChoices 1 ps = [[p] | p<-ps]  -- aka `pure<$>ps`
descendingChoices n ps = [ p : qs | qs <- descendingChoices (n-1) ps
                                  , p <- ps
                                  , all (<=p) qs
                         ]

鉴于您的素数列表是按升序排列的,您可以完全避免守卫,只需生成每组产品一次即可开始:

cross :: Int -> [a] -> [[a]]
cross 0 _ = [[]]
cross n [] = []
cross n all@(x:xs) = ((x:) <$> cross (n - 1) all) ++ cross n xs

dim :: Num a => Int -> [a] -> [a]
dim n xs = map product $ cross n xs

如果素数列表不是按升序排列,那么最好的选择是对其进行排序并使用假定列表已排序的算法。 amalloy 给出了一个,但是您可以通过使用共享 (an example) .

另一个这样的算法是

dim :: (Num a, Ord a) => Int -> [a] -> [a]
dim 0 xs = [1]
dim n xs = [y * x | x <- xs, y <- dim (n - 1) (takeWhile (<= x) xs)]

请注意 takeWhile 而不是 filter。这样你就不需要一遍又一遍地处理整个素数列表,而是总是只处理你真正需要的那些素数。