我发布这个问题已经一年了。发布后,我深入研究了 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
值的内容,然后假装没有发生什么奇怪的事情,并像使用普通整数一样使用它们,然后将它们加在一起。我们将结果包装在最后的可空值中,并且该可空值将是空值(如果有的话f
,g
或者h
返回空值)或者它将是求和的结果f
,g
和h
一起。(这类似于我们如何将数据库中的行绑定到 LINQ 中的变量,并对其进行处理,知道Bind
操作员将确保变量只会被传递有效的行值是安全的)。
您可以使用它并更改任何f
、g
和h
以返回 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 a
to
M b
(Func<A, Nullable<B>>
在 C# 语法中),它返回一个M b
( Nullable<B>
)。
代码只是检查可空值是否包含一个值,如果是,则提取它并将其传递给函数,否则它只返回空值。这意味着Bind
操作员将为我们处理所有的空值检查逻辑。当且仅当我们调用的值
Bind
不为空时,该值才会“传递”给 lambda 函数,否则我们会提前退出,整个表达式为空。这允许我们使用 monad 编写的代码完全没有这种 null 检查行为,我们只需使用Bind
并获取一个绑定到 monadic 值(和示例代码中)内的值的变量,我们可以安全地使用fval
它们
知道在传递它们之前会检查它们是否为空。gval
hval
Bind
还有其他可以用 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
符号,这意味着你基本上可以通过定义两个函数来编写自己的小语言!