绑定函数中的不对称

foo*_*bar 11 monads haskell

ghci> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

为什么第二个论点(a -> m b)不是(m a -> m b)甚至是(a -> b)?这是什么概念有关单子需要此签名?具有替代签名的类型类是否有意义t a -> (t a -> t b) -> t b.t a -> (a -> b) -> t b

fuz*_*fuz 16

monad的更对称定义是Kleisli组合子,它基本上(.)用于monad:

(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
Run Code Online (Sandbox Code Playgroud)

它可以取代(>>=)monad的定义:

f >=> g = \a -> f a >>= g

a >>= f = const a >=> f $ ()
Run Code Online (Sandbox Code Playgroud)

  • 关于`(> =>)`的好处在于它使得"返回"的"左侧身份"和"右侧身份"法律变得更加明智. (8认同)
  • `(> =>)`使关联法更加明智. (7认同)

Stu*_*ook 12

这是在Haskell通常定义Monad在以下方面return(>>=):

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a
Run Code Online (Sandbox Code Playgroud)

但是,我们可以使用这个等价的定义,它更接近原始的数学定义:

class Monad m where
    fmap :: (a -> b) -> m a -> m b
    join :: m (m a) -> m a
    return :: a -> m a
Run Code Online (Sandbox Code Playgroud)

正如你所看到的那样,不对称性(>>=)已被不对称性所取代,这种不对称性joinm (m a)"两层" m变为"正常" m a.

您还可以看到fmap匹配的签名t a -> (a -> b) -> t b,但参数相反.这是特征化类型类的操作Functor,它严格地弱于Monad:每个monad都可以成为一个仿函数,但并不是每个仿函数都可以成为一个monad.

这在实践中意味着什么?好吧,当转换只是一个仿函数的东西时,你可以fmap用来转换仿函数"内"的值,但这些值永远不会影响仿函数本身的"结构"或"效果".然而,对于monad,这种限制被解除了.

作为一个具体的例子,当你这样做时fmap f [1, 2, 3],你知道无论如何f,结果列表将有三个元素.但是,当您这样做时[1, 2, 3] >>= g,可以g将这三个数字中的每一个转换为包含任意数量值的列表.

同样,如果我这样做fmap f readLn,我知道它除了读取一行之外不能执行任何I/O操作.如果我这样做readLn >>= g,在另一方面,有可能为g检查所读取的值,然后用它来决定是否打印出一个消息,或者阅读ñ更多的线,或做其他任何内可能IO.


Car*_*ten 11

Brian Beckman在(在我看来)对monad的精彩介绍中给出了一个非常好的答案:不要害怕Monad

你也可以看看"了解你一个哈克尔 "这个很好的章节:一个充满莫纳德的东西.这也很好地解释了它.

如果你想要务实:它必须是这样才能让'do'-laague具功能继续下去;) - 但Brian和Lipovaca解释它比那更好(和更深);)

PS:对你的选择:第一个或多或少是第二个参数的应用到第一个.第二种选择几乎fmapFunctor类型 - 只有切换参数(并且每个Monad都是一个Functor - 即使Haskell类型类没有约束它 - 但它应该 - 但这是另一个主题;) )

  • 如果标题是"(不要害怕)Monad",会更有趣. (2认同)

C. *_*ann 10

嗯,这种类型(>>=)对于desugaring do符号很方便,但在某种程度上不自然.

目的(>>=)是在monad中使用一个类型,并使用该类型的参数在monad中创建其他类型的函数,然后通过提升函数并展平额外的图层来组合它们.如果查看join函数Control.Monad,它只执行展平步骤,所以如果我们将它作为基本操作,我们可以这样写(>>=):

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
m >>= k = join (fmap k m)
Run Code Online (Sandbox Code Playgroud)

但请注意,参数的反转顺序为fmap.如果我们考虑Identitymonad,这个原因就变得清晰了,monad只是普通值的新类型包装器.忽略newtypes,fmapfor Identity是函数应用程序并且join什么都不做,所以我们可以认为(>>=)它是一个应用程序操作符,它的参数被反转.比较此运算符的类型,例如:

(|>) :: a -> (a -> b) -> b
x |> f = f x
Run Code Online (Sandbox Code Playgroud)

一个非常相似的模式.因此,为了更清楚地了解(>>=)类型的含义是什么,我们将改为查看(=<<),其中定义了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)
Run Code Online (Sandbox Code Playgroud)

因此,所有这四个本质上都是函数应用程序,后三个是"提升"函数以处理某些函数类型的值的方法.它们之间的差异对于简单值Functor以及基于它的两个类如何不同至关重要.从宽松的意义上讲,类型签名可以解读如下:

fmap :: (Functor f) => (a -> b) -> (f a -> f b)
Run Code Online (Sandbox Code Playgroud)

这意味着给定一个普通函数a -> b,我们可以将它转换为在类型f a和函数上执行相同操作的函数f b.因此,它只是一个简单的转换,不能改变或检查结构f,无论它是什么.

(<*>) :: (Applicative f) => f (a -> b) -> (f a -> f b)
Run Code Online (Sandbox Code Playgroud)

就像fmap,除了它需要一个本身已经存在的函数类型f.函数类型仍然无视结构f,但在某种意义上(<*>)它本身必须结合两个f结构.因此,这可以改变和检查结构,但仅以结构本身确定的方式,独立于值.

(=<<) :: (Monad m) => (a -> m b) -> (m a -> m b)
Run Code Online (Sandbox Code Playgroud)

这是一个深刻的,根本性的转变,因为现在我们采用一个创建一些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)
Run Code Online (Sandbox Code Playgroud)

这是一个逆向函子,它"向后"工作.如果类型一开始看起来不可能,请考虑类型newtype Flipped b a = Flipped (a -> b)以及您可以使用它做什么.

(<<=) :: (Comonad w) => (w a -> b) -> (w a -> w b)
Run Code Online (Sandbox Code Playgroud)

这是monad的双重 - 而参数(=<<)只能检查局部区域并产生一块结构放在那里,(<<=)可以检查全局结构并产生汇总值的参数.(<<=)本身通常在某种意义上扫描结构,从每个视角获取汇总值,然后重新组合它们以创建新结构.


Lan*_*dei 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]
Run Code Online (Sandbox Code Playgroud)

m a -> (m a -> m b) -> m b不是很有趣.这只是$具有反向参数的函数应用程序(或):我有一个函数m a -> m b并提供一个参数m a,然后我得到m b.

[编辑]

奇怪的是,没有人提到第四个可能的签名:m a -> (m a -> b) -> m b.这实际上也有意义,并导致Comonads