197

这些天有很多关于 monad 的讨论。我已经阅读了几篇文章/博客文章,但我不能通过他们的例子走得足够远来完全掌握这个概念。原因是 monad 是一个函数式语言概念,因此这些示例使用的是我没有使用过的语言(因为我没有深入使用函数式语言)。我不能深入地掌握语法以完全遵循这些文章......但我可以说那里有一些值得理解的东西。

但是,我非常了解 C#,包括 lambda 表达式和其他功能特性。我知道 C# 只有一个功能特性的子集,所以也许 monads 不能用 C# 表达。

但是,肯定可以传达这个概念吗?至少我希望如此。也许您可以提供一个 C# 示例作为基础,然后描述 C# 开发人员希望他能从那里做什么但由于该语言缺乏函数式编程特性而无法做到的事情。这将是很棒的,因为它会传达单子的意图和好处。所以这是我的问题:对于 C# 3 开发人员,你能给 monad 最好的解释是什么?

谢谢!

(编辑:顺便说一句,我知道SO上已经有至少3个“什么是monad”问题。但是,我面临同样的问题......所以这个问题是需要imo的,因为C#-developer重点。谢谢。)

4

6 回答 6

154

你整天在编程中所做的大部分事情都是将一些函数组合在一起以从中构建更大的函数。通常,您的工具箱中不仅有函数,还有其他东西,如运算符、变量赋值等,但通常您的程序将大量“计算”组合到更大的计算中,这些计算将进一步组合在一起。

monad 是执行这种“计算组合”的某种方式。

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

a; b

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

在面向对象语言中可以被视为 monad 的另一件事是.. 通常你会发现这样的事情:

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

基本上意味着“评估左侧的.计算,然后根据结果调用右侧的方法”。这是将函数/计算组合在一起的另一种方式,比;. 将事物链接在一起的概念.是一个单子,因为它是将两个计算组合在一起以形成新计算的一种方式。

另一个相当常见的没有特殊语法的 monad 是这种模式:

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 调用需要以这种方式组合。这基本上只是另一个按规则组合函数调用的 monad:“如果左边的函数返回 -1,则自己返回 -1,否则调用右边的函数”。如果我们有一个操作员>>=来做这件事,我们可以简单地写:

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

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

还有更多的方法可以组合函数/计算,这些方法作为通用模式很有用,并且可以抽象到 monad 中,使 monad 的用户能够编写更简洁明了的代码,因为所有的簿记和管理使用的函数是在 monad 中完成的。

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

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

正式定义有点复杂,因为如果该函数需要该输入,并且您想确保组合的函数适合,您必须担心如何将一个函数的结果作为下一个函数的输入您尝试将它们组合到您的 monad 中的方式。但基本概念只是您将不同的方式形式化以将功能组合在一起。

于 2009-03-23T21:15:36.313 回答
47

我发布这个问题已经一年了。发布后,我深入研究了 Haskell 几个月。我非常喜欢它,但是当我准备深入研究 Monads 时,我把它放在一边。我回去工作,专注于我的项目所需的技术。

昨晚,我来重读了这些回复。最重要的是,我重新阅读了上面有人提到的 Brian Beckman 视频的文本评论中的特定 C# 示例。它是如此的清晰和有启发性,我决定直接在这里发布。

由于这个评论,我不仅觉得我完全理解 Monad 是什么……我意识到我实际上已经用 C# 编写了一些Monad 的东西……或者至少非常接近,并努力解决相同的问题。

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

这很酷。虽然有点抽象。我可以想象那些不知道单子是什么的人已经因为缺乏真实的例子而感到困惑。

因此,让我尝试遵守,并且为了非常清楚,我将在 C# 中做一个示例,即使它看起来很丑。我将在最后添加等效的 Haskell 并向您展示很酷的 Haskell 语法糖,这是 IMO monad 真正开始变得有用的地方。

好的,所以最简单的 Monad 之一在 Haskell 中被称为“Maybe monad”。在 C# 中,Maybe 类型称为Nullable<T>. 它基本上是一个很小的类,它只是封装了一个值的概念,该值要么是有效的并且有一个值,要么是“null”并且没有值。

坚持在 monad 中组合这种类型的值的一个有用的东西是失败的概念。即,我们希望能够查看多个可为空的值,并null在其中任何一个为空时立即返回。例如,如果您在字典或其他东西中查找大量键,最后您想要处理所有结果并以某种方式组合它们,但如果任何键不在字典中,这可能很有用,你想null为整个事情回来。手动检查每个查找并返回会很乏味 null,因此我们可以将这种检查隐藏在绑定运算符中(这有点像 monads,我们将簿记隐藏在绑定运算符中,这使得代码更容易使用,因为我们可以忘记细节)。

这是激发整个事情的程序(我稍后会定义 Bind,这只是为了向您展示为什么它很好)。

 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();
        }
    }

现在,暂时忽略在 C# 中已经支持执行此Nullable操作(您可以将可为空的整数添加在一起,如果其中一个为空,则为空)。让我们假设没有这样的功能,它只是一个用户定义的类,没有什么特殊的魔法。关键是我们可以使用该Bind函数将变量绑定到我们的Nullable值的内容,然后假装没有发生什么奇怪的事情,并像使用普通整数一样使用它们,然后将它们加在一起。我们将结果包装在最后的可空值中,并且该可空值将是空值(如果有的话fg或者h返回空值)或者它将是求和的结果fgh一起。(这类似于我们如何将数据库中的行绑定到 LINQ 中的变量,并对其进行处理,知道Bind操作员将确保变量只会被传递有效的行值是安全的)。

您可以使用它并更改任何fgh以返回 null,您将看到整个事情将返回 null。

所以很明显,bind 操作符必须为我们做这个检查,如果遇到 null 值,则返回 null ,否则将结构内的值传递Nullable给 lambda。

这是Bind运算符:

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;
}

这里的类型和视频中的一样。在这种情况下,它需要一个M a (Nullable<A>在 C# 语法中) 和一个函数 from ato M b(Func<A, Nullable<B>>在 C# 语法中),它返回一个M b ( Nullable<B>)。

代码只是检查可空值是否包含一个值,如果是,则提取它并将其传递给函数,否则它只返回空值。这意味着Bind操作员将为我们处理所有的空值检查逻辑。当且仅当我们调用的值 Bind不为空时,该值才会“传递”给 lambda 函数,否则我们会提前退出,整个表达式为空。这允许我们使用 monad 编写的代码完全没有这种 null 检查行为,我们只需使用Bind并获取一个绑定到 monadic 值(和示例代码中)内的值的变量,我们可以安全地使用fval它们 知道在传递它们之前会检查它们是否为空。gvalhvalBind

还有其他可以用 monad 做的事情的例子。例如,您可以让Bind操作员处理输入的字符流,并使用它来编写解析器组合器。然后,每个解析器组合器可以完全忘记诸如回溯、解析器故障等之类的事情,而只是将较小的解析器组合在一起,就好像事情永远不会出错一样,安全地知道一个聪明的实现会Bind整理出所有背后的逻辑困难的位。然后稍后可能有人将日志记录添加到 monad,但是使用 monad 的代码没有改变,因为所有的魔法都发生在Bind操作符的定义中,其余的代码没有改变。

最后,这是 Haskell 中相同代码的实现(-- 开始注释行)。

-- 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)

正如你所看到的do,最后的漂亮符号使它看起来像直接的命令式代码。事实上,这是设计使然。Monad 可用于封装命令式编程中所有有用的东西(可变状态、IO 等),并使用这种漂亮的命令式语法来使用,但在幕后,这一切都只是 monad 和绑定运算符的巧妙实现!>>=很酷的是,您可以通过实现和来实现自己的 monad return。如果你这样做了,这些 monad 也可以使用do符号,这意味着你基本上可以通过定义两个函数来编写自己的小语言!

于 2010-03-20T17:56:47.843 回答
11

monad 本质上是延迟处理。如果你试图用一种不允许它们并且只允许纯计算的语言编写具有副作用(例如 I/O)的代码,一个躲闪的方法是说,“好吧,我知道你不会做副作用对我来说,但你能计算一下如果你这样做会发生什么吗?”

这是一种欺骗。

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

概述某人习惯于命令式编程的最佳方式是说它把你放在一个 DSL 中,其中在语法上看起来像你习惯于 monad 之外的操作被用来构建一个可以做的函数如果您可以(例如)写入输出文件,您想要什么。几乎(但不是真的)就好像您正在构建一个字符串中的代码以便稍后进行评估。

于 2009-03-23T19:25:31.477 回答
4

我相信其他用户会深入发布,但我发现这个视频在一定程度上很有帮助,但我会说我仍然不能流利地理解这个概念,所以我可以(或应该)开始解决Monads 的直观问题。

于 2009-03-23T19:25:49.837 回答
0

您可以将 monad 视为类必须实现的 C#interface。这是一个务实的答案,它忽略了为什么您要选择在界面中包含这些声明背后的所有类别理论数学,并忽略了您希望在一种试图避免副作用的语言中使用 monad 的所有原因,但我发现对于了解(C#)接口的人来说,这是一个好的开始。

于 2009-03-23T19:36:10.937 回答
0

请参阅我对“什么是单子?”的回答。

它从一个激励性的例子开始,通过这个例子,推导出一个 monad 的例子,并正式定义“monad”。

它假定没有函数式编程知识,并且使用function(argument) := expression具有最简单表达式的语法的伪代码。

这个 C# 程序是伪代码 monad 的实现。(供参考:M是类型构造函数,feed是“绑定”操作,wrap是“返回”操作。)

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);
    }
}
于 2015-03-08T21:31:53.197 回答