在c#中,什么是单子?

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

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

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

谢谢!

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

35600 次浏览

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

这是一种欺骗。

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

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

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

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

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

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

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

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

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

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

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

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

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

昨晚,我又读了一遍这些回复。最重要的是,我在布莱恩·贝克曼的视频某人上面提到了的文本注释中重读了具体的c#示例。它是如此清晰和启发性,我决定直接张贴在这里。

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

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

这太酷了。这有点抽象。我可以想象 谁不知道什么单子已经弄糊涂了,由于缺乏 真正的例子。< / p > 所以让我试着遵守,为了更清楚,我会用an 示例,即使它看起来很丑。我会加上等价物 Haskell的最后,给你们展示Haskell的语法糖 在我看来,单子真的开始有用了。

其中一个最简单的单子叫做Maybe单子 Haskell。在c#中,Maybe类型称为Nullable<T>。它基本上是 一个很小的类,它封装了值的概念

.

.

.

.

. 一个有用的东西粘在一个单子的值组合这个 类型是失败的概念。也就是说,我们希望能够看到 多个可空值,并在其中任何一个返回null 是零。这可能会很有用,如果你,例如,查找很多 字典之类的键,最后你想处理 所有的结果,并以某种方式组合它们,但如果有任何键 不在字典中,你想返回null为整个 事情手动检查每一个查找将是乏味的 null和返回,所以我们可以将这个检查隐藏在绑定中 操作符(这是单子的要点,我们隐藏簿记 在绑定操作符中,这使得代码更容易使用,因为我们可以

.

.

. 下面是驱动整个事情的程序(我将定义 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();
}
}
现在,暂时忽略已经有人支持这样做了 for c#中的Nullable(你可以把可为空的整数加在一起,你会得到 如果任意一个为空,则为空)。让我们假设没有这样的特征, 它只是一个用户定义的类,没有特殊的魔力。重点是 我们可以使用Bind函数将变量绑定到内容 Nullable的值,然后假装没有什么奇怪的 继续,像普通整数一样使用它们,然后把它们加在一起。我们 最后将结果包装在一个可空对象中,这个可空对象就会被执行 如果fgh中的任何一个返回null,则它将为null fgh相加的结果。(这是类似的 如何将数据库中的一行绑定到LINQ中的变量,并执行 在Bind操作符将会处理它的知识中,它是安全的 确保变量只被传递有效行 值). < / p > 你可以使用这个函数并更改fgh中的任何一个来返回

显然,绑定操作符必须为我们做这个检查,然后保释 Out如果遇到空值则返回null,否则pass 沿着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 (本例中c#语法中的Nullable<A>),以及a to的函数 M b (c#语法中的Func<A, Nullable<B>>),它返回一个M b (Nullable<B>)。< / p > 代码只是检查nullable是否包含值,如果有 提取它并将它传递给函数,否则它就会返回 null。这意味着Bind操作符将处理所有 空检查逻辑。当且仅当我们调用的值 Bind on为非空,则该值将被“传递”到 函数,否则我们会提前跳出整个表达式 null。这允许我们使用单子写的代码 完全没有这种空检查行为,我们只使用Bind和 获取一个绑定到一元值中的值的变量(fvalgvalhval在示例代码中),我们可以安全地使用它们 知道Bind会在之前检查它们是否为空

你可以用单子做一些其他的事情。为 例如,你可以让Bind操作符处理输入流 ,并使用它来编写解析器组合子。每个解析器 这样,Combinator就可以完全忽略一些事情 回溯,解析器失败等等,只是合并较小的解析器 在一起,好像永远不会出差错,因为知道 Bind的一个聪明的实现整理了对象背后的所有逻辑 困难的部分。然后稍后可能有人将日志添加到单子, 但是使用单子的代码不会改变,因为所有的魔法 发生在Bind操作符的定义中,其余的代码 是不变的。< / p > 最后,下面是相同代码在Haskell (-- . xml)中的实现

.开始注释行)
-- 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符号在结尾使它看起来像 直接的命令式代码。事实上,这是有意为之。单子可以是 用于封装命令式编程中所有有用的东西 (可变状态,IO等)并使用这种漂亮的命令式 语法,但在窗帘后面,这一切只是单子和一个聪明的 绑定操作符的实现!最酷的是你可以做到 通过实现>>=return来实现你自己的单子。如果 你这样做,这些单子也可以使用do符号, 这意味着你基本上可以编写自己的小语言 定义两个函数!< / p >

参见回答到“什么是单子?”

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

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

这个c#程序是伪代码单子的实现。(供参考: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);
}
}