Monads - 定义,定律和例子

时间:2012-08-29 15:23:19

标签: haskell

  

可能重复:
  What is a monad?

我正在学习使用Haskell的函数式语言进行编程,在学习解析器时遇到了 Monads 。我之前从未听说过这些,所以我做了一些额外的学习,以了解它们是什么。

为了学习这个话题我到处看,只是让我感到困惑。我真的找不到Monad是什么以及如何使用它们的简单定义。 “monad是一种使用这些值来计算值和计算顺序的计算方法” - 呃???

有人可以提供一个简单的定义Monad在Haskell中的含义,与之相关的法则并举例说明吗?

  • 注意:我知道如何使用do语法,因为我已经查看了带有副作用的I / O操作和函数。

3 个答案:

答案 0 :(得分:6)

直觉

粗略的直觉是Monad是一种特殊的容器(Functor),你有两个可用的操作。一个包装操作return,它将单个元素放入容器中。将容器容器合并到单个容器中的操作join

return :: Monad m => a -> m a     
join   :: Monad m => m (m a) -> m a

所以Monad也许你有:

return :: a -> Maybe a     
return x = Just x

join :: Maybe (Maybe a) -> Maybe a
join (Just (Just x) = Just x
join (Just Nothing) = Nothing 
join Nothing        = Nothing

同样对于Monad [],这些操作被定义为:

return :: a -> [a]     
return x = [x]

join :: [[a]] -> [a]
join xs = concat xs

Monad的标准数学定义基于这些返回和连接运算符。但是在Haskell中,Monad类的定义用于替换绑定运算符以进行连接。

Haskell中的Monads

在函数式编程语言中,这些特殊容器通常用于表示有效的计算。类型Maybe a表示可能成功或可能不成功的计算,类型[a]表示非确定性计算。特别是我们对具有效果的函数感兴趣,即某些a->m b类型为Monad m。我们需要能够组成它们。这可以使用monadic组合或绑定操作符来完成。

(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
(>>=) :: Monad m => m a -> (a -> m b) -> m b

在Haskell中,后者是标准的。请注意,它的类型与应用程序运算符的类型非常相似(但使用翻转的参数):

(>>=)    :: Monad m => m a -> (a -> m b) -> m b
flip ($) ::              a -> (a ->   b) -> b

它需要一个有效的函数f :: a -> m b和一个计算mx :: m a返回类型为a的值,并执行应用程序mx >>= f。那么我们如何使用Monads呢?可以映射容器(Functors),在这种情况下,结果是计算中的计算,然后可以展平:

fmap f mx        :: m (m b)
join (fmap f mx) :: m b

所以我们有:

(mx >>= f) = join (fmap f mx) :: m b

要在实践中看到这一点,请考虑使用列表(非确定性函数)的简单示例。假设您有一个可能结果列表mx = [1,2,3]和一个非确定性函数f x = [x-1, x*2]。要计算mx >>= f,首先将mx与f映射,然后合并结果::

fmap f mx                = [[0,2],[1,4],[2,6]]    
join [[0,2],[1,4],[2,6]] = [0,2,1,4,2,6]

因为在Haskell中,绑定运算符(>>=)join更重要,因为后者的效率是从前者定义而不是相反。

join mx = mx >>= id

使用join和fmap定义的绑定操作符也可用于定义映射操作。因此,Monads不需要是类Functor的实例。对于fmap的等效操作在Monad库中称为liftM

liftM f mx = mx >>= \x-> return (f x) 

因此,Monads的实际定义可能变为:

return :: a -> Maybe a     
return x = Just x

(>>=)    :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>= f = Nothing 
Just x  >>= f = f x

对于Monad []:

return :: a -> [a]     
return x = [x]

(>>=)    :: [a] -> (a -> [b]) -> [b]
xs >>= f = concat (map f xs)
         = concatMap f xs    -- same as above but more efficient

在设计自己的Monads时,您可能会发现更容易,而不是尝试直接定义(>>=),将问题分成几部分并找出如何映射和连接结构的方法。拥有地图和联接也可用于验证您的Monad是否已经明确定义,因为它符合所需的法律。

Monad Laws

你的Monad应该是一个Functor,所以映射操作应该满足:

fmap id = id
fmap g . fmap f = fmap (g . f)

返回和加入的法律是:

join . return      = id
join . fmap return = id  
join . join = join . fmap join 

前两个法律规定合并撤销包装。如果您将容器包装在另一个容器中,则连接将返回原始容器。如果使用包装操作映射容器的内容,则再次加入会返回您最初的内容。最后的法则是联合的相关性。如果你有三层容器,你可以从内部或外部合并得到相同的结果。

再次,您可以使用bind而不是join和fmap。你得到的更少但(可以说是)更复杂的法则:

return a >>= f  = f a
m >>= return    = m
(m >>= f) >>= g = m >>= (\x -> f x >>= g) 

答案 1 :(得分:4)

Haskell中的monad是定义了两个操作的东西:

(>>=)  :: Monad m => m a -> (a -> m b) -> m b -- also called bind
return :: Monad m => a -> m a

这两项操作需要满足某些法律,如果你对狡猾的想法没有诀窍,那么在这一点上真的可能会让你感到困惑。从概念上讲,您使用bind来操作monadic级别的值并返回以从“普通”值创建monadic值。例如,

getLine :: IO String

因此您无法修改putStrLnString - 因为它不是String而是IO String

好吧,我们有一个IO Monad方便,所以不用担心。我们所要做的就是使用bind来做我们想要的。让我们看看IO Monad中的绑定是什么样的:

(>>=) :: IO a -> (a -> IO b) -> IO b

如果我们将getLine放在bind的左侧,我们可以使它更具体。

(>>=) :: IO String -> (String -> IO b) -> IO b

好的,所以getLine >>= putStrLn . (++ ". No problem after all!")会打印输入的行,并添加额外的内容。右侧是一个需要String并生成IO ()的函数 - 这根本不难!我们只是选择类型。

为许多不同的类型定义了Monads,例如Maybe[a],并且它们在概念上以相同的方式运行。

正如您所料,

Just 2 >>= return . (+2)会产生Just 4。请注意,我们必须在此处使用return,否则右侧的函数与返回类型m b匹配,而只是b,将是一个类型错误。它适用于putStrLn的情况,因为它已经产生了IO的东西,这正是我们的类型需要匹配的东西。 (剧透:形状foo >>= return . bar的表达是愚蠢的,因为每个Monad都是Functor。你能弄明白这意味着什么吗?)

我个人认为,只要直觉能让你了解monad的主题,如果你想深入探讨,你真的需要深入了解这个理论。我喜欢先使用它们。您可以查找各种Monad实例的源代码,例如Hoogle上的List([])Monad或Maybe Monad,并在确切的实现方面更加智能。一旦你对此感到满意,就要了解实际的monad定律并尝试对它们有更多的理论认识!

答案 2 :(得分:2)

Typeclassopedia有一个关于Monad的部分(但请先阅读前面有关FunctorApplicative的部分。)