7

假设我们有两个一元函数:

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

绑定函数定义为

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

我的问题是为什么我们不能做下面的事情。声明一个接受一元值并返回另一个一元值的函数?

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

绑定函数定义为

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

haskell 中有什么限制函数将一元值作为参数?

编辑:我想我没有把我的问题说清楚。关键是,当您使用绑定运算符组合函数时,为什么绑定运算符的第二个参数是一个采用非单子值(b)的函数?为什么它不能采用一元值 ( mb) 并回馈mc 。是不是这样,当您处理 monad 时,您将编写的函数将始终具有以下类型。

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

h = f 'compose' g

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

4

9 回答 9

7

的一个关键能力Monad是“查看内部”m a类型并查看a; 但是 的一个关键限制Monad是 monad 必须是“不可避免的”,即Monadtypeclass 操作不应该足以编写 type 的函数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一起给你这种能力。 (>>=)一般可以写成fmapand join

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

事实上,Monad当你想出一个定义时,这是定义实例的一个常见技巧——为你的可能的 monad 编写函数,然后(>>=)使用.join(>>=)


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

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

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

更一般地说,这些基本构建块的类型看起来像Monad m => m a, Monad m => a -> m b, Monad 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)

更一般地说,(>>=)它体现了一个很像动作“管道”的概念,因此对于使用 monad 作为一种编程语言来说,它是更有用的运算符。


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

于 2012-08-15T16:53:10.180 回答
5

为什么我们不能做下面的事情。声明一个接受一元值并返回另一个一元值的函数?

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 = a,y = m bz = 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 (.)

请注意,我们在这里甚至没有提到单子。这对于任何类型的构造函数都非常有效m,无论它是否是 monad。


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

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

停止。胡歌时间。搜索该类型签名,我们发现完全匹配!它>=>来自 Control.Monad,但请注意,对于这个函数,它m 必须是一个 monad。

现在的问题是为什么。是什么让这个组合与另一个不同,使得这个组合需要m是 Monad,而另一个不需要?嗯,这个问题的答案在于理解 Monad 抽象是什么的核心,所以我将为讨论这个主题的各种互联网资源留下更详细的答案。可以说,composeM不了解.就无法写作m。来吧,试试看。如果没有一些关于是什么的额外知识,你就无法编写它m,而编写此函数所需的额外知识恰好是m具有以下结构的Monad.

于 2012-08-15T16:23:25.477 回答
4

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

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

答案是,我们已经使用 Functors 了。关于where没有什么特别“单调的”。单子是函子;我们只需使用 good old 就可以获得这样的功能:fmap f :: Functor m => m a -> m bf :: a -> bfmap

class Functor f a where
    fmap :: (a -> b) -> f a -> f b
于 2012-08-16T13:10:29.020 回答
3

如果你有两个函数f :: m a -> m b和一个单值x :: m a,你可以简单地应用f x。您不需要任何特殊的一元运算符,只需函数应用程序即可。但是诸如此类的函数f永远不能“看到” type 的值a

函数的一元组合是一个更强大的概念,类型函数是a -> m b一元计算的核心。如果你有一个单子 value x :: m a,你不能“进入它”来检索一些 type 的值a。但是,如果您有一个f :: a -> m b对 type 值进行操作的函数a,您可以使用>>=to get的函数组合该值x >>= f :: m b。关键是,f“看到”一个类型的值a并且可以使用它(但它不能返回它,它只能返回另一个单子值)。这是 monad 的好处,>>=每个 monad 都需要提供其正确的实现。

比较两个概念:

  • 如果你有g :: m a -> m b,你可以用它return来获得g . return :: a -> m b(然后用>>=),但是
  • 反之亦然。通常,无法m a -> m b从 type 的函数创建 type 的函数a -> m b

因此,组合 like 类型的函数a -> m b是一个比组合 like 类型的函数严格更强的概念m a -> m b


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

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


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

于 2012-08-15T19:30:00.330 回答
1

The reason (>>=)'s second argument does not take a monad as input is because there is no need to bind such a function at all. Just apply it:

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

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

You don't need (>>=) for g at all.

于 2012-08-15T15:30:12.037 回答
1

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

例如,Maybemonad 提供了一个可能在某个阶段失败的程序(上下文是失败状态)。考虑这个伪 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,但这通常不适用于 monads。相反,我们将分支的决定写为 type 的函数a -> m b。由于ais 不在此函数的 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完全相同。>>=

于 2012-08-15T15:07:43.523 回答
1

正如其他人指出的那样,没有什么可以限制函数将一元值作为参数。bind 函数本身需要一个,但不是赋予绑定的函数。

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

于 2012-08-15T11:01:22.627 回答
0

如果需要,该函数可以采用一元值。但它不是被迫这样做的。

考虑以下人为的定义,使用来自 Data.Char 的列表 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;结果将具有 type [Char]

(这很重要,m :: [[Int]]而不是m :: [Int].>>=总是从它的第一个参数中“剥离”一个单子层。如果你不希望这种情况发生,请做f m而不是m >>= f.)

于 2012-08-15T10:56:43.103 回答
0

正如其他人所提到的,没有什么可以限制编写此类函数。

事实上,有一大类 type 函数:: 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

... ETC。 ...

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

但是这个函数有什么作用呢?它多次执行一个单子动作,最后丢弃所有结果,并将 mf 的应用程序返回给 m。

有时有用,但通常几乎没有用,尤其是与>>=.

快速的Hoogle 搜索不会出现任何结果;也许是一个有说服力的结果。

于 2012-08-15T15:08:48.260 回答