【发布时间】:2016-11-03 22:08:59
【问题描述】:
考虑下面的 Haskell 语句:
mapM print ["1", "2", "3"]
确实,这会按顺序打印“1”、“2”和“3”。
问题:你怎么知道mapM会首先打印“1”,然后然后打印“2”,然后终于打印“3”。有没有保证它会这样做?或者它是如何在 GHC 内部实现的巧合?
【问题讨论】:
标签: haskell monads io-monad traversable
考虑下面的 Haskell 语句:
mapM print ["1", "2", "3"]
确实,这会按顺序打印“1”、“2”和“3”。
问题:你怎么知道mapM会首先打印“1”,然后然后打印“2”,然后终于打印“3”。有没有保证它会这样做?或者它是如何在 GHC 内部实现的巧合?
【问题讨论】:
标签: haskell monads io-monad traversable
如果您通过扩展mapM 的定义来评估mapM print ["1", "2", "3"],您将得到(忽略一些不相关的细节)
print "1" >> print "2" >> print "3"
您可以将print 和>> 视为无法进一步评估的IO 操作的抽象构造函数,就像无法进一步评估像Just 这样的数据构造函数一样。
print s的解释是打印s的动作,a >> b的解释是先执行a再执行b的动作。所以,解释
mapM print ["1", "2", "3"] = print "1" >> print "2" >> print "3"
是先打印1,再打印2,最后打印3。
这在 GHC 中是如何实际实现的完全是另一回事,您不必担心很长时间。
【讨论】:
print = putStrLn . show。你可以走得更远一点,但这至少解释了Show 的作用。
无法保证评估的顺序,但可以保证效果的顺序。欲了解更多信息,请参阅this answer that discusses forM.
你需要学会做出以下棘手的区分:
- 评估顺序
- 效果的顺序(也称为“动作”)
什么 forM、sequence 和类似的函数承诺的效果是 从左到右排序。例如,以下是 保证以它们出现的相同顺序打印字符 字符串...
Note: "forM 是 mapM,其参数被翻转。有关忽略结果的版本,请参阅 forM_。"
【讨论】:
初步说明:Reid Barton 和 Dair 的回答完全正确,完全涵盖了您的实际问题。我提到,因为在这个答案的中途,人们可能会有这样的印象,即它与他们相矛盾,但事实并非如此,当我们结束时会很清楚。很明显,是时候沉迷于一些语言律师了。
是否可以保证 [
mapM print] 将 [按顺序打印列表元素]?
是的,正如其他答案所解释的那样。在这里,我将讨论什么可以证明这种保证是合理的。
在这个时代,mapM 默认只是 traverse 专用于 monad:
traverse
:: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
mapM
:: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
既然如此,接下来我将主要关注traverse,以及我们对效果排序的期望与Traversable 类之间的关系。
就效果的产生而言,traverse 为遍历容器中的每个值生成一个Applicative 效果,并通过相关的Applicative 实例组合所有这些效果。这第二部分清楚地反映在sequenceA 的类型上,通过它,应用上下文可以说是从容器中分解出来的:
sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
-- sequenceA and traverse are interrelated by:
traverse f = sequenceA . fmap f
sequenceA = traverse id
The Traversable instance for lists例如是:
instance Traversable [] where
{-# INLINE traverse #-} -- so that traverse can fuse
traverse f = List.foldr cons_f (pure [])
where cons_f x ys = (:) <$> f x <*> ys
很明显,效果的组合和排序是通过(<*>) 完成的,所以让我们暂时关注它。以IO applicative functor 为例,我们可以看到(<*>)从左到右的排序效果:
GHCi> -- Superfluous parentheses added for emphasis.
GHCi> ((putStrLn "Type something:" >> return reverse) <*> getLine) >>= putStrLn
Type something:
Whatever
revetahW
(<*>),然而,从左到右排列效果by convention, and not for any inherent reason。正如the Backwards wrapper from transformers 所见证的那样,原则上,始终可以使用从右到左的顺序实现(<*>),并且仍然可以获得合法的Applicative 实例。在不使用包装器的情况下,也可以利用(<**>) from Control.Applicative 来反转排序:
(<**>) :: Applicative f => f a -> f (a -> b) -> f b
GHCi> import Control.Applicative
GHCi> (getLine <**> (putStrLn "Type something:" >> return reverse)) >>= putStrLn
Whatever
Type something:
revetahW
鉴于翻转Applicative 效果的顺序非常容易,人们可能想知道这个技巧是否会转移到Traversable。例如,假设我们实现...
esrevart :: Applicative f => (a -> f b) -> [a] -> f [b]
... 所以它就像traverse 用于列表,除了使用Backwards 或(<**>) 来翻转效果的顺序(我将把它作为练习留给读者)。 esrevart 会是 traverse 的合法实现吗?虽然我们可以通过尝试证明 identity and composition laws of Traversable 成立来解决这个问题,但这实际上是没有必要的:鉴于 Backwards f 对于任何适用的 f 也是适用的,在任何合法的 traverse 之后模式化的 esrevart 也将遵守Traversable 法律。 The Reverse wrapper,也是 transformers 的一部分,提供了这种反转的一般实现。
因此,我们得出结论,可能存在合法的Traversable 实例,它们的效果顺序不同。特别是,一个列表traverse 从尾部到头部对效果进行排序是可以想象的。不过,这并没有让这种可能性变得不那么奇怪。为了避免完全困惑,Traversable 实例通常使用普通的(<*>) 实现,并遵循构造函数用于构建可遍历容器的自然顺序,在列表的情况下相当于预期的从头到尾的顺序的影响。这种约定出现的一个地方是the DeriveTraversable extension 自动生成实例。
最后的历史记录。就Traversable 类而言,这次讨论最终是关于mapM,这在不久的过去将是一个可疑的相关性举措。 mapM 仅在去年才有效地被 traverse 包含,但它已经存在了更长的时间。例如,1996 年的Haskell Report 1.3,在Applicative 和Traversable 出现之前的几年(实际上甚至没有ap),为mapM 提供了以下规范:
accumulate :: Monad m => [m a] -> m [a]
accumulate = foldr mcons (return [])
where mcons p q = p >>= \x -> q >>= \y -> return (x:y)
mapM :: Monad m => (a -> m b) -> [a] -> m [b]
mapM f as = accumulate (map f as)
这里通过(>>=) 强制执行的效果顺序是从左到右的,没有其他原因,只是这样做是明智的。
P.S.:值得强调的是,虽然可以根据Monad 操作编写从右到左的mapM(例如,在此处引用的 Report 1.3 实现中,它只需要交换p 和 q 在 mcons 的右侧),对于 monads 没有一般的 Backwards 这样的东西。由于x >>= f 中的f 是一个从值创建效果的Monad m => a -> m b 函数,因此与f 关联的效果取决于x。因此,像(<*>) 这样的简单排序倒置甚至不能保证有意义,更不用说合法了。
【讨论】:
Data.Functor.Reverse 实现Traversable 实例并非易事,它基本上依赖于Backwards。值得一提的是,对于 Monad 类,没有什么像 Backwards 这样的。
(Traversable f) => Traversable (Reverse f) 的情况下,您既不能使用(<*>),也不能使用(未知)Traversable 的结构。)顺便说一下,我将添加一个指向@的链接987654403@。 [2] 这绝对值得一提;我会添加一个关于它的注释。 (我选择不包含关于Monad 的额外段落,并且该评论应该在其中的某个地方进行。)有用的提醒,谢谢。