每当我阅读 Monad 示例时,他们总是将 IO 作为案例研究。
有没有人可以提出的单子进行列表操作的例子?我认为这可能有点矫枉过正,但如果 monad 能够比常规列表操作技术更具优势,我很感兴趣。
Haskell 中 list monad 的一大秘密是,列表推导是do 块的语法糖。任何时候你写一个列表推导式,你都可以用一个 do 块来代替它,它使用 list monad 实例。
假设您要获取两个列表,并返回它们的笛卡尔积(即第一个列表和第二个列表(x,y)
的每个组合的列表)。x
y
您可以通过列表理解来做到这一点:
ghci> [(x,y) | x <- [1,2], y <- [3,4]] -- [(1,3),(1,4),(2,3),(2,4)]
列表理解是这个 do 块的语法糖:
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
)。
使用 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]
。在 list monad 的上下文中,这是一个非确定性列表过滤器:一个过滤器操作,它采用一个返回替代答案列表的非确定性谓词。的结果又是替代可能结果的列表。filterM
或者用更简单的语言,filterM (\x -> [True, False])
意味着:对于列表的每个元素,我都希望保留它并丢弃它。 filterM
找出对每个列表元素执行此操作的所有可能组合。
这是为给定整数查找除数对的一种非常愚蠢的方法:
divisors:: Int -> [(Int,Int)]
divisors n = do
x <- [1 .. n]
y <- [1 .. n]
if x*y == n then return (x, y) else []
这个片段的意思应该是相当直截了当的。显然,这是解决这个特定问题的愚蠢方法。(在这种情况下可能有更有效的方法。)但是现在想象一些更复杂的问题,其中这个搜索非常复杂。这种用于搜索的成语可能非常有用。
另一个例子是从它的素数分解构造一个数字的所有除数(尽管是无序的):
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
inmapM 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 的回答中给出的精彩描述也适用于这里:
在 list monad 的上下文中,
mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]
是一个nondeterministic list map:映射一个非确定性函数的映射操作 - 这样产生一个替代结果的列表 - 在一个列表上,创建所有替代的可能结果列表。
列表操作的一个很好的例子是查询 HTML/XML 文档,例如jQuery。jQuery 是一个单子。让我们看一个例子:
$(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
list monad 所做的是结合这些步骤。monad 本身对 HTML/XML 一无所知。
在HXT
您以几乎相同的方式进行操作。较新HXT
的版本使用所谓的箭头而不是单子,但基本原理是相同的。
guard
当与ofMonadPlus
或者另一个 monad结合使用时,Monad 和列表操作会更加有趣。举个例子,让我们解决经典的口头算术难题:SEND + MORE = MONEY
. 每个字母必须代表一个唯一的数字,并且前导数字不能为零。
为此,我们将从StateT
and构造一个 monad 堆栈[]
:
import Control.Monad
import Control.Monad.List
import Control.Monad.State
-- Verbal Arithmetic monad
type VA a = StateT [Int] [] a
单子列表允许我们分支计算以搜索所有可能的方式,状态是可供选择的数字列表。我们可以通过给它所有可能的数字来运行这个 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)
。(当然代码可以进一步优化,但我希望它是简单的。)
更多谜题可以在这里找到:http: //www.primepuzzle.com/leeslatest/alphameticpuzzles.html。