为什么函数不能获取monadic值并返回另一个monadic值?

时间:2012-08-15 10:21:18

标签: haskell functional-programming monads

假设我们有两个monadic函数:

  f :: a -> m b
  g :: b -> m c
  h :: a -> m c

绑定功能定义为

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

我的问题是为什么我们不能做类似下面的事情。声明一个函数,该函数将采用monadic值并返回另一个monadic值?

  f :: a -> m b
  g :: m b -> m c
  h :: a -> m c

绑定功能定义为

(>>=) :: m a -> (ma -> m b) -> m b

haskell中有什么限制函数采用monadic值作为其参数?

编辑:我想我没有说清楚我的问题。关键是,当你使用bind运算符编写函数时,为什么bind运算符的第二个参数是一个采用非monadic值(b)的函数?为什么不能采用monadic值(mb)并返回mc。是这样的,当你处理monad并且你将要编写的函数将始终具有以下类型。

  f :: a -> m b
  g :: b -> m c
  h :: a -> m c

h = f 'compose' g

我正在努力学习单子,这是我无法理解的。

9 个答案:

答案 0 :(得分:6)

Monad的关键功能是“查看”m a类型并查看a;但是Monad的一个关键限制是monad必须是“不可避免的”,即Monad类型类操作不应该足以编写Monad m => m a -> a类型的函数。 (>>=) :: Monad m => m a -> (a -> m b) -> m b为您提供了这种能力。

但实现这一目标的方法不止一种。 Monad类可以这样定义:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

class Functor f => Monad m where
    return :: a -> m a
    join :: m (m a) -> m a

你问为什么我们没有Monad m => m a -> (m a -> m b) -> m b功能。好吧,鉴于f :: a -> bfmap f :: ma -> mb基本上就是这样。但是fmap本身并没有让你能够“查看”Monad m => m a但却无法逃脱它。但是joinfmap一起为您提供了这种能力。 (>>=)一般可以使用fmapjoin编写:

(>>=) :: Monad m => m a -> (a -> m b) -> m b
ma >>= f = join (fmap f ma)

事实上,当您在找到Monad的定义时遇到问题时,这是定义(>>=)实例的常用技巧 - 为您的想法编写join函数monad,然后使用(>>=)的通用定义。


那么,回答“它必须是它的方式”是问题的一部分,带有“否”。但是,为什么会这样呢?

我不能代表Haskell的设计者,但我喜欢这样思考:在Haskell monadic编程中,基本构建块是这样的行为:

getLine :: IO String
putStrLn :: String -> IO ()

更一般地说,这些基本构建块的类型看起来像Monad m => m aMonad m => a -> m bMonad m => a -> b -> m c,...,Monad m => a -> b -> ... -> m z。人们非正式地称这些行动Monad m => m a是无参数动作,Monad m => a -> m b是单参数动作,依此类推。

嗯,(>>=) :: Monad m => m a -> (a -> m b) -> m b基本上是“连接”两个动作的最简单的函数。 getLine >>= putStrLn是首先执行getLine的操作,然后执行putStrLn传递从执行getLine获得的结果。如果你有fmapjoin而不是>>=,你必须写下这个:

join (fmap putStrLn getLine)

更一般地说,(>>=)体现了一个很像行为“管道”的概念,因此使用monads作为一种编程语言更有用。


最后一件事:确保您了解Control.Monad模块。虽然return(>>=)是monad的基本函数,但是你可以使用这两个函数来定义其他更多高级函数,并且该模块会收集几十个更常见的函数。 (>>=)不应强迫您的代码进入紧身衣;它是一个重要的构建块,既可以单独使用,也可以作为较大构建块的组件。

答案 1 :(得分:5)

  

为什么我们不能做类似下面的事情。声明一个函数,该函数将采用monadic值并返回另一个monadic值?

f :: a -> m b
g :: m b -> m c
h :: a -> m c

我是否理解您希望撰写以下内容?

compose :: (a -> m b) -> (m b -> m c) -> (a -> m c)
compose f g = h where
  h = ???

事实证明,这只是常规函数组合,但参数顺序相反

(.) :: (y -> z) -> (x -> y) -> (x -> z)
(g . f) = \x -> g (f x)

让我们选择使用(.)x = ay = m b

类型对z = m c进行专门化
(.) :: (m b -> m c) -> (a -> m b) -> (a -> m c)

现在按顺序翻转输入,即可得到所需的compose函数

compose :: (a -> m b) -> (m b -> m c) -> (a -> m c)
compose = flip (.)

请注意,我们在这里任何地方都没有提到monad。这适用于任何类型的构造函数m,无论它是否为monad。


现在让我们考虑你的另一个问题。假设我们要编写以下内容:

composeM :: (a -> m b) -> (b -> m c) -> (a -> m c)

停止。霍格时间。 Hoogling for that type signature,我们发现确切匹配!来自Control.Monad的是>=>,但请注意,对于此功能,m 必须为monad。

现在的问题是为什么。 组合与其他组合的不同之处在于组合要求m成为Monad,而其他不?那么,这个问题的答案在于理解Monad抽象是什么的核心,所以我将对谈论这个主题的各种互联网资源留下更详细的答案。可以说,如果不知道关于composeM某事,就无法编写m。来吧,试一试。如果没有关于m的更多知识,你就无法编写它,而编写这个函数所需的额外知识恰好是m具有Monad的结构。

答案 2 :(得分:3)

如果您有两个函数f :: m a -> m b和一个monadic值x :: m a,则只需应用f x即可。你不需要任何特殊的monadic操作符,只需要函数应用程序。 f之类的功能永远不会“看到”a类型的值。

函数的Monadic组合是更强大的概念,a -> m b类型的函数是monadic计算的核心。如果您具有monadic值x :: m a,则无法“进入”以检索a类型的值。但是,如果您有一个函数f :: a -> m b可以对a类型的值进行操作,则可以使用>>=使用函数组合值以获取x >>= f :: m b。关键是, f“看到”类型a的值并且可以使用它(但它不能返回它,它只能返回另一个monadic值)。这是>>=的好处,每个monad都需要提供正确的实现。

比较两个概念:

  • 如果您有g :: m a -> m b,则可以使用return撰写g . return :: a -> m b(然后使用>>=),但
  • 反之亦然。通常,无法从类型m a -> m b的函数创建类型a -> m b的函数。

因此,编写像a -> m b这样的类型的函数比构成m a -> m b类型的函数要强大得多。


例如:list monad表示可以提供可变数量答案的计算,包括0个答案(您可以将其视为非确定性计算)。列表monad中计算的关键元素是a -> [b]类型的函数。他们接受一些输入并产生可变数量的答案。这些函数的组合从第一个函数中获取结果,将第二个函数应用于每个结果,并将其合并到一个包含所有可能答案的列表中。

类型[a] -> [b]的函数会有所不同:它们表示采用多个输入并产生多个答案的计算。它们也可以结合起来,但我们得到的东西不如原始概念强。


也许更独特的例子是IO monad。如果您致电getChar :: IO Char并仅使用IO a -> IO b类型的函数,则您将永远无法使用已读取的字符。但>>=允许您将这样的值与a -> IO b类型的函数结合起来,该函数可以“看到”该角色并对其执行某些操作。

答案 3 :(得分:3)

让我稍微解释一下你的问题:

  

为什么不能 我们在Monads中使用g :: m a -> m b类型的函数?

答案是,我们已经,使用Functors 。关于fmap f :: Functor m => m a -> m b f :: a -> b,其中没有特别的“monadic”。 Monads是Functors;我们只使用好的fmap

来获得这些功能
class Functor f a where
    fmap :: (a -> b) -> f a -> f b

答案 4 :(得分:1)

正如其他人所指出的,没有任何东西可以限制函数将monadic值作为参数。绑定函数本身需要一个,但不是赋予绑定的函数。

我认为你可以用“Monad is a Container”这个比喻让自己理解。一个很好的例子就是Maybe。虽然我们知道如何从Maybe conatiner中解包一个值,但我们并不知道每个monad都有这个值,而在一些monad(如IO)中,这是完全不可能的。 现在的想法是,Monad以一种您不必了解的方式在幕后进行。例如,您确实需要使用IO monad中返回的值,但是您无法解包它,因此执行此操作的函数需要位于IO monad本身。

答案 5 :(得分:1)

我喜欢将monad视为构建具有特定上下文的程序的方法。 monad提供的功能是能够在构建的程序中的任何阶段根据先前的值进行分支。选择通常的>>=函数作为此分支能力的最常用的接口。

例如,Maybe monad提供的程序可能在某个阶段失败(上下文是失败状态)。考虑这个psuedo-Haskell示例:

-- take a computation that produces an Int.  If the current Int is even, add 1.
incrIfEven :: Monad m => m Int -> m Int
incrIfEven anInt =
    let ourInt = currentStateOf anInt
    in if even ourInt then return (ourInt+1) else return ourInt

为了根据当前的计算结果进行分支,我们需要能够访问当前结果。如果我们可以访问currentStateOf :: m a -> a,上面的伪代码将会起作用,但monad通常不可能。相反,我们决定将分支作为类型a -> m b的函数进行分支。由于a在此函数中不在monad中,因此我们可以将其视为常规值,这样更容易使用。

incrIfEvenReal :: Monad m => m Int -> m Int
incrIfEvenReal anInt = anInt >>= branch
  where branch ourInt = if even ourInt then return (ourInt+1) else return ourInt

因此>>=的类型实际上是为了便于编程,但有一些替代方案有时更有用。值得注意的是函数Control.Monad.join,当与fmap结合使用时,其功效与>>=完全相同(可以用另一种来定义)。

答案 6 :(得分:1)

原因(>> =)的第二个参数不接受monad作为输入,因为根本不需要绑定这样的函数。只需应用它:

m :: m a
f :: a -> m b
g :: m b -> m c
h :: c -> m b

(g (m >>= f)) >>= h

你根本不需要(>> =)。

答案 7 :(得分:0)

如果需要,该函数可以采用monadic值。但没有强制这样做。

使用Data.Char中的list monad和函数来考虑以下设计定义:

m :: [[Int]]
m = [[71,72,73], [107,106,105,104]]

f :: [Int] -> [Char]
f mx = do
    g <- [toUpper, id, toLower]
    x <- mx
    return (g $ chr x)

你当然可以m >>= f;结果将具有类型[Char]

(这里重要的是m :: [[Int]]而不是m :: [Int]>>=总是从第一个参数“剥离”一个monadic图层。如果你不希望发生这种情况,请执行f m代替m >>= f。)

答案 8 :(得分:0)

正如其他人所提到的,没有任何限制这些功能被写入。

事实上,有一大类:: m a -> (m a -> m b) -> m b类型的函数:

f :: Monad m => Int -> m a -> (m a -> m b) -> m b
f n m mf = replicateM_ n m >>= mf m

其中

f 0 m mf = mf m

f 1 m mf = m >> mf m

f 2 m mf = m >> m >> mf m

......等......

(注意基本情况:当n为0时,它只是正常的功能应用。)

但这个功能有什么作用?它多次执行monadic动作,最后丢弃所有结果,并将mf的应用程序返回给m。

有时有用,但几乎没有用,特别是与>>=相比。

快速Hoogle search没有出现任何结果;也许是一个有说服力的结果。