ghci> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
为什么第二个参数是(a -> m b)
而不是(m a -> m b)
甚至是(a -> b)
?什么是概念上关于需要此签名的Monads?具有替代签名t a -> (t a -> t b) -> t b
的类型类是否有意义。 t a -> (a -> b) -> t b
?
答案 0 :(得分:16)
monad的一个更对称的定义是Kleisli组合子,对于monad来说基本上是(.)
:
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
它可以替换monad定义中的(>>=)
:
f >=> g = \a -> f a >>= g
a >>= f = const a >=> f $ ()
答案 1 :(得分:12)
在Haskell中通常根据Monad
和return
定义(>>=)
:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
但是,我们可以使用这个等效的定义,它更接近原始的数学定义:
class Monad m where
fmap :: (a -> b) -> m a -> m b
join :: m (m a) -> m a
return :: a -> m a
正如您所看到的,(>>=)
的不对称性已被join
的不对称性所取代,m (m a)
将m
的两层m a
带入“{1}}”只是fmap
。
您还可以看到t a -> (a -> b) -> t b
的签名与您的Functor
匹配,但参数相反。这是表征类型Monad
的操作,它比fmap
严格弱:每个monad都可以成为一个仿函数,但不是每个仿函数都可以成为monad。
这在实践中意味着什么?好吧,当转换只是一个仿函数的东西时,你可以使用fmap f [1, 2, 3]
来转换仿函数“内”的值,但这些值永远不会影响仿函数本身的“结构”或“效果”。然而,对于monad,这种限制被解除了。
作为一个具体的例子,当你f
时,你知道无论[1, 2, 3] >>= g
做什么,结果列表都会有三个元素。但是,当您执行g
时,fmap f readLn
可以将这三个数字中的每一个转换为包含任意数量值的列表。
同样,如果我readLn >>= g
,我知道除了读取一行之外,它不能执行任何I / O操作。另一方面,如果我执行g
,IO
可以检查已读取的值,然后使用它来决定是打印出消息,还是读取 n 更多行,或在{{1}}内执行任何其他操作。
答案 2 :(得分:11)
这个问题的一个非常好的答案是Brian Beckman(在我看来)对monad的精彩介绍:Don't fear the Monad
你也可以看看“了解你一个哈克尔”这个很好的章节:A Fistful of Monads。 这也很好地解释了它。
如果你想要务实:必须采用这种方式来实现“do”-laague的功能;) - 但Brian和Lipovaca对此的解释要比那更好(和更深);)
PS:替代方案:
第一个是或多或少地将第二个参数应用于第一个。
第二种选择几乎是Functor-type class的fmap
- 只有切换参数(并且每个Monad都是一个Functor - 即使Haskell类型类没有约束它 - 但它应该 - 但是这是另一个话题;))
答案 3 :(得分:10)
嗯,(>>=)
的类型可以方便地使用do
符号,但有些不自然。
(>>=)
的目的是在monad中使用一个类型,并使用该类型的参数在monad中创建其他类型的函数,然后通过提升函数并展平额外的内容来组合它们层。如果你查看join
中的Control.Monad
函数,它只执行展平步骤,所以如果我们把它作为基本操作,我们就可以这样写(>>=)
:
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
m >>= k = join (fmap k m)
但请注意fmap
的参数反转顺序。如果我们考虑Identity
monad,这只是一个围绕普通值的newtype包装,那么这个原因就变得清晰了。忽略新类型,fmap
Identity
是函数应用程序而join
什么都不做,因此我们可以将(>>=)
识别为应用程序运算符,并将其参数反转。比较此运算符的类型,例如:
(|>) :: a -> (a -> b) -> b
x |> f = f x
非常相似的模式。因此,为了更清楚地了解(>>=)
类型的含义,我们将改为查看(=<<)
中的Control.Monad
,它在(<*>)
中定义其他订单。将其与Control.Applicative
,fmap
,($)
和(->)
进行比较,并记住($) :: (a -> b) -> ( a -> b)
fmap :: (Functor f) => (a -> b) -> (f a -> f b)
(<*>) :: (Applicative f) => f (a -> b) -> (f a -> f b)
(=<<) :: (Monad m) => (a -> m b) -> (m a -> m b)
是正确关联的,并添加多余的括号:
Functor
所以这四个本质上都是函数应用程序,后三者是“提升”函数以处理某些函数类型的值的方法。它们之间的差异对于简单值fmap :: (Functor f) => (a -> b) -> (f a -> f b)
和基于它的两个类如何不同至关重要。从宽松的意义上讲,类型签名可以理解如下:
a -> b
这意味着给定一个普通函数f a
,我们可以将它转换为在f b
和f
类型上执行相同操作的函数。所以这只是一个简单的转换,不能改变或检查(<*>) :: (Applicative f) => f (a -> b) -> (f a -> f b)
的结构,无论它是什么。
fmap
就像f
一样,除了它需要一个本身已经在f
中的函数类型。函数类型仍然无视(<*>)
的结构,但f
本身必须在某种意义上组合两个(=<<) :: (Monad m) => (a -> m b) -> (m a -> m b)
结构。因此,这可以改变和检查结构,但只能以结构本身决定的方式,独立于值。
m
这是一个深刻的,根本性的转变,因为现在我们采用创建某个m a
结构的函数,该结构与(=<<)
参数中已存在的结构相结合。所以t a -> (t a -> t b) -> t b
不仅可以改变上面的结构,而且被提升的功能可以根据值创建新的结构。但是仍然存在一个很大的局限性:该函数只接收一个普通值,因而无法检查整体结构;它只能检查一个位置,然后决定放在那里的结构类型。
所以,回到你的问题:
使用替代签名
t a -> (a -> b) -> t b
的类型类是否有意义。($)
?
如果您按照上面的“标准”顺序重写这两种类型,您可以看到第一种只有fmap
具有特殊类型,而第二种是contramap :: (Contravariant f) => (a -> b) -> (f b -> f a)
。然而,是其他有意义的变体!这里有几个例子:
newtype Flipped b a = Flipped (a -> b)
这是一个逆变仿函数,它“向后”工作。如果首先看不到类型,请考虑类型(<<=) :: (Comonad w) => (w a -> b) -> (w a -> w b)
以及您可以使用它做什么。
(=<<)
这是monad的双重 - 而(<<=)
的参数只能检查局部区域并产生一块结构放置在那里,(<<=)
的参数可以检查全局结构并产生一个汇总值。 {{1}}本身通常在某种意义上扫描结构,从每个视角获取摘要值,然后重新组合它们以创建新结构。
答案 4 :(得分:5)
m a -> (a -> b) -> m b
是Functor.fmap
的行为,非常有用。但它比>>=
更有限。例如。如果您处理列表,fmap
可以更改这些元素及其类型,但不列表的长度。另一方面,>>=
可以轻松完成此任务:
[1,2,3,4,5] >>= (\x -> replicate x x)
-- [1,2,2,3,3,3,4,4,4,4,5,5,5,5,5]
m a -> (m a -> m b) -> m b
不是很有趣。这只是具有反向参数的函数应用程序(或$
):我有一个函数m a -> m b
并提供参数m a
,然后我得到m b
。
<强> [编辑] 强>
奇怪的是,没有人提到第四个可能的签名:m a -> (m a -> b) -> m b
。这实际上也有意义,并导致Comonads
答案 5 :(得分:4)
我打算通过倒退来解决这个问题。
<强>简介强>
在基本级别,我们有值:类型为Int
,Char
,String
*等的内容。通常这些类型具有多态类型a
,这只是一个类型变量。
有时在上下文中使用值非常有用。在sigfpe的博客之后,我喜欢将其视为花哨的价值。例如,如果我们的某些内容可能是Int
但可能不是任何内容,则它位于Maybe
上下文中。如果某个内容是Int
或String
,则它位于Either String
上下文中。如果一个值可能是几个差异Char
之一,则它在非确定性上下文中,在haskell中是一个列表,即[Char]
。
(有点高级:使用类型构造函数引入了一个新的上下文,其中有* -> *
种类。)
<强>函子强>
如果你有一个奇特的值(上下文中的值),那么能够将一个函数应用于它是很好的。当然,您可以编写特定的函数来为每个不同的上下文(Maybe
,Either n
,Reader
,IO
等)执行此操作,但我们希望使用在所有这些情况下相同的界面。这由Functor
类型类提供。
Functor唯一的方法是fmap
,其类型为(a -> b) -> f a -> f b
。这意味着,如果您有一个函数,从键入到类型b ,您可以将它应用于花式来获取花哨b ,其中b
在中与a
完全相同。
g' = fmap (+1) (g :: Maybe Int) -- result :: Maybe Int
h' = fmap (+1) (h :: Either String Int) -- result :: Either String Int
i' = fmap (+1) (i :: IO Int) -- result :: IO Int
此处g'
,h'
和i'
与g
,h
和i
具有完全相同的上下文。上下文不会改变,只会改变其中的值。
(下一步是Applicative
,我现在暂时跳过这一步。
<强>单子强>
有时仅将函数应用于奇特值是不够的。有时您希望基于该值分支。也就是说,您希望新上下文依赖于当前上下文和当前值。您可能想要这样的示例:
safe2Div :: Int -> Maybe Int
safe2Div 0 = Nothing
safe2Div n = Just (2 `div` n)
如何将此应用于Maybe Int
?您无法使用fmap
,因为
fmap safe2Div (Just 0) :: Maybe (Maybe Int)
看起来更复杂。*你需要一个函数Maybe Int -> (Int -> Maybe Int) -> Maybe Int
或许这个:
printIfZ :: Char -> IO ()
printIfZ 'z' = putStrLn "z"
printIfZ _ = return ()
如何将此应用于IO Char
?同样,您希望函数IO Char -> (Char -> IO ()) -> IO ()
根据值执行适当的IO操作。
通常,这会为您提供类型签名
branchContext :: f a -> (a -> f b) -> f b
这正是Monad
的{{1}}方法提供的功能。
我建议Typeclassopedia了解更多相关信息。
编辑:对于(>>=)
,不需要为此类型类,因为它只是翻转函数应用程序,即t a -> (t a -> t b) -> t b
。这是因为它完全不依赖于上下文结构或内部值。
* - 忽略flip ($)
是String
的类型同义词。无论如何,这仍然是一个价值。
* - 它看起来更复杂,但事实证明[Char]
和(>>=) :: m a -> (a -> m b) -> m b
给你完全相同的力量。 join :: m (m a) -> m a
通常在实践中更有用。
答案 6 :(得分:3)
关于Monads需要签名的概念是什么?
基本上,一切。 Monads都是关于这种特殊类型的签名,至少它们来自于一种看待它们的方式。
“绑定”类型签名m a -> (a -> m b) -> m b
基本上说:“我有a
,但是它被卡在Monad m
中。我有这个monadic函数会带我从a
到m b
。我不能只将a
应用于此函数,因为我没有a
,而是{{1}所以让我们发明一个类似于m a
的函数并将其命名为$
。任何Monad基本上都必须告诉我(定义)如何从{{展开>>=
1}}这样我就可以在它上面使用这个函数a
。“
答案 7 :(得分:0)
每个monad都连接到一些“adjunction”,这是一对彼此部分相反的地图。例如,考虑“goInside”和“goOutside”对。你从里面开始,然后goOutside。你现在在外面。如果你去了里面,你最终会回到里面。
注意内部和外部是如何相关的 - 通过这对来回映射物体或人的功能。
Bind是一个函数,它在monad中“取值”,将它放在monad之外的上下文中,将函数放入monad中,然后在monad中生成一个值,这样你就可以放心了处于继续运营的正确起点。
这允许我们随意切换两个上下文 - 在monad之外的“纯粹”(我在模糊的,暗示性的意义上使用它)以及它内部的monadic上下文。