在新行中打印列表元素

4

我对列表和单子完全感到困惑,所以也许我的问题不正确或者很天真。我在这里看到了使用mapM_ func的方法。

mapM_ print [1, 2, 3, 4]

但我不知道它的具体工作原理,想知道如何像这样实现:

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

或者,如果我理解正确:
[1, 2, 3] >>= print

我理解[1, 2, 3]的类型是[a],而print的类型是Show a => a -> IO ()。此外,我明白使用Monad List需要左边的类型是List a,右边带有类型为a -> List b的函数。我的理解正确吗?你需要帮忙吗? 更新。感谢@MathematicalOrchid对mapM_如何工作的解释。从我的角度来看,真正的问题不在于在不同行打印任何结果,而在于以Monad List提供的方式执行一些monadic操作(因为现在我正在处理OpenGL的东西)。但是我知道误解的根源在于混淆了monads。 更新2。感谢大家回答。对于这个有点含糊的问题我感到抱歉。我并不完全知道我需要什么样的答案和问题是什么。这是因为我没有理解一些基础知识。所以现在很难选择“正确的答案”,因为每个答案都包含了我要找的一个小部分。我已经决定选择最接近(尽管现在不是最有用的)我想要的内容。

1
forM_ 可能看起来更加熟悉。它只是将 mapM_ 的参数反转了一下。尝试使用 forM_ [1, 2, 3] print - jtobin
1
值得一提的是,您正在使用两个单子:ListIO。您的 >>= 不起作用的原因是您试图混合这两个单子。 - stusmith
@stusmith:是的,我也刚明白 :) - pkuderov
5个回答

12
你好像对多个概念有些混淆。(特别是列表形成了一个单子,而 I/O 形成了另一个单子。)我会试着解决这个问题...
首先,`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

这完全不起作用。第一行在列表单子中,但第二行在I/O单子中。你不能那样做。(您将获得一个相当令人困惑的类型检查错误。) 我应该指出,这是Haskell的所谓“do-notation”,上面的片段需要在前面加上do关键字才能实际上是有效的语法:

do
  x <- [1, 2, 3]
  print x
无论如何,它仍然不起作用。它几乎做到了map print [1, 2, 3]所做的事情,但并不完全相同。(就像我说的那样,它无法通过类型检查。)
您还建议使用[1, 2, 3] >>= print,它与前面的代码片段相同。(实际上,编译器会将前者转换为后者。)原始版本无法通过类型检查,这个也同样无法通过,因为相同的原因。
这有点像尝试将数字添加到矩阵中。数字是可以加的东西。矩阵也是可以加的东西。但是你不能将它们相加,因为它们不一样。如果这听起来有点抽象,请见谅。

非常清晰的解释!但是还有一个问题,请问是否有任何方法可以混合单子(从一个单子跳到另一个单子)?目前我至少看到两种方法可以“独立”地执行某些操作-通过映射map函数(或其单子反射)或将操作放入List单子中。我是正确的吗? - pkuderov
1
@pkuderov 对于某些特定的单子对 mn,存在从 m 跳转到 n 的方法,但列表和 IO 不属于这样一对。相反,通常的技术是使用单子转换器创建一个新的单子,该单子具有您想要在其中跳转的两个单子的特征。没有单子变换器允许您像列表那样添加 IO 效果,但有一种变换器可以允许您向 IO 添加非确定性(即类似列表的特征)。此处 有一种变换器可用于实现该功能。 - Daniel Wagner

9

你想要的方式行不通,因为你试图将两个单子混合在一起:

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

具体来说,您正在混合IO[]单子。在do-notation中,所有语句的类型都应为某个单子mm a。但在上面的代码中,第一个语句的类型为[Integer],而第二个语句的类型为IO ()
为了获得所需的效果,您应该使用ListT单子转换器。Monad transformers允许以特定顺序将单子堆叠在一起,并根据需要组合它们的效果。
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]的效果,可以使用Control.Monad中的void

GHCI> void . runListT $ value
1
2
3

感谢 ListT。我现在不会使用 Monad 变换器 - 我的能力还没有达到那个水平。但这确实是我正在寻找的东西之一。 - pkuderov
@pkuderov,我很高兴能够帮助你。我知道这对你来说可能有点高级,但我认为我应该加入它,因为它正好解决了你正在尝试解决的问题类型。 - is7s

4
您可以使用sequence_按顺序执行IO操作:
sequence_ $ [1, 2, 3] >>= (\x -> [print x])

但我认为mapM_更加清晰易懂。


只是不希望任何人认为这是一个明智的建议。虽然“sequence”对于将动作列表制作为单个动作很方便。 - stusmith
此外,sequence_ $ map print [1,2,3] - Will Ness

3
我不会精确回答你的问题,因为我认为这个问题本身有些误导性。特别是,在这里使用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_函数只是将参数顺序颠倒的mapMmapM_函数,也就是说:

forM list action = mapM action list

我经常在我的代码中使用forM,因为它能够吸引注意力到函数上而不是列表,这通常是我们想要的。同时,当函数跨越多行时,它看起来更整洁。

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 一样崩溃。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接