考虑以下Haskell语句:
mapM print ["1", "2", "3"]
确实,这会打印" 1"," 2"," 3"按顺序。
问题:您如何知道mapM
首先打印" 1"和然后打印" 2",最后打印" 3"。有没有保证会这样做?或者它是如何在GHC深层实施的巧合?
答案 0 :(得分:10)
如果您通过扩展mapM print ["1", "2", "3"]
的定义来评估mapM
,那么您将会到达(忽略一些不相关的细节)
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中如何实际实施完全是另一回事,你不应该长期担心。
答案 1 :(得分:7)
评估顺序无法保证,但对效果的顺序有保证。有关详细信息,请参阅this answer that discusses forM
.
你需要学会做出如下的,狡猾的区别:
- 评估顺序
- 效果的顺序(a.k.a。“行动”)
醇>什么 forM,序列和类似函数的承诺是效果会 从左到右订购。例如,以下是 保证按照它们出现的顺序打印字符 字符串......
Note:“forM
为mapM
,其参数被翻转。对于忽略结果的版本,请参阅forM_
。”
答案 2 :(得分:3)
初步说明: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仿函数作为说明性示例,我们可以从左到右看到(<*>)
排序效果:
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
实例。在不使用包装器的情况下,还可以利用(<**>)
中的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
也适用,esrevart
图案traverse
任何合法的Traversable
也将遵循Traversable
法律。 The Reverse
wrapper,也是变形金刚的一部分,提供了此逆转的一般实现。
我们因此得出结论,可能存在合法排序不同的合法traverse
个实例。特别是,可以设想从尾部到头部排序效果的列表Traversable
。但这并没有使这种可能性变得更加奇怪。为了避免完全困惑,(<*>)
实例通常使用普通mapM
实现,并遵循构造函数用于构建可遍历容器的自然顺序,在列表的情况下,它等于预期的头部 - 效果的尾部排序。此约定出现的一个地方是the DeriveTraversable
extension自动生成实例。
最后的历史记录。根据{{1}}类来讨论这个最终约为Traversable
的讨论将是一个在不太遥远的过去具有可疑相关性的举动。 mapM
仅在去年有效归入traverse
,但它已经存在了更长时间。例如,1996年的Haskell Report 1.3,Applicative
和Traversable
之前的年份(事实上甚至不是ap
),为{{{{{}}提供了以下规范1}}:
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)
强制执行的效果排序是从左到右,除了它是明智之举之外没有其他原因。
PS:值得强调的是,虽然可以根据(>>=)
操作编写一个从右到左mapM
(例如,在此处引用的报告1.3实现中,它只需要在Monad
的右侧交换p
和q
,对于monad没有通用mcons
这样的东西。由于Backwards
中的f
是x >>= f
函数,可以根据值创建效果,因此与Monad m => a -> m b
相关联的效果取决于f
。因此,像x
一样简单的排序反转甚至不能保证有意义,更不用说合法了。