在C#中,什么是单子?


189

这些天有很多关于单子的讨论。我已经阅读了几篇文章/博客文章,但是对于他们的例子我无法走得太远,无法完全理解这个概念。原因是monad是一种功能语言概念,因此示例使用的是我未曾使用过的语言(因为我没有深入使用功能语言)。我不能足够深入地理解语法以完全阅读本文……但是我可以告诉您那里有一些值得理解的地方。

但是,我非常了解C#,包括lambda表达式和其他功能。我知道C#仅具有部分功能,因此也许monads无法用C#表示。

但是,肯定可以传达这个概念吗?至少我希望如此。也许您可以提出一个C#示例作为基础,然后描述C#开发人员希望他从那里做些什么,但是不能做,因为该语言缺少功能性的编程功能。这将是很棒的,因为它将传达单子的意图和好处。所以这是我的问题:对C#3开发人员来说,可以给monad最好的解释是什么?

谢谢!

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


请注意,它实际上是C#3.0开发人员。请勿将其与.NET 3.5混淆。除此之外,很好的问题。
拉齐

3
值得指出的是,LINQ查询表达式是C#3的单子行为的一个例子
埃里克·福布斯

1
我仍然认为这是一个重复的问题。stackoverflow.com/questions/2366/can-anyone-explain-monads中的答案之一链接到 channel9vip.orcsweb.com/shows/Going+Deep/…,其中的注释中有一个很好的C#示例。:)
杰夫

4
尽管如此,那仅仅是从一个答案到一个SO问题之一的链接。在针对C#开发人员的问题中,我看到了价值。我会问一位曾经做过C#的函数式程序员是否知道这一点,因此在SO上询问它似乎是合理的。但我也尊重您的见解权利。
查理·弗罗里斯2009年

1
虽然不是您需要的所有答案?;)我的意思是,其他问题之一(现在也是如此),确实有一个C#特定的答案(实际上看起来写得不错,也许是我见过的最好的解释)
杰夫

Answers:


147

整天编程中的大部分工作是将一些函数组合在一起,以从中构建更大的函数。通常,您不仅在工具箱中具有函数,而且还具有其他诸如运算符,变量赋值之类的东西,但是通常您的程序将许多“计算”组合在一起,形成更大的计算量,这些计算量将进一步组合在一起。

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

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

a; b

说这句话的意思是“先做a,然后做b”。结果a; b基本上又是可以与更多内容组合在一起的计算。这是一个简单的monad,它是将小型计算与大型计算结合的一种方式。该;说“我们该做的左侧,再做右侧的事”。

可以在面向对象语言中视为monad的另一件事是.。通常,您会发现以下内容:

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

.基本意思是“对左侧的计算,然后调用上的这个结果正确的方法”。这是将函数/计算组合在一起的另一种方式,比复杂一些;。链接事物的概念.是monad,因为它是将两个计算组合在一起形成新计算的一种方式。

另一个没有特殊语法的相当普通的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中的方式。但是,基本概念只是形式化将功能组合在一起的不同方法。


28
好答案!我要引用Oliver Steele的报价,试图将Monad与运算符重载àla C ++或C#联系起来:Monad允许您重载';'。操作员。
约尔格W¯¯米塔格

6
@JörgWMittag我以前读过那句话,但听起来像是胡说八道。现在,我确实了解单子,并阅读了有关“;”的解释。是一个,我明白了。但是我认为对于大多数命令性开发人员来说,这确实是不合理的说法。';' 在大多数情况下,//不再被视为运算符。
吉米·霍法

2
您确定知道什么是单子吗?Monad不是“函数”或计算,Monad有规则。
路易斯

在您的;示例中:;映射哪些对象/数据类型?(认为List映射TList<T>)如何;在对象/数据类型之间映射态射/函数?什么是purejoinbind;
Micha Wiedenmann

44

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

昨晚,我来重新阅读了这些回复。更重要的是,我重读了特定C#示例中的文本注释的布赖恩·贝克曼视频有人提到以上。它是如此的清晰和有启发性,以至于我决定将其直接发布在这里。

正因为如此评论,而不是只做我觉得我明白究竟是什么单子......我意识到我其实C#编写的一些事情,单子......或者至少非常接近,努力解决同样的问题。

所以,这里的评论-这是所有直接引用此评论西尔万

这很酷。虽然有点抽象。我可以想象,由于缺乏真实的例子,不知道什么单子的人已经感到困惑。

因此,让我尝试遵守,并且要明确一点,尽管它看起来很丑,但我仍将使用C#进行示例。我将在末尾添加等效的Haskell,并向您展示凉爽的Haskell语法糖,这是IMO,单核函数真正开始变得有用的地方。

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

插入单子以组合此类型的值时有用的事情是失败的概念。也就是说,我们希望能够查看多个可null为空的值,并在其中任何一个为空时立即返回。例如,如果您在字典中查找大量关键字或其他内容,并且最后要处理所有结果并以某种方式组合它们,则这可能会很有用,但是如果任何关键字不在词典中,你想null整个回来。手动检查每个查找 null并返回很麻烦,因此我们可以将此检查隐藏在bind运算符内(这是单子的要点,我们将簿记隐藏在bind运算符中,这使代码更容易使用,因为我们可以忘记细节)。

这是激发整个过程的程序(我将在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();
        }
    }

现在,暂时忽略一下,Nullable在C#中已经为此提供了支持(您可以将可为null的int一起添加,如果其中任何一个为null,则为null)。让我们假装没有这种功能,它只是一个用户定义的类,没有特殊的魔术。关键是我们可以使用该Bind函数将变量绑定到Nullable值的内容,然后假装没有什么奇怪的事情,可以像正常int一样使用它们并将它们加在一起。我们总结的结果在最后一空,并为空的要么是空(如果有的话fgh返回null)或将是总结的结果fgh一起。(这类似于我们如何将数据库中的行绑定到LINQ中的变量,并对其进行填充,这是安全的,前提是Bind操作员将确保仅向变量传递有效的行值)。

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

因此,很显然,bind运算符必须为我们执行此检查,并在遇到空值时避免返回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 aNullable<A>在这种情况下,使用C#语法)和一个函数ato M bFunc<A, Nullable<B>>在C#语法中),然后返回M bNullable<B>)。

该代码仅检查nullable是否包含值,如果是则将其提取并将其传递给函数,否则它仅返回null。这意味着Bind操作员将为我们处理所有空检查逻辑。当且仅当我们调用的值 Bind不为null时,该值才会“传递”给lambda函数,否则我们会尽早纾困,并且整个表达式为null。这允许我们编写使用的单子是完全免费的这个空检查行为,我们只是使用的代码Bind,并获得绑定到一元价值内值的变量(fvalgvalhval在示例代码),我们可以使用他们的安全Bind会在传递它们之前检查它们是否为null 的知识。

您还可以使用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等)中的所有有用内容,并使用这种类似于命令式的漂亮语法使用,但是在幕后,它们全都是monads和bind操作符的巧妙实现!很酷的事情是,您可以通过实现>>=和来实现自己的monad return。如果这样做,这些单子也将能够使用该do表示法,这意味着您只需定义两个函数就可以基本上编写自己的小语言!


3
我个人更喜欢F#版本的monad,但无论哪种情况,它们都很棒。
ChaosPandion

3
感谢您回到这里并更新您的帖子。像这样的后续活动可以帮助正在研究特定领域的程序员真正了解其他程序员最终如何看待所述领域,而不是简单地放弃“我如何在技术中进行应用”。你这个家伙!
kappasims 2011年

我走过的路基本与您到达了理解monad的地方,这是我对命令式开发人员所见过的monad绑定行为的唯一最好的解释。尽管我认为您不太了解monad的所有内容,但在上文中已对此稍作解释。
吉米·霍法

@Jimmy Hoffa-毫无疑问,你是对的。我认为要真正深入了解它们,最好的方法是开始大量使用它们并获得经验。我还没有机会,但是希望很快。
查理·弗劳斯

从编程的角度看,monad似乎只是一个更高层次的抽象,或者它只是数学中一个连续且不可微的函数定义。无论哪种方式,它们都不是新概念,尤其是在数学中。
2013年

11

monad本质上是延迟处理的。如果您试图用一种不允许有副作用的语言(例如I / O)编写代码,而只允许进行纯计算,那么一个闪避就是说:“好吧,我知道您不会产生副作用对我来说,但是请您计算一下如果发生了会怎样?”

这有点作弊。

现在,这种解释将帮助您了解monad的整体意图,但细节在于魔鬼。究竟是如何你计算的后果是什么?有时,它不是很漂亮。

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


1
就像在《我的机器人》一书中一样?科学家在哪里要求计算机计算太空旅行并要求他们跳过某些规则?:):) :) :)
OscarRyz

3
嗯,一个Monad 可以用于延迟处理和封装副作用功能,的确是那是Haskell中的第一个实际应用程序,但实际上它是一种更为通用的模式。其他常见用途包括错误处理和状态管理。语法糖(在Haskell中使用,在F#中使用计算表达式,在C#中使用Linq语法)仅仅是这样,对Monads也是如此。
Mike Hadlow

@MikeHadlow:用于错误处理(MaybeEither e)和状态管理()的monad实例令我震惊State sST s因为它们是“请计算如果您对我有[副作用]会发生什么”的特定实例。另一个例子是不确定性([])。
2015年

这是完全正确的;用一个(很好,两个)加法表示它是E DSL,即嵌入式 DSL,因为每个“单调”值都是“纯”语言本身的有效值,表示潜在的不纯“计算”。此外,在您的纯语言中,存在一个单子的“ bind”构造,该构造使您可以链接这些值的纯构造函数整个组合计算“运行” ,将使用其先前计算的结果来调用每个值。这意味着我们有能力根据将来的结果(或在任何情况下,单独的“运行”时间轴)进行分支。
尼斯

但是对于程序员来说,这意味着我们可以在EDSL中进行编程,同时将其与纯语言的纯计算混合在一起。一叠多层三明治是多层三明治。就这么简单。
尼斯·威尔斯'18

4

我确定其他用户也会对此进行深入介绍,但是我发现该视频在一定程度上有所帮助,但我要说的是,我仍然不太理解这个概念,因此我可以(或应该)开始解决Monads直观的问题。


1
我发现更有用的是在视频下方包含C#示例的评论。
jalf

我不愿意提供更多帮助,但是确实可以将这些想法付诸实践。
TheMissingLINQ 2009年

0

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


你能详细说明吗?与单子相关的接口有什么作用?
Joel Coehoorn

2
我认为该博客帖子花了几段专门讨论该问题。

0

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

它从一个具有启发性的示例开始,通过示例进行工作,派生出monad的示例,并正式定义“ monad”。

它不具备任何函数式编程知识,并且使用伪代码和function(argument) := expression语法以及尽可能简单的表达式。

此C#程序是伪代码monad的实现。(供参考: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);
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.