绑定函数中的不对称

时间:2011-09-06 11:49:55

标签: haskell monads

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

8 个答案:

答案 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中通常根据Monadreturn定义(>>=)

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操作。另一方面,如果我执行gIO可以检查已读取的值,然后使用它来决定是打印出消息,还是读取 n 更多行,或在{{1}}内执行任何其他操作。

答案 2 :(得分:11)

这个问题的一个非常好的答案是Brian Beckman(在我看来)对monad的精彩介绍:Don't fear the Monad

你也可以看看“了解你一个哈克尔”这个很好的章节:A Fistful of Monads。 这也很好地解释了它。

如果你想要务实:必须采用这种方式来实现“do”-laague的功能;) - 但Brian和Lipovaca对此的解释要比那更好(和更深);)

PS:替代方案: 第一个是或多或少地将第二个参数应用于第一个。 第二种选择几乎是Functor-type classfmap - 只有切换参数(并且每个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.Applicativefmap($)(->)进行比较,并记住($) :: (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 bf类型上执行相同操作的函数。所以这只是一个简单的转换,不能改变或检查(<*>) :: (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 bFunctor.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)

我打算通过倒退来解决这个问题。

<强>简介

在基本级别,我们有:类型为IntCharString *等的内容。通常这些类型具有多态类型a,这只是一个类型变量。

有时在上下文中使用值非常有用。在sigfpe的博客之后,我喜欢将其视为花哨的价值。例如,如果我们的某些内容可能是Int但可能不是任何内容,则它位于Maybe上下文中。如果某个内容是IntString,则它位于Either String上下文中。如果一个值可能是几个差异Char之一,则它在非确定性上下文中,在haskell中是一个列表,即[Char]

(有点高级:使用类型构造函数引入了一个新的上下文,其中有* -> *种类。)

<强>函子

如果你有一个奇特的值(上下文中的值),那么能够将一个函数应用于它是很好的。当然,您可以编写特定的函数来为每个不同的上下文(MaybeEither nReaderIO等)执行此操作,但我们希望使用在所有这些情况下相同的界面。这由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'ghi具有完全相同的上下文。上下文不会改变,只会改变其中的值。

(下一步是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函数会带我从am b。我不能只将a应用于此函数,因为我没有a,而是{{1}所以让我们发明一个类似于m a的函数并将其命名为$。任何Monad基本上都必须告诉我(定义)如何从{{展开>>= 1}}这样我就可以在它上面使用这个函数a。“

答案 7 :(得分:0)

每个monad都连接到一些“adjunction”,这是一对彼此部分相反的地图。例如,考虑“goInside”和“goOutside”对。你从里面开始,然后goOutside。你现在在外面。如果你去了里面,你最终会回到里面。

注意内部和外部是如何相关的 - 通过这对来回映射物体或人的功能。

Bind是一个函数,它在monad中“取值”,将它放在monad之外的上下文中,将函数放入monad中,然后在monad中生成一个值,这样你就可以放心了处于继续运营的正确起点。

这允许我们随意切换两个上下文 - 在monad之外的“纯粹”(我在模糊的,暗示性的意义上使用它)以及它内部的monadic上下文。