4

我只是对列表和单子完全感到困惑,所以也许我的问题不正确或非常幼稚。我在这里看到了使用 mapM_ func 的方法:

mapM_ print [1, 2, 3, 4]

但我不确切知道它是如何工作的,并且想知道如何以这样的方式做到这一点:

x <- [1, 2, 3]
print x

或者,如果我理解正确:

[1, 2, 3] >>= print

我知道 [1, 2, 3] 有 type[a]并且 print 有 type Show a => a -> IO ()。我也明白,为了使用 monad List,我们需要List a左边的类型和a -> List b右边的类型。我对吗?你能帮我解决这个问题吗?

升级版。感谢@MathematicalOrchid 解释 mapM_ 的工作原理。从我的角度来看,我想解释真正的问题不是在不同的行中打印任何结果,而是以 monad List 提供的方式执行一些 monadic 操作(因为现在我在 OpenGL 周围徘徊)。但我知道误解的根源在于混合单子。

UPD2。谢谢大家的回答。我为这个模糊的问题道歉。我不完全知道我需要什么答案以及问题是什么。那是因为我不了解一些基础知识。所以现在很难选择“正确的答案”,因为每个答案都与我正在寻找的东西有点和平。我决定选择最接近我想要的(虽然现在不是最有用的)。

4

5 回答 5

13

您似乎在这里混淆了几件事。(特别是,列表形成了一个 monad,而 I/O 形成了一个不同的 monad。)我将尝试澄清这一点......

首先,该print函数获取任何可显示的内容并将其写入标准输出,然后是换行符。所以print [1, 2, 3]工作得很好,但显然将所有内容都写在同一行。print要在单独的行上写东西,我们需要对每个项目单独调用。到现在为止还挺好。

map函数将函数应用于列表的每个元素。所以map print [1, 2, 3]将适用print于列表中的每个项目。但是,结果是 I/O 操作列表。这不是我们所追求的。我们想要执行这些操作,而不是列出它们。

做到这一点的方法是使用>>运算符,它将两个 I/O 操作链接在一起(前提是您对它们的结果不感兴趣 - 并且打印一些东西不会返回任何有趣的东西)。所以foldr (>>) (return ())将把你的 I/O 动作列表变成一个单一的 I/O 动作。这个函数实际上已经定义好了;它被称为sequence

但是,map+sequence是一种常见的组合,也已经被定义;它被称为mapM_。(如果您想保留结果,还有mapM,没有下划线。但打印不会返回任何内容,所以没有必要。)


现在,这就是为什么mapM_有效。现在你问为什么其他几种方法都行不通......

x <- [1, 2, 3]
print x

这根本行不通。第一行在列表 monad 中。但第二行在 I/O monad 中。你不能那样做。(你会得到一个相当莫名其妙的类型检查器错误。)我可能应该指出这是 Haskell 所谓的“do-notation”,上面的片段需要do前面的关键字才能真正成为有效的语法:

do
  x <- [1, 2, 3]
  print x

无论哪种方式,它仍然无法正常工作。它几乎做了什么map print [1, 2, 3],但不完全是。(正如我所说,它不会进行类型检查。)

您还建议[1, 2, 3] >>= print,这与前面的代码片段相同。(实际上,编译器前者转换为后者。)出于同样的原因,原版不进行类型检查,而本版也不进行类型检查。

这有点像尝试将数字添加到矩阵中。数字是可加的东西。矩阵是可添加的东西。但是您不能将一个添加到另一个,因为它们不一样。如果这是有道理的。

于 2012-09-17T15:15:27.680 回答
10

你想要的东西不能这样工作,因为你试图将两个单子混合在一起:

do x <- [1,2,3]
   print x

具体来说,您正在混合 theIO[]monad。在 do-notation 中,所有语句都应该具有m a某些 Monad的类型m。但是在上面的代码中,第一条语句的类型是 type[Integer]而第二条语句的类型是IO ()

要获得您想要的效果,您应该使用ListTmonad 转换器。Monad 转换器允许在堆栈中以特定顺序将 monad 混合在一起,并根据需要组合它们的效果。

import Control.Monad.Trans
import Control.Monad.Trans.List

value = do x <- ListT (return [1,2,3])
           lift (print x)

这将返回一个类型的值ListT IO Integer。要IO从此转换器中获取计算,请使用runListT. 这将返回一个类型的值IO [Integer]。这将输出:

GHCI> runListT value
1
2
3
[(),(),()]

这相当于mapM print [1,2,3]. 要丢弃列表并获得mapM_ print [1,2,3]您可以使用的效果voidfrom Control.Monad

GHCI> void . runListT $ value
1
2
3
于 2012-09-17T15:34:39.403 回答
5

您可以使用按顺序sequence_执行操作:IO

sequence_ $ [1, 2, 3] >>= (\x -> [print x])

但我认为mapM_要清楚得多。

于 2012-09-17T15:16:30.503 回答
4

我不会准确回答你的问题,因为我认为这个问题本身有点误导。特别是,在这里使用mapM或类似的东西是正确的。对这个任务使用 do 符号只会让它变得更加复杂,而且我讨厌告诉人们不应该做的事情。但我会为您提供一种替代方案,您可能会发现它更容易消化。

如果您来自命令式背景(即您熟悉 C、Java、Python 等),那么您可能会发现它更易于使用,forM而不是mapM. 语法是

forM <list of things> <action to perform for each thing>

即它就像一个for-each 循环!例如:

ghci> import Control.Monad
ghci> forM [1,2,3] print
1
2
3
[(),(),()]

事物列表是[1,2,3],每件事要执行的动作是print。注意到最后的返回值了吗?那是因为每次调用print返回(),最后都被收集在一起。如果您不想要使用forM_而不是返回值forM,如下所示:

ghci> forM_ [1,2,3] print
1
2
3

你准备好接受秘密了吗?函数forMforM_只是mapM并且mapM_参数相反,即:

forM list action = mapM action list

我经常forM在我的代码中使用它,因为它会引起人们对函数的关注,而不是你想要的列表。当 then 函数跨越多行时,它看起来也更整洁。

于 2012-09-17T16:03:25.900 回答
1

这可能是对如何工作的最简单的解释mapM_

main = foldr1 (>>) (map print [1, 2, 3])

也就是说,print应用于每个列表成员并使用 连接结果>>,所以首先你得到

main = foldr1 (>>) [print 1, print 2, print 3]

最后你得到

main = print 1 >> print 2 >> print 3 

更准确的解释是这样的:

main = foldr (>>) (return ()) (map print [1, 2, 3])

所以最后你得到

main = print 1 >> print 2 >> print 3 >> return ()

return ()部分让函数使用空列表 -foldr1只是以相同的方式在空列表上崩溃headtail执行。

于 2012-09-18T11:49:49.877 回答