-1

可能重复:
什么是单子?

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

为了学习这个主题,我到处寻找都让我感到困惑。我真的找不到关于 Monad 是什么以及如何使用它们的简单定义。“单子是一种根据值和使用这些值的计算序列来构造计算的方法” - 嗯???

有人可以提供一个简单的定义,什么是 Haskell 中的 Monad,与它们相关的法律并举个例子吗?

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

3 回答 3

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 中的单子

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

(>=>) :: 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 aa,并执行应用程序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 定义的绑定运算符也可用于定义映射操作。由于这个原因,Monad 不需要是类 Functor 的实例。liftM在 Monad 库中调用 fmap 的等效操作。

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

所以 Monads Maybe 的实际定义变成:

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

在设计自己的 Monad 时,您可能会发现更容易,而不是尝试直接定义(>>=),将问题分成几部分并弄清楚如何映射和连接您的结构。拥有 map 和 join 也可以用于验证您的 Monad 是否定义良好,即它是否满足所需的法律。

单子定律

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

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

返回和加入的规律是:

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

前两个定律指定合并撤消换行。如果您将一个容器包装在另一个容器中,则 join 会返回原始容器。如果您使用包装操作映射容器的内容,则再次加入会返回您最初拥有的内容。最后一条定律是连接的结合性。如果您有三层容器,则通过从内部或外部合并可以获得相同的结果。

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

return a >>= f  = f a
m >>= return    = m
(m >>= f) >>= g = m >>= (\x -> f x >>= g) 
于 2012-08-30T15:28:31.283 回答
4

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

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

如果您没有数学思想的诀窍,这两个操作需要满足某些在这一点上可能会让您感到困惑的定律。从概念上讲,您使用 bind 在单子级别上操作值并返回以从“微不足道”的值创建单子值。例如,

getLine :: IO String,

所以你不能修改putStrLn这个String——因为它不是一个String,而是一个IO String

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

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

如果我们把getLine它放在 bind 的左边,我们可以让它更具体。

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

好的,因此getLine >>= putStrLn . (++ ". No problem after all!")将打印输入的行并添加额外的内容。右手边是一个函数,它接受 aString并产生一个IO ()- 这一点都不难!我们只是按类型。

为许多不同的类型定义了 Monad,例如Maybeand [a],它们在概念上的行为方式相同。

Just 2 >>= return . (+2)Just 4如您所料,会产生。请注意,我们必须在return这里使用,否则右侧的函数将匹配返回类型m b,而只是匹配b,这将是类型错误。它在 putStrLn 的情况下工作,因为它已经产生了一个IO东西,这正是我们的类型需要匹配的东西。(剧透:形状的表达foo >>= return . bar很愚蠢,因为每个Monad都是一个Functor。你能弄清楚那是什么意思吗?)

我个人认为,就直觉而言,这只是让您了解单子的主题,如果您想更深入地研究,您确实需要深入研究该理论。我喜欢先使用它们。您可以查找各种 Monad 实例的源代码,例如 List ( []) Monad 或MaybeHoogle 上的 Monad,并在具体实现上更加智能。一旦您对此感到满意,请尝试实际的单子定律并尝试对它们进行更理论的理解!

于 2012-08-29T17:10:54.463 回答
2

Typeclassopedia有一个部分 about (但请先阅读前面的部分 aboutfirst )。MonadFunctorApplicative

于 2012-08-30T08:48:34.513 回答