使用monads进行列表操作等琐碎任务?

时间:2012-10-03 10:31:31

标签: haskell monads

每当我读到关于Monad的例子时,他们总是将IO作为案例研究。

是否有任何monad进行列表操作的例子,有人可以提出?我认为这可能是矫枉过正,但我​​感兴趣的是monad可以提供优于常规列表操作技术的优势。

6 个答案:

答案 0 :(得分:9)

Haskell中列表monad的最大秘密是列表推导是do blocks 的语法糖。无论何时编写列表推导,您都可以使用do块来编写它,它使用list monad实例。

一个简单的例子

假设您想要获取两个列表,并返回其笛卡尔积(即(x,y)的列表,其中x的每个组合来自第一个列表,y来自第二个清单)。

你可以用列表理解来做到这一点:

ghci> [(x,y) | x <- [1,2], y <- [3,4]] -- [(1,3),(1,4),(2,3),(2,4)]

列表理解是这个块的语法糖:

zs = do x <- [1,2]
        y <- [3,4]
        return (x,y)

反过来又是

的语法糖
zs = [1,2] >>= \x -> [3,4] >>= \y -> return (x,y)

一个更复杂的例子

这个例子并没有真正展示monad的强大功能,因为你可以轻松地编写它而不依赖于列表具有Monad实例的事实。例如,如果我们只使用Applicative实例:

ghci> import Control.Applicative
ghci> (,) <$> [1,2] <*> [3,4] -- [(1,3),(1,4),(2,3),(2,4)]

现在让我们假设您获取正整数列表中的每个元素,并将其复制多次(例如f [1,2,3] = [1,2,2,3,3,3])。谁知道你为什么要那样做,但很容易:

ghci> let f xs = [ y | x <- xs, y <- replicate x x ]
ghci> f [1,2,3] -- [1,2,2,3,3,3]

这只是语法糖:

f xs = do x <- xs
          y <- replicate x x
          return y

反过来又是

的语法糖
f xs = xs >>= \x -> replicate x x >>= \y -> return y

这次我们不能只使用应用实例来编写。关键的区别在于我们从第一个绑定(x)获取输出,并使块的其余部分依赖于它(y <- replicate x x)。

答案 1 :(得分:9)

使用list monad巧妙地编写“简单”列表实用程序函数的典型示例是:

import Control.Monad (filterM)

-- | Compute all subsets of the elements of a list.
powerSet :: [a] -> [[a]]
powerSet = filterM (\x -> [True, False])

filterM的类型为Monad m => (a -> m Bool) -> [a] -> m [a]。在列表monad的上下文中,这是一个非确定性列表过滤器:一个过滤操作,它采用非确定性谓词,返回替代列表答案。 filterM的结果反过来列出了替代可能的结果。

或者用更简单的语言,filterM (\x -> [True, False])表示:对于列表中的每个元素,我希望保留它并将其丢弃。 filterM计算出为每个列表元素执行此操作的所有可能组合。

答案 2 :(得分:4)

这是一种非常愚蠢的方法来查找给定整数的除数对:

divisors:: Int -> [(Int,Int)]
divisors n = do
  x <- [1 .. n]
  y <- [1 .. n]
  if x*y == n then return (x, y) else []

这个片段的意义应该是相当直截了当的。显然,这是解决这个特定问题的一种愚蠢方法。 (在这种情况下,有更多有效的方法可行。)但现在想象一些更复杂的问题,这种搜索非常复杂。这种用于搜索的习惯用法非常有用。

答案 3 :(得分:3)

另一个例子是从其素数因子分解中构造一个数的所有除数(虽然不按顺序):

divs n = map product 
           . mapM (\(p,n)-> map (p^) [0..n]) 
           . primeFactorization $ n

-- mapM f = sequence . map f

-- primeFactorization 12  ==>  [(2,2),(3,1)]  -- 12 == 2^2 * 3^1

f中的函数mapM f == sequence . map f为输入列表中的每个条目生成一个因子的幂列表;然后sequence形成列表中的所有路径,一次从每个路径中选择一个号码;然后将所有可能组合的列表输入map product,计算除数:

12                                      -- primeFactorization:
[(2,2),(3,1)]                           -- map (\(p,n)-> ...):
[ [1,2,4], [1,3] ]                      -- sequence:
[[1,1],[1,3],[2,1],[2,3],[4,1],[4,3]]   -- map product:
[1,3,2,6,4,12]                          --   the divisors of 12

Luis Casillas's answer中给出的精彩描述也适用于此:

  

在列表monad的上下文中,mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]非确定性列表映射:映射非确定性函数的映射操作 - 这样生成一个列表替代结果 - 在列表上,创建所有可能的替代结果列表。

答案 4 :(得分:3)

列表操作的一个很好的例子是查询HTML / XML文档, a la jQuery。 jQuery是monad。我们来看一个例子:

$(doc).find('> body')
      .find('> *')
      .is('table')
      .is('.bar')
      .find('> caption')
      .text()

这为您提供了具有CSS类bar的所有顶级表的标题。这是使用jQuery的一种不寻常的方式(通常你只是做body > table.bar > caption),但这是因为我想显示步骤。

xml-conduit中,您可以同样地执行此操作:

child cursor >>= element "body" >>= child
        >>= element "table" >=> attributeIs "class" "bar"
        >>= child >>= element "caption"
        >>= descendants >>= content

monad所做的是组合这些步骤。 monad本身对HTML / XML一无所知。

HXT中,您几乎以同样的方式执行此操作。较新的HXT版本使用所谓的箭头而不是monad,但基本原理是相同的。

答案 5 :(得分:2)

Monad和列表操作与MonadPlus的{​​{1}}和另一个monad结合使用时更加有趣。举个例子,让我们解决经典的guard难题:SEND + MORE = MONEY。每个字母必须代表一个唯一的数字,前导数字不能为零。

为此,我们将从StateT[]构建一个monad堆栈:

import Control.Monad
import Control.Monad.List
import Control.Monad.State

-- Verbal Arithmetic monad
type VA a = StateT [Int] [] a

列表monads允许我们分支计算以搜索所有可能的方式,状态是可供选择的数字列表。我们可以通过给出所有可能的数字来运行这个monad:

runVA :: VA a -> [a]
runVA k = evalStateT k [0..9]

我们需要一个辅助功能,通过选择所有可用数字来分支计算,并用剩下的内容更新状态:

pick :: VA Int
pick = do
    -- Get available digits:
    digits <- get
    -- Pick one and update the state with what's left.
    (d, ds) <- lift $ split digits
    put ds
    -- Return the picked one.
    return d
  where
    -- List all possible ways how to remove an element from a list.
    split :: [a] -> [(a, [a])]
    split = split' []
    split' _ []      = []
    split' ls (x:rs) = (x, ls ++ rs) : split' (x : ls) rs

此外,我们经常需要选择一个非零数字:

pickNZ :: VA Int
pickNZ = do
    d <- pick
    guard (d /= 0)
    return d

解决这个难题现在很简单:我们可以简单地逐个数字地实现添加,并使用guard检查结果的数字是否等于总和:

--   SEND
-- + MORE
-- ------
--  MONEY
money :: [(Int,Int,Int)]
money = runVA $ do
    d <- pick
    e <- pick
    let (d1, r1) = (d + e) `divMod` 10
    y <- pick
    guard $ y == r1
    n <- pick
    r <- pick
    let (d2, r2) = (n + r + d1) `divMod` 10
    guard $ e == r2
    o <- pick
    let (d3, r3) = (e + o + d2) `divMod` 10
    guard $ n == r3
    s <- pickNZ
    m <- pickNZ -- Actually M must be 1, but let's pretend we don't know it.
    let (d4, r4) = (s + m + d3) `divMod` 10
    guard $ r4 == o
    guard $ d4 == m
    return (ds [s,e,n,d], ds [m,o,r,e], ds [m,o,n,e,y])
  where
    -- Convert a list of digits into a number.
    ds = foldl (\x y -> x * 10 + y) 0

这个谜题只有一个结果:(9567,1085,10652)。 (当然代码可以进一步优化,但我希望它很简单。)

可以在这里找到更多谜题:verbal arithmetic