这些天有很多关于单子的讨论。我已经读了一些文章/博客文章,但我不能足够深入地了解他们的例子,以完全掌握这个概念。原因是单子是一个函数式语言的概念,因此这些例子使用的是我没有使用过的语言(因为我没有深入使用过函数式语言)。我不能完全理解文章的语法……但我能看出其中有值得理解的地方。

但是,我非常了解c#,包括lambda表达式和其他函数特性。我知道c#只有一个功能特性的子集,所以可能单子不能在c#中表达。

然而,是否有可能传达这个概念呢?至少我希望如此。也许您可以提供一个c#示例作为基础,然后描述c#开发人员希望他能从那里做什么,但由于语言缺乏函数式编程特性而不能做什么。这太棒了,因为它传达了单子的意图和好处。所以我的问题是:你能给c# 3开发人员的单子最好的解释是什么?

谢谢!

(编辑:顺便说一下,我知道至少有3个“什么是单子”的问题已经在SO。然而,我也面临着同样的问题…所以这个问题在我看来是有必要的,因为c#开发者的关注点。谢谢。)


当前回答

自从我发布这个问题已经有一年了。在发布了这篇文章之后,我花了几个月时间研究Haskell。我非常喜欢它,但当我准备深入研究单子时,我把它放在了一边。我回去工作,专注于我的项目所需的技术。

昨晚,我又读了一遍这些回复。最重要的是,我重新阅读了上面有人提到的Brian Beckman视频文本注释中的特定c#示例。它是如此清晰和启发性,我决定直接张贴在这里。

因为这个评论,我不仅觉得我确切地理解了单子是什么,我意识到我实际上用c#写了一些单子的东西,或者至少非常接近,并努力解决相同的问题。

所以,这是评论-这都是直接引用sylvan的评论:

This is pretty cool. It's a bit abstract though. I can imagine people who don't know what monads are already get confused due to the lack of real examples. So let me try to comply, and just to be really clear I'll do an example in C#, even though it will look ugly. I'll add the equivalent Haskell at the end and show you the cool Haskell syntactic sugar which is where, IMO, monads really start getting useful. Okay, so one of the easiest Monads is called the "Maybe monad" in Haskell. In C# the Maybe type is called Nullable<T>. It's basically a tiny class that just encapsulates the concept of a value that is either valid and has a value, or is "null" and has no value. A useful thing to stick inside a monad for combining values of this type is the notion of failure. I.e. we want to be able to look at multiple nullable values and return null as soon as any one of them is null. This could be useful if you, for example, look up lots of keys in a dictionary or something, and at the end you want to process all of the results and combine them somehow, but if any of the keys are not in the dictionary, you want to return null for the whole thing. It would be tedious to manually have to check each lookup for null and return, so we can hide this checking inside the bind operator (which is sort of the point of monads, we hide book-keeping in the bind operator which makes the code easier to use since we can forget about the details). Here's the program that motivates the whole thing (I'll define the Bind later, this is just to show you why it's nice). class Program { static Nullable<int> f(){ return 4; } static Nullable<int> g(){ return 7; } static Nullable<int> h(){ return 9; } static void Main(string[] args) { Nullable<int> z = f().Bind( fval => g().Bind( gval => h().Bind( hval => new Nullable<int>( fval + gval + hval )))); Console.WriteLine( "z = {0}", z.HasValue ? z.Value.ToString() : "null" ); Console.WriteLine("Press any key to continue..."); Console.ReadKey(); } } Now, ignore for a moment that there already is support for doing this for Nullable in C# (you can add nullable ints together and you get null if either is null). Let's pretend that there is no such feature, and it's just a user-defined class with no special magic. The point is that we can use the Bind function to bind a variable to the contents of our Nullable value and then pretend that there's nothing strange going on, and use them like normal ints and just add them together. We wrap the result in a nullable at the end, and that nullable will either be null (if any of f, g or h returns null) or it will be the result of summing f, g, and h together. (this is analogous of how we can bind a row in a database to a variable in LINQ, and do stuff with it, safe in the knowledge that the Bind operator will make sure that the variable will only ever be passed valid row values). You can play with this and change any of f, g, and h to return null and you will see that the whole thing will return null. So clearly the bind operator has to do this checking for us, and bail out returning null if it encounters a null value, and otherwise pass along the value inside the Nullable structure into the lambda. Here's the Bind operator: public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) where B : struct where A : struct { return a.HasValue ? f(a.Value) : null; } The types here are just like in the video. It takes an M a (Nullable<A> in C# syntax for this case), and a function from a to M b (Func<A, Nullable<B>> in C# syntax), and it returns an M b (Nullable<B>). The code simply checks if the nullable contains a value and if so extracts it and passes it onto the function, else it just returns null. This means that the Bind operator will handle all the null-checking logic for us. If and only if the value that we call Bind on is non-null then that value will be "passed along" to the lambda function, else we bail out early and the whole expression is null. This allows the code that we write using the monad to be entirely free of this null-checking behaviour, we just use Bind and get a variable bound to the value inside the monadic value (fval, gval and hval in the example code) and we can use them safe in the knowledge that Bind will take care of checking them for null before passing them along. There are other examples of things you can do with a monad. For example you can make the Bind operator take care of an input stream of characters, and use it to write parser combinators. Each parser combinator can then be completely oblivious to things like back-tracking, parser failures etc., and just combine smaller parsers together as if things would never go wrong, safe in the knowledge that a clever implementation of Bind sorts out all the logic behind the difficult bits. Then later on maybe someone adds logging to the monad, but the code using the monad doesn't change, because all the magic happens in the definition of the Bind operator, the rest of the code is unchanged. Finally, here's the implementation of the same code in Haskell (-- begins a comment line). -- Here's the data type, it's either nothing, or "Just" a value -- this is in the standard library data Maybe a = Nothing | Just a -- The bind operator for Nothing Nothing >>= f = Nothing -- The bind operator for Just x Just x >>= f = f x -- the "unit", called "return" return = Just -- The sample code using the lambda syntax -- that Brian showed z = f >>= ( \fval -> g >>= ( \gval -> h >>= ( \hval -> return (fval+gval+hval ) ) ) ) -- The following is exactly the same as the three lines above z2 = do fval <- f gval <- g hval <- h return (fval+gval+hval) As you can see the nice do notation at the end makes it look like straight imperative code. And indeed this is by design. Monads can be used to encapsulate all the useful stuff in imperative programming (mutable state, IO etc.) and used using this nice imperative-like syntax, but behind the curtains, it's all just monads and a clever implementation of the bind operator! The cool thing is that you can implement your own monads by implementing >>= and return. And if you do so those monads will also be able to use the do notation, which means you can basically write your own little languages by just defining two functions!

其他回答

请看我对“什么是单子?”

它从一个激励的例子开始,通过这个例子,派生出一个单子的例子,并正式定义“单子”。

它假设没有函数式编程的知识,并且使用带有function(参数):=表达式语法的伪代码,并使用尽可能简单的表达式。

这个c#程序是伪代码单子的实现。(供参考:M是类型构造函数,feed是“bind”操作,wrap是“return”操作。)

using System.IO;
using System;

class Program
{
    public class M<A>
    {
        public A val;
        public string messages;
    }

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x)
    {
        M<B> m = f(x.val);
        m.messages = x.messages + m.messages;
        return m;
    }

    public static M<A> wrap<A>(A x)
    {
        M<A> m = new M<A>();
        m.val = x;
        m.messages = "";
        return m;
    }

    public class T {};
    public class U {};
    public class V {};

    public static M<U> g(V x)
    {
        M<U> m = new M<U>();
        m.messages = "called g.\n";
        return m;
    }

    public static M<T> f(U x)
    {
        M<T> m = new M<T>();
        m.messages = "called f.\n";
        return m;
    }

    static void Main()
    {
        V x = new V();
        M<T> m = feed<U, T>(f, feed(g, wrap<V>(x)));
        Console.Write(m.messages);
    }
}

自从我发布这个问题已经有一年了。在发布了这篇文章之后,我花了几个月时间研究Haskell。我非常喜欢它,但当我准备深入研究单子时,我把它放在了一边。我回去工作,专注于我的项目所需的技术。

昨晚,我又读了一遍这些回复。最重要的是,我重新阅读了上面有人提到的Brian Beckman视频文本注释中的特定c#示例。它是如此清晰和启发性,我决定直接张贴在这里。

因为这个评论,我不仅觉得我确切地理解了单子是什么,我意识到我实际上用c#写了一些单子的东西,或者至少非常接近,并努力解决相同的问题。

所以,这是评论-这都是直接引用sylvan的评论:

This is pretty cool. It's a bit abstract though. I can imagine people who don't know what monads are already get confused due to the lack of real examples. So let me try to comply, and just to be really clear I'll do an example in C#, even though it will look ugly. I'll add the equivalent Haskell at the end and show you the cool Haskell syntactic sugar which is where, IMO, monads really start getting useful. Okay, so one of the easiest Monads is called the "Maybe monad" in Haskell. In C# the Maybe type is called Nullable<T>. It's basically a tiny class that just encapsulates the concept of a value that is either valid and has a value, or is "null" and has no value. A useful thing to stick inside a monad for combining values of this type is the notion of failure. I.e. we want to be able to look at multiple nullable values and return null as soon as any one of them is null. This could be useful if you, for example, look up lots of keys in a dictionary or something, and at the end you want to process all of the results and combine them somehow, but if any of the keys are not in the dictionary, you want to return null for the whole thing. It would be tedious to manually have to check each lookup for null and return, so we can hide this checking inside the bind operator (which is sort of the point of monads, we hide book-keeping in the bind operator which makes the code easier to use since we can forget about the details). Here's the program that motivates the whole thing (I'll define the Bind later, this is just to show you why it's nice). class Program { static Nullable<int> f(){ return 4; } static Nullable<int> g(){ return 7; } static Nullable<int> h(){ return 9; } static void Main(string[] args) { Nullable<int> z = f().Bind( fval => g().Bind( gval => h().Bind( hval => new Nullable<int>( fval + gval + hval )))); Console.WriteLine( "z = {0}", z.HasValue ? z.Value.ToString() : "null" ); Console.WriteLine("Press any key to continue..."); Console.ReadKey(); } } Now, ignore for a moment that there already is support for doing this for Nullable in C# (you can add nullable ints together and you get null if either is null). Let's pretend that there is no such feature, and it's just a user-defined class with no special magic. The point is that we can use the Bind function to bind a variable to the contents of our Nullable value and then pretend that there's nothing strange going on, and use them like normal ints and just add them together. We wrap the result in a nullable at the end, and that nullable will either be null (if any of f, g or h returns null) or it will be the result of summing f, g, and h together. (this is analogous of how we can bind a row in a database to a variable in LINQ, and do stuff with it, safe in the knowledge that the Bind operator will make sure that the variable will only ever be passed valid row values). You can play with this and change any of f, g, and h to return null and you will see that the whole thing will return null. So clearly the bind operator has to do this checking for us, and bail out returning null if it encounters a null value, and otherwise pass along the value inside the Nullable structure into the lambda. Here's the Bind operator: public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) where B : struct where A : struct { return a.HasValue ? f(a.Value) : null; } The types here are just like in the video. It takes an M a (Nullable<A> in C# syntax for this case), and a function from a to M b (Func<A, Nullable<B>> in C# syntax), and it returns an M b (Nullable<B>). The code simply checks if the nullable contains a value and if so extracts it and passes it onto the function, else it just returns null. This means that the Bind operator will handle all the null-checking logic for us. If and only if the value that we call Bind on is non-null then that value will be "passed along" to the lambda function, else we bail out early and the whole expression is null. This allows the code that we write using the monad to be entirely free of this null-checking behaviour, we just use Bind and get a variable bound to the value inside the monadic value (fval, gval and hval in the example code) and we can use them safe in the knowledge that Bind will take care of checking them for null before passing them along. There are other examples of things you can do with a monad. For example you can make the Bind operator take care of an input stream of characters, and use it to write parser combinators. Each parser combinator can then be completely oblivious to things like back-tracking, parser failures etc., and just combine smaller parsers together as if things would never go wrong, safe in the knowledge that a clever implementation of Bind sorts out all the logic behind the difficult bits. Then later on maybe someone adds logging to the monad, but the code using the monad doesn't change, because all the magic happens in the definition of the Bind operator, the rest of the code is unchanged. Finally, here's the implementation of the same code in Haskell (-- begins a comment line). -- Here's the data type, it's either nothing, or "Just" a value -- this is in the standard library data Maybe a = Nothing | Just a -- The bind operator for Nothing Nothing >>= f = Nothing -- The bind operator for Just x Just x >>= f = f x -- the "unit", called "return" return = Just -- The sample code using the lambda syntax -- that Brian showed z = f >>= ( \fval -> g >>= ( \gval -> h >>= ( \hval -> return (fval+gval+hval ) ) ) ) -- The following is exactly the same as the three lines above z2 = do fval <- f gval <- g hval <- h return (fval+gval+hval) As you can see the nice do notation at the end makes it look like straight imperative code. And indeed this is by design. Monads can be used to encapsulate all the useful stuff in imperative programming (mutable state, IO etc.) and used using this nice imperative-like syntax, but behind the curtains, it's all just monads and a clever implementation of the bind operator! The cool thing is that you can implement your own monads by implementing >>= and return. And if you do so those monads will also be able to use the do notation, which means you can basically write your own little languages by just defining two functions!

你整天在编程中做的大部分事情就是把一些函数组合在一起,然后用它们来构建更大的函数。通常你的工具箱里不仅有函数,还有其他东西,比如操作符、变量赋值等等,但通常你的程序将大量的“计算”组合在一起,形成更大的计算,这些计算将进一步组合在一起。

单子是一种“计算组合”的方式。

通常你将两个计算组合在一起的最基本的“运算符”是;:

a; b

当你这么说的时候,你的意思是“先做a,然后做b”。结果a;B基本上是一个可以结合更多东西的计算。 这是一个简单的单子,它是一种将小计算结合到大计算的方法。的;说"先做左边的事,再做右边的事"

在面向对象语言中,另一个可以被视为单子的东西是..你经常会发现这样的事情:

a.b().c().d()

的。基本上意思是“计算左边的计算,然后调用右边的方法来计算结果”。这是将函数/计算组合在一起的另一种方式,比;稍微复杂一点。还有将事物连接在一起的概念。是一个单子,因为它是一种将两个计算组合在一起形成一个新计算的方式。

另一个相当常见的单子,没有特殊的语法,是这样的模式:

rv = socket.bind(address, port);
if (rv == -1)
  return -1;

rv = socket.connect(...);
if (rv == -1)
  return -1;

rv = socket.send(...);
if (rv == -1)
  return -1;

返回值为-1表示失败,但是没有真正的方法可以抽象出这种错误检查,即使您需要以这种方式组合许多api调用。这基本上只是另一个单子,它根据“如果左边的函数返回-1,我们自己也返回-1,否则调用右边的函数”的规则组合函数调用。如果我们有一个运算符>>=来做这个事情,我们可以简单地写:

socket.bind(...) >>= socket.connect(...) >>= socket.send(...)

这将使事情更具可读性,并有助于抽象出我们组合函数的特殊方式,这样我们就不需要一遍又一遍地重复了。

还有更多的方法来组合函数/计算,这些方法作为一个通用的模式是有用的,并且可以在单子中抽象出来,使单子的用户能够编写更简洁和清晰的代码,因为所有使用的函数的簿记和管理都是在单子中完成的。

例如,上面的>>=可以扩展为“进行错误检查,然后调用我们作为输入的套接字的右侧”,这样我们就不需要多次显式地指定套接字:

new socket() >>= bind(...) >>= connect(...) >>= send(...);

正式的定义有点复杂,因为你必须担心如何将一个函数的结果作为下一个函数的输入,如果那个函数需要那个输入,而且因为你想确保你组合的函数适合你在单子中组合它们的方式。但基本的概念是你将不同的方法形式化来组合函数。

你可以把单子看作是一个c#接口,类必须实现它。这是一个实用主义的答案,忽略了为什么你想要在你的接口中选择这些声明背后的所有类别理论数学,忽略了为什么你想要在一种试图避免副作用的语言中有单子的所有原因,但我发现作为一个理解(c#)接口的人,这是一个很好的开始。

单子本质上是延迟处理。如果你试图用一种语言编写有副作用的代码(例如I/O),而这种语言不允许产生副作用,只允许纯计算,一种逃避方法是说,“好吧,我知道你不会为我做副作用,但你能计算一下如果你做了会发生什么吗?”

这是一种欺骗。

现在,这个解释将帮助你理解单子的总体意图,但魔鬼在细节中。你究竟如何计算后果?有时候,它并不漂亮。

对于习惯于命令式编程的人来说,概述命令式编程的最好方法是说,它将您置于DSL中,其中的操作在语法上与您在单子之外所习惯的操作相似,而不是用于构建一个函数,如果您可以(例如)写入输出文件,则该函数将执行您想要的操作。几乎(但不是真的)就像你在一个字符串中构建代码,然后再进行计算。