帮助C#开发人员了解:什么是monad?

这些天有很多关于monad的讨论。 我已经阅读了一些文章/博客文章,但是我不能用他们的例子去充分理解这个概念。 原因是monad是一个function性的语言概念,因此这些例子是用我没有用过的语言(因为我没有深入地使用函数式语言)。 我不能够深入地把握这个语法来完全遵循这些文章,但是我可以告诉我们这里有一些值得理解的东西。

但是,我非常了解C#,包括lambdaexpression式和其他function特性。 我知道C#只有一个function特性的子集,所以monad不能用C#表示。

但是,当然可以传达这个概念吗? 至less我希望如此。 也许你可以提供一个C#的例子作为基础,然后描述一下C#开发人员希望他能从那里做什么,但不能,因为这种语言缺乏函数式编程function。 这将是太棒了,因为它会传达单子的意图和好处。 所以这里是我的问题: 对于C#3开发人员来说,可以给monads最好的解释是什么?

谢谢!

(编辑:顺便说一下,我知道至less有3个“monad”是什么问题,但是我面对同样的问题…所以这个问题是需要imo的,因为C#开发人员重点,谢谢。)

你整天编程的大部分function是把一些function结合起来,从而build立更大的function。 通常,你不仅在你的工具箱中有函数,还有其他一些东西,比如运算符,variables赋值等,但是通常你的程序把大量的“计算”结合到一起,进一步结合在一起。

monad是一种“计算组合”的方法。

通常你最基本的“算子”将两个计算结合在一起;

 a; b 

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

在面向对象语言中另一个可以被看作monad的东西是. 。 通常你会发现这样的事情:

 ab().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(...) 

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

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

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

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

正式的定义有点复杂,因为你不得不担心如何得到一个函数的结果作为下一个函数的input,如果这个函数需要这个input,并且你想确保你组合的函数适合你试图把它们组合在你的monad中。 但是基本的概念只不过是把不同的function组合在一起。

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

昨天晚上,我来重读这些回复。 最重要的是 ,我在上面提到 的Brian Beckmanvideo的文本评论中重新阅读了具体的C#示例 。 这是完全清楚,照亮,我决定直接在这里发布。

因为这个意见,不仅我觉得我明白Monads是什么…我知道我已经用C#写了一些Monad的东西…或者至less非常接近,并且努力解决同样的问题。

所以,这里的评论 – 这是从sylvan 的评论这里直接引用:

这很酷。 这虽然有点抽象。 我可以想象,由于缺乏实际的例子,不知道单子的人已经被弄糊涂了。

所以,让我试着去遵守,而且要真正清楚我会在C#中做一个例子,即使它看起来很丑。 我将在最后添加相当于Haskell的内容,并向您展示酷酷的Haskell语法糖,这是IMO,monads真正开始变得有用的地方。

好吧,所以最简单的Monad之一在Haskell被称为“Maybe monad”。 在C#中,Maybetypes被称为Nullable<T> 。 它基本上是一个微小的类,只是封装了一个值的概念,它是有效的,有一个值,或者是“空的”,没有价值。

坚持在monad中结合这种types值的一个有用的东西是失败的概念。 也就是说,我们希望能够查看多个可为null的值,并在其中任何一个为null立即返回null。 例如,如果您在字典中查找大量的键,并且最后想要处理所有结果并以某种方式将它们组合在一起,那么这可能很有用,但是如果任何键不在字典中,你想为整个事情返回null 。 手动检查每个查找是否为null并且返回是非常繁琐的,所以我们可以在bind操作符中隐藏这个检查(这就是monads的要点,我们在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(); } } 

现在,忽略一下,C#中的Nullable已经有了支持(你可以一起添加可为null的int,如果null为null,你将得到null)。 让我们假装没有这样的function,它只是一个没有特殊魔法的用户定义的类。 关键是我们可以使用Bind函数将一个variablesBind到我们的Nullable值的内容上,然后假装没有什么奇怪的事情,像正常的整数一样使用它们,然后把它们加在一起。 我们用最后一个可为空的结果来包装结果,并且可以为null(如果fgh任何一个返回null),或者它将是fgh相加的结果。 (这与我们如何将数据库中的行绑定到LINQ中的variables类似,并且知道Bind运算符将确保该variables只能传递有效的行值,这是安全的。

你可以玩这个,改变任何fgh返回null,你会看到整个事情将返回null。

所以很显然,绑定操作符必须为我们执行这个检查,如果遇到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; } 

这里的types就像在video中一样。 它需要一个M a (在这种情况下C#语法中的Nullable<A> )和a到M bFunc<A, Nullable<B>> C#语法中的Func<A, Nullable<B>>的函数,并返回一个M bNullable<B> )。

代码简单地检查可为空的值是否包含一个值,如果是,则将其提取并传递给函数,否则返回null。 这意味着Bind操作符将为我们处理所有的空值检查逻辑。 当且仅当我们调用Bind的值非空时,那么这个值将被传递给lambda函数,否则我们提前退出,整个expression式为空。 这允许我们使用monad编写的代码完全没有这种空值检查行为,我们只需使用Bind并获得一个绑定到fval值(在示例代码中的fvalgvalhval中的值的fval ,而我们可以使用它们的安全知识, Bind会照顾检查他们为null之前通过他们。

还有其他一些你可以用monad做的事情。 例如,可以让Bind操作符处理input的字符stream,并使用它来编写parsing器组合器。 然后,每个parsing器组合器可以完全忽略诸如回溯,parsing器故障等,并且只是将较小的parsing器组合在一起,就好像事情永远不会出错一样,安全的知识是巧妙的Bind实现将所有的逻辑困难的一点。 然后稍后也许有人向monad添加日志logging,但是使用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 = fx -- 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 ,最后的好记法使它看起来像是直接的命令式的代码。 事实上,这是devise。 Monad可以用来封装命令式编程(可变状态,IO等)中的所有有用的东西,并且使用这个很好的命令式语法来使用,但是在幕后,这只是monad和绑定操作符的巧妙实现! 很酷的是,你可以通过实现>>=return来实现你自己的monads。 如果你这样做,那些monad也可以使用符号,这意味着你可以通过定义两个函数来基本编写自己的小语言!

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

这是一种作弊。

现在,这个解释会帮助你理解单子的全貌,但魔鬼是在细节中。 你究竟如何计算后果? 有时候,这并不漂亮。

概述一下用户如何使用命令式编程的最好方法就是说,它把你放在一个DSL中,其中在语法上看起来像你在monad之外使用的操作被用来构build一个函数,如果你可以(例如)写入输出文件,你想要什么。 几乎(但不是真的),就好像你在一个string中构build代码,以后将被评估。

我相信其他用户会深入的发表,但是我发现这个video在一定程度上是有用的,但是我会说我还没有达到stream利的概念,以至于我可以(或者应该)开始解决Monads直观的问题。

您可以将monad看作是类必须实现的C# interface 。 这是一个实用的答案,忽略了为什么你想select在你的界面中使用这些声明的所有类别的理论math,并忽略了所有的原因,为什么你想要一个语言,monad试图避免副作用,但是我发现作为理解(C#)接口的人是一个好开始。

看到我对“什么是monad?”的回答

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

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

这个C#程序是伪代码monad的一个实现。 (供参考: M是types构造函数, 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); } }