帮助C#开发人员理解:什么是monad?
这些天有很多关于monad的讨论。 我已经阅读了一些文章/博客文章,但我无法用他们的例子去充分掌握这个概念。 原因是monad是一个功能性语言概念,因此这些例子都是我没有使用过的语言(因为我没有深入地使用函数式语言)。 我无法深入地理解语法以充分跟踪文章......但我可以告诉那里有值得理解的东西。
但是,我非常了解C#,包括lambda表达式和其他功能特性。 我知道C#只有一部分功能特性,所以也许monad不能用C#表达。
但是,当然可以传达这个概念吗? 至少我希望如此。 也许你可以提供一个C#示例作为基础,然后描述C#开发人员希望他可以从那里做什么,但不能这样做,因为该语言缺乏函数式编程功能。 这将是太棒了,因为它会传达单子的意图和好处。 所以,这里是我的问题: 您可以给C#3开发人员提供monads的最佳解释是什么?
谢谢!
(编辑:顺便说一句,我知道至少有3个“单子”是什么,但是,我面临着同样的问题...所以这个问题需要imo,因为C#开发人员重点,谢谢。)
你整天编程的大部分功能是将一些功能组合在一起,从而构建更大的功能。 通常,你不仅在你的工具箱中有函数,而且还有其他一些东西,比如运算符,变量赋值等,但是通常你的程序将大量的“计算”结合到更大的计算中,这些计算将进一步结合在一起。
monad是一种“结合计算”的方法。
通常你最基本的“操作员”将两个计算结合在一起;
:
a; b
当你这样说时,你的意思是“先做a
,然后做b
”。 结果a; b
a; b
基本上又是一个可以与更多东西结合在一起的计算。 这是一个简单的monad,它是将较小的计算结合到较大的计算的一种方法。 The ;
说:“做左边的事情,然后做右边的事情”。
另一件可以在面向对象的语言中看作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中的方式。 但基本的概念只不过是你将不同的方法组合在一起。
我发布这个问题已经有一年了。 发布后,我钻研了几个月的Haskell。 我非常喜欢它,但是当我准备好钻研蒙纳德时,我把它放在一边。 我回去工作,专注于我的项目所需的技术。
昨天晚上,我来重读这些回复。 最重要的是,我重新阅读了上面提到的Brian Beckman视频的文本评论中的具体C#示例。 它非常清晰明了,我决定直接在这里发布。
由于这个评论,我不仅觉得我完全明白Monads是什么......我意识到我已经用C#写了一些Monad的东西......或者至少非常接近,并且努力解决相同的问题。
所以,这里是评论 - 这是所有来自sylvan评论的直接引用:
这很酷。 这虽然有点抽象。 我可以想象,由于缺乏真实的例子,不知道单子的人已经感到困惑。
所以让我试着遵守,而且要清楚地说明,我将在C#中做一个例子,即使它看起来很丑。 我会在最后添加相应的Haskell,并向您展示酷酷的Haskell语法糖,这是IMO,monads真正开始变得有用的地方。
好吧,所以最简单的Monad之一在Haskell被称为“Maybe monad”。 在C#中,Maybe类型被称为Nullable<T>
。 它基本上是一个微小的类,它只是封装了一个有效并且有一个值的概念,或者是“null”并且没有任何价值。
坚持在单子中结合这种类型的价值的一个有用的东西是失败的概念。 也就是说,我们希望能够查看多个可为null的值,并在其中的任何一个为null
立即返回null。 例如,如果你在字典中查找很多键,并且最后想要处理所有结果并以某种方式将它们组合起来,那么这会很有用,但是如果任何键不在字典中,你想为整个事情返回null
。 手动检查每个查找是否为null
并返回是非常乏味的,因此我们可以在bind操作符中隐藏这种检查(这就是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
执行此操作(您可以一起添加可为null的int,如果其中任何一个为null,您将获得null)。 让我们假装没有这样的功能,它只是一个用户定义的类,没有特殊的魔法。 关键是我们可以使用Bind
函数将变量Bind
到我们的Nullable
值的内容,然后假装没有什么奇怪的事情发生,并像普通整数一样使用它们,并将它们加在一起。 我们在最后将结果包装为可空,并且可以为空(如果f
, g
或h
任何一个返回null),或者它将是将f
, g
和h
一起求和的结果。 (这与我们如何将数据库中的一行绑定到LINQ中的变量类似,并且知道Bind
运算符将确保该变量只能传递有效的行值)。
你可以玩这个,改变f
, g
和h
任何一个以返回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;
}
这里的类型就像在视频中一样。 它需要一个M a
( Nullable<A>
从这种情况下,C#语法),和一个功能a
到M b
( Func<A, Nullable<B>>
在C#语法),并返回一个M b
( Nullable<B>
)。
代码简单地检查nullable是否包含一个值,如果是,则将其提取并传递给函数,否则它只返回null。 这意味着Bind
操作符将为我们处理所有的空值检查逻辑。 当且仅当我们调用Bind
on的值非空时,那个值将被“传递”给lambda函数,否则我们提前退出并且整个表达式为空。 这允许我们使用monad编写的代码完全没有这种空值检查行为,我们只是使用Bind
并获得绑定到fval
值内的值的变量(示例代码中的fval
, gval
和hval
),而我们可以安全使用它们,因为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
,最后的好记号使它看起来像是直接的命令式代码。 事实上这是设计。 Monads可用于封装命令式编程(可变状态,IO等)中的所有有用的东西,并使用这种很好的命令式语法来使用,但在幕后,它们都只是monad和绑定操作符的巧妙实现! 最酷的是你可以通过实现>>=
和return
来实现你自己的monads。 如果你这样做的单子还可以使用do
符号,这意味着你基本上可以通过只定义两个函数编写自己的小语言!
monad基本上是延迟处理。 如果你想用不允许它们的语言编写有副作用的代码(例如I / O),并且只允许纯计算,那么闪避就是说:“好吧,我知道你不会做副作用对我来说,但是请你计算一下如果你做了什么会发生什么?“
这有点作弊。
现在,这个解释将帮助你理解单子的全局意图,但魔鬼在细节中。 你究竟如何计算后果? 有时候,它不漂亮。
概述某人习惯于命令式编程的最佳方式是说,它将您置于DSL中,其中使用语法上看起来像您在单子之外使用的操作,而不是用于构建可以执行如果可以(例如)写入输出文件,则需要什么。 几乎(但不是真的),就好像你在一个字符串中构建代码,以后将被评估。
链接地址: http://www.djcxy.com/p/47673.html