在我看来,对于“什么是单子?”这个著名问题的答案,尤其是那些投票最多的答案,试图解释什么是单子,却没有清楚地解释为什么单子是必要的。它们能被解释为问题的解决方案吗?


当前回答

答案当然是“我们没有”。与所有抽象一样,这是不必要的。

Haskell不需要单子抽象。在纯语言中执行IO并不是必需的。IO类型自己就能很好地处理这个问题。现有的do块的单方糖化可以替换为GHC中定义的bindIO、returnIO和failIO糖化。基础模块。(它不是关于hackage的文档模块,所以我必须指出它的文档来源。)所以不,没有必要抽象单子。

如果不需要它,为什么它会存在?因为人们发现许多计算模式形成了单一结构。结构的抽象允许编写跨该结构的所有实例的代码。更简单地说——代码重用。

在函数式语言中,最强大的代码重用工具是函数的组合。老的(.)::(b -> c) -> (a -> b) -> (a -> c)运算符非常强大。它可以很容易地编写小函数,并以最小的语法或语义开销将它们粘合在一起。

但在某些情况下,这些类型并不完全正确。当你有foo::(b ->也许c)和bar::(a ->也许b)你会做什么?foo。bar不进行类型检查,因为b和b可能不是相同的类型。

但是…几乎是对的。你只是需要一点回旋的余地。你想要把Maybe b看成是b,但是直接把它们看成是同一种类型不是一个好主意。这或多或少和空指针是一样的,Tony Hoare把空指针称为“十亿美元的错误”。因此,如果不能将它们视为同一类型,也许可以找到一种方法来扩展组合机制(.)提供的功能。

In that case, it's important to really examine the theory underlying (.). Fortunately, someone has already done this for us. It turns out that the combination of (.) and id form a mathematical construct known as a category. But there are other ways to form categories. A Kleisli category, for instance, allows the objects being composed to be augmented a bit. A Kleisli category for Maybe would consist of (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c) and id :: a -> Maybe a. That is, the objects in the category augment the (->) with a Maybe, so (a -> b) becomes (a -> Maybe b).

突然之间,我们将复合的功能扩展到了传统(.)操作无法处理的事情上。这是一种新的抽象力量的来源。Kleisli分类适用于更多类型,而不仅仅是Maybe。他们与每一种能够组合出合适类别的类型一起工作,并遵循类别法则。

左标识:id。F = F 右恒等式:f。Id = f 结合律:f。(g。H) = (f。g)。h

As long as you can prove that your type obeys those three laws, you can turn it into a Kleisli category. And what's the big deal about that? Well, it turns out that monads are exactly the same thing as Kleisli categories. Monad's return is the same as Kleisli id. Monad's (>>=) isn't identical to Kleisli (.), but it turns out to be very easy to write each in terms of the other. And the category laws are the same as the monad laws, when you translate them across the difference between (>>=) and (.).

那么为什么要这么麻烦呢?为什么在语言中有一个单子抽象?如上所述,它支持代码重用。它甚至可以在两个不同的维度上实现代码重用。

代码重用的第一个维度直接来自抽象的存在。您可以编写跨所有抽象实例工作的代码。有一个完整的Monad -loops包,由与Monad的任何实例一起工作的循环组成。

第二个维度是间接的,但它源于构图的存在。当组合很容易时,很自然地编写小的、可重用的代码块。同样,使用(.)操作符可以鼓励编写小型的、可重用的函数。

那么为什么抽象存在呢?因为它被证明是一种工具,可以在代码中实现更多的组合,从而创建可重用的代码,并鼓励创建更多可重用的代码。代码重用是编程的终极目标之一。单子抽象的存在是因为它将我们推向了圣杯。

其他回答

为什么我们需要单一类型?

由于I/O的困境和它在非严格语言(如Haskell)中可观察到的影响,使得单元接口如此突出:

[...] monads are used to address the more general problem of computations (involving state, input/output, backtracking, ...) returning values: they do not solve any input/output-problems directly but rather provide an elegant and flexible abstraction of many solutions to related problems. [...] For instance, no less than three different input/output-schemes are used to solve these basic problems in Imperative functional programming, the paper which originally proposed `a new model, based on monads, for performing input/output in a non-strict, purely functional language'. [...] [Such] input/output-schemes merely provide frameworks in which side-effecting operations can safely be used with a guaranteed order of execution and without affecting the properties of the purely functional parts of the language. Claus Reinke (pages 96-97 of 210). (emphasis by me.) [...] When we write effectful code – monads or no monads – we have to constantly keep in mind the context of expressions we pass around. The fact that monadic code ‘desugars’ (is implementable in terms of) side-effect-free code is irrelevant. When we use monadic notation, we program within that notation – without considering what this notation desugars into. Thinking of the desugared code breaks the monadic abstraction. A side-effect-free, applicative code is normally compiled to (that is, desugars into) C or machine code. If the desugaring argument has any force, it may be applied just as well to the applicative code, leading to the conclusion that it all boils down to the machine code and hence all programming is imperative. [...] From the personal experience, I have noticed that the mistakes I make when writing monadic code are exactly the mistakes I made when programming in C. Actually, monadic mistakes tend to be worse, because monadic notation (compared to that of a typical imperative language) is ungainly and obscuring. Oleg Kiselyov (page 21 of 26). The most difficult construct for students to understand is the monad. I introduce IO without mentioning monads. Olaf Chitil.

更普遍的:

Still, today, over 25 years after the introduction of the concept of monads to the world of functional programming, beginning functional programmers struggle to grasp the concept of monads. This struggle is exemplified by the numerous blog posts about the effort of trying to learn about monads. From our own experience we notice that even at university level, bachelor level students often struggle to comprehend monads and consistently score poorly on monad-related exam questions. Considering that the concept of monads is not likely to disappear from the functional programming landscape any time soon, it is vital that we, as the functional programming community, somehow overcome the problems novices encounter when first studying monads. Tim Steenvoorden, Jurriën Stutterheim, Erik Barendsen and Rinus Plasmeijer.

如果有另一种方法可以在Haskell中指定“有保证的执行顺序”,同时保持将常规Haskell定义与那些涉及I/O(及其可观察的效果)的定义分开的能力-翻译Philip Wadler的这种变化:

val echoML    : unit -> unit
fun echoML () = let val c = getcML () in
                if c = #"\n" then
                  ()
                else
                  let val _ = putcML c in
                  echoML ()
                end

fun putcML c  = TextIO.output1(TextIO.stdOut,c);
fun getcML () = valOf(TextIO.input1(TextIO.stdIn));

...然后可以像这样简单:

echo :: OI -> ()                         
echo u = let !(u1:u2:u3:_) = partsOI u in
         let !c = getChar u1 in          
         if c == '\n' then               
           ()                            
         else                            
           let !_ = putChar c u2 in      
           echo u3                       

地点:

data OI  -- abstract

foreign import ccall "primPartOI" partOI :: OI -> (OI, OI)
                      ⋮

foreign import ccall "primGetCharOI" getChar :: OI -> Char
foreign import ccall "primPutCharOI" putChar :: Char -> OI -> ()
                      ⋮

and:

partsOI         :: OI -> [OI]
partsOI u       =  let !(u1, u2) = partOI u in u1 : partsOI u2 

这是如何运作的呢?在运行时,Main。main接收一个初始OI伪数据值作为参数:

module Main(main) where

main            :: OI -> ()
          ⋮

使用parttoi或partsOI从其中产生其他OI值。您所要做的就是确保每个新的OI值在每次调用基于OI的定义(外部或其他)时最多使用一次。作为回报,你会得到一个普通的结果——它并没有与一些奇怪的抽象状态配对,或者需要使用回调延续等等。

使用OI,而不是像标准ML那样使用单元类型(),意味着我们可以避免总是必须使用单一的接口:

一旦你进入IO单子,你就永远被困在那里,并被简化为algolstyle命令式编程。 罗伯特·哈珀。

但如果你真的需要它:

type IO a       =  OI -> a

unitIO          :: a -> IO a
unitIO x        =  \ u -> let !_ = partOI u in x

bindIO          :: IO a -> (a -> IO b) -> IO b
bindIO m k      =  \ u -> let !(u1, u2) = partOI u in
                          let !x        = m u1 in
                          let !y        = k x u2 in
                          y

                      ⋮

所以,单体类型并不总是需要的-有其他的接口:

LML早在1989年就有了一个完整的oracle多处理器(sequence Symmetry)实现。Fudgets论文中的描述引用了这个实现。和它一起工作很愉快,也很实用。 […] 现在所有的事情都是用单子完成的,所以其他的解决方案有时会被遗忘。 Lennart Augustsson(2006)。


等一下:既然它与标准ML直接使用效果非常相似,那么这种方法及其使用的伪数据引用是透明的吗?

当然,只要找到一个合适的“参考透明度”的定义;有很多选择…

为什么我们需要单子?

We want to program only using functions. ("functional programming (FP)" after all). Then, we have a first big problem. This is a program: f(x) = 2 * x g(x,y) = x / y How can we say what is to be executed first? How can we form an ordered sequence of functions (i.e. a program) using no more than functions? Solution: compose functions. If you want first g and then f, just write f(g(x,y)). This way, "the program" is a function as well: main = f(g(x,y)). OK, but ... More problems: some functions might fail (i.e. g(2,0), divide by 0). We have no "exceptions" in FP (an exception is not a function). How do we solve it? Solution: Let's allow functions to return two kind of things: instead of having g : Real,Real -> Real (function from two reals into a real), let's allow g : Real,Real -> Real | Nothing (function from two reals into (real or nothing)). But functions should (to be simpler) return only one thing. Solution: let's create a new type of data to be returned, a "boxing type" that encloses maybe a real or be simply nothing. Hence, we can have g : Real,Real -> Maybe Real. OK, but ... What happens now to f(g(x,y))? f is not ready to consume a Maybe Real. And, we don't want to change every function we could connect with g to consume a Maybe Real. Solution: let's have a special function to "connect"/"compose"/"link" functions. That way, we can, behind the scenes, adapt the output of one function to feed the following one. In our case: g >>= f (connect/compose g to f). We want >>= to get g's output, inspect it and, in case it is Nothing just don't call f and return Nothing; or on the contrary, extract the boxed Real and feed f with it. (This algorithm is just the implementation of >>= for the Maybe type). Also note that >>= must be written only once per "boxing type" (different box, different adapting algorithm). Many other problems arise which can be solved using this same pattern: 1. Use a "box" to codify/store different meanings/values, and have functions like g that return those "boxed values". 2. Have a composer/linker g >>= f to help connecting g's output to f's input, so we don't have to change any f at all. Remarkable problems that can be solved using this technique are: having a global state that every function in the sequence of functions ("the program") can share: solution StateMonad. We don't like "impure functions": functions that yield different output for same input. Therefore, let's mark those functions, making them to return a tagged/boxed value: IO monad.

总幸福!

答案当然是“我们没有”。与所有抽象一样,这是不必要的。

Haskell不需要单子抽象。在纯语言中执行IO并不是必需的。IO类型自己就能很好地处理这个问题。现有的do块的单方糖化可以替换为GHC中定义的bindIO、returnIO和failIO糖化。基础模块。(它不是关于hackage的文档模块,所以我必须指出它的文档来源。)所以不,没有必要抽象单子。

如果不需要它,为什么它会存在?因为人们发现许多计算模式形成了单一结构。结构的抽象允许编写跨该结构的所有实例的代码。更简单地说——代码重用。

在函数式语言中,最强大的代码重用工具是函数的组合。老的(.)::(b -> c) -> (a -> b) -> (a -> c)运算符非常强大。它可以很容易地编写小函数,并以最小的语法或语义开销将它们粘合在一起。

但在某些情况下,这些类型并不完全正确。当你有foo::(b ->也许c)和bar::(a ->也许b)你会做什么?foo。bar不进行类型检查,因为b和b可能不是相同的类型。

但是…几乎是对的。你只是需要一点回旋的余地。你想要把Maybe b看成是b,但是直接把它们看成是同一种类型不是一个好主意。这或多或少和空指针是一样的,Tony Hoare把空指针称为“十亿美元的错误”。因此,如果不能将它们视为同一类型,也许可以找到一种方法来扩展组合机制(.)提供的功能。

In that case, it's important to really examine the theory underlying (.). Fortunately, someone has already done this for us. It turns out that the combination of (.) and id form a mathematical construct known as a category. But there are other ways to form categories. A Kleisli category, for instance, allows the objects being composed to be augmented a bit. A Kleisli category for Maybe would consist of (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c) and id :: a -> Maybe a. That is, the objects in the category augment the (->) with a Maybe, so (a -> b) becomes (a -> Maybe b).

突然之间,我们将复合的功能扩展到了传统(.)操作无法处理的事情上。这是一种新的抽象力量的来源。Kleisli分类适用于更多类型,而不仅仅是Maybe。他们与每一种能够组合出合适类别的类型一起工作,并遵循类别法则。

左标识:id。F = F 右恒等式:f。Id = f 结合律:f。(g。H) = (f。g)。h

As long as you can prove that your type obeys those three laws, you can turn it into a Kleisli category. And what's the big deal about that? Well, it turns out that monads are exactly the same thing as Kleisli categories. Monad's return is the same as Kleisli id. Monad's (>>=) isn't identical to Kleisli (.), but it turns out to be very easy to write each in terms of the other. And the category laws are the same as the monad laws, when you translate them across the difference between (>>=) and (.).

那么为什么要这么麻烦呢?为什么在语言中有一个单子抽象?如上所述,它支持代码重用。它甚至可以在两个不同的维度上实现代码重用。

代码重用的第一个维度直接来自抽象的存在。您可以编写跨所有抽象实例工作的代码。有一个完整的Monad -loops包,由与Monad的任何实例一起工作的循环组成。

第二个维度是间接的,但它源于构图的存在。当组合很容易时,很自然地编写小的、可重用的代码块。同样,使用(.)操作符可以鼓励编写小型的、可重用的函数。

那么为什么抽象存在呢?因为它被证明是一种工具,可以在代码中实现更多的组合,从而创建可重用的代码,并鼓励创建更多可重用的代码。代码重用是编程的终极目标之一。单子抽象的存在是因为它将我们推向了圣杯。

Monads are just a convenient framework for solving a class of recurring problems. First, monads must be functors (i.e. must support mapping without looking at the elements (or their type)), they must also bring a binding (or chaining) operation and a way to create a monadic value from an element type (return). Finally, bind and return must satisfy two equations (left and right identities), also called the monad laws. (Alternatively one could define monads to have a flattening operation instead of binding.)

列表单子通常用于处理不确定性。绑定操作选择列表中的一个元素(直观地说,它们都在并行世界中),让程序员对它们进行一些计算,然后将所有世界中的结果组合到一个列表中(通过连接或平铺嵌套列表)。下面是如何在Haskell的单元框架中定义一个排列函数:

perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

下面是一个示例repl会话:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

需要注意的是,列表单子绝不是计算的副作用。一个数学结构是一个单子(即符合上面提到的接口和规律)并不意味着副作用,尽管副作用现象通常很好地适合单子框架。

本杰明·皮尔斯在TAPL中说

一个类型系统可以看作是计算一种静态 近似于程序中项的运行时行为。

这就是为什么配备了强大类型系统的语言严格来说比类型差的语言更具表现力。你可以用同样的方式来思考单子。

正如@Carl和sigfpe所指出的那样,你可以为一个数据类型配备你想要的所有操作,而无需求助于单子、类型类或任何其他抽象的东西。然而,单子不仅允许你编写可重用的代码,还可以抽象出所有冗余的细节。

举个例子,假设我们想过滤一个列表。最简单的方法是使用filter函数:filter (> 3) [1..]10],等于[4,5,6,7,8,9,10]。

filter的一个稍微复杂一点的版本也是从左向右传递累加器

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]

获取所有i,使i <= 10, sum [1..]I] > 4, sum [1..I] < 25,我们可以写

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

等于[3,4,5,6]。

或者我们可以重新定义nub函数,它从列表中删除重复的元素,使用filterAccum:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4] equals [1,2,4,5,3,8,9]. A list is passed as an accumulator here. The code works, because it's possible to leave the list monad, so the whole computation stays pure (notElem doesn't use >>= actually, but it could). However it's not possible to safely leave the IO monad (i.e. you cannot execute an IO action and return a pure value — the value always will be wrapped in the IO monad). Another example is mutable arrays: after you have leaved the ST monad, where a mutable array live, you cannot update the array in constant time anymore. So we need a monadic filtering from the Control.Monad module:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterM对列表中的所有元素执行一个单体操作,生成元素,单体操作返回True。

一个带有数组的过滤示例:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

按预期打印[1,2,4,5,3,8,9]。

还有一个带有IO单子的版本,它询问要返回哪些元素:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

E.g.

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

最后一个例子,filterAccum可以用filterM来定义:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

StateT单子,它在底层使用,只是一个普通的数据类型。

这个例子说明,单子不仅允许您抽象计算上下文和编写干净的可重用代码(由于单子的可组合性,正如@Carl解释的那样),而且还可以统一对待用户定义的数据类型和内置原语。