使用Haskell的`mapM`执行的顺序

时间:2016-11-03 22:08:59

标签: haskell monads io-monad traversable

考虑以下Haskell语句:

mapM print ["1", "2", "3"]

确实,这会打印" 1"," 2"," 3"按顺序。

问题:您如何知道mapM 首先打印" 1"和然后打印" 2",最后打印" 3"。有没有保证会这样做?或者它是如何在GHC深层实施的巧合?

3 个答案:

答案 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.

  

你需要学会做出如下的,狡猾的区别:

     
      
  1. 评估顺序
  2.   
  3. 效果的顺序(a.k.a。“行动”)
  4.         

    什么   forM,序列和类似函数的承诺是效果会   从左到右订购。例如,以下是   保证按照它们出现的顺序打印字符   字符串......

Note:“forMmapM,其参数被翻转。对于忽略结果的版本,请参阅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(<**>)来翻转效果的排序(我会将其作为读者练习) )。 esrevarttraverse的合法实施吗?虽然我们可能会通过尝试证明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.3ApplicativeTraversable之前的年份(事实上甚至不是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的右侧交换pq,对于monad没有通用mcons这样的东西。由于Backwards中的fx >>= f函数,可以根据值创建效果,因此与Monad m => a -> m b相关联的效果取决于f。因此,像x一样简单的排序反转甚至不能保证有意义,更不用说合法了。