3 回答
TA贡献1811条经验 获得超5个赞
更新:这个问题是一个非常长的博客系列的主题,您可以在Monads上阅读它 -感谢您提出的伟大问题!
用OOP程序员会理解的术语(没有任何函数式编程背景),什么是monad?
monad是一种类型的“放大器”,它遵循某些规则并提供某些操作。
首先,什么是“类型放大器”?我的意思是说,有一个系统可以让您选择一种类型并将其转换为更特殊的类型。例如,在C#中考虑Nullable<T>。这是一种放大器。它使您可以使用一个类型,例如int,并为该类型添加新功能,即现在可以在以前无法使用时为null。
作为第二个示例,请考虑IEnumerable<T>。它是一种类型的放大器。它允许您采用一个类型,例如,string并为该类型添加新功能,即,您现在可以从任意数量的单个字符串中构成一个字符串序列。
什么是“某些规则”?简而言之,存在一种合理的方式,使基础类型上的功能在放大类型上起作用,从而使它们遵循功能组成的正常规则。例如,如果您有一个整数函数,请说
int M(int x) { return x + N(x * 2); }
则相应的on函数Nullable<int>可以使所有运算符和其中的调用与以前一样“以相同的方式”协同工作。
(这是非常模糊和不精确的;您要求提供一种解释,该解释没有假定任何有关功能组成的知识。)
什么是“操作”?
有一个“单位”操作(有时也称为“返回”操作),该操作从普通类型获取值并创建等效的单价值。本质上,这提供了一种获取未放大类型的值并将其转换为放大类型的值的方法。可以将其实现为OO语言的构造函数。
有一个“绑定”操作,它接受一个单子值和一个可以转换该值并返回新单子值的函数。绑定是定义monad语义的关键操作。它使我们可以将未放大类型的操作转换为对放大类型的操作,这要遵循前面提到的功能组成规则。
通常有一种方法可以使未放大类型从放大类型中退回。严格来说,此操作不需要具有monad。(尽管如果您想拥有自己的名字是很有必要的。我们将不在本文中进一步讨论。)
再以Nullable<T>一个例子为例。您可以使用构造函数将int转换为Nullable<int>。C#编译器会为您处理大多数可为空的“提升”,但是如果没有,提升转换将非常简单:例如,
int M(int x) { whatever }
变成
Nullable<int> M(Nullable<int> x)
{
if (x == null)
return null;
else
return new Nullable<int>(whatever);
}
并通过该属性将其变Nullable<int>回原样。intValue
函数转换是关键。注意,在转换中如何捕获可为空操作的实际语义,即可null传播操作的语义null。我们可以对此进行概括。
假设您具有从int到的功能int,就像我们原来的功能一样M。您可以轻松地将其转换为接受an int并返回a 的函数,Nullable<int>因为您可以通过可为null的构造函数运行结果。现在假设您具有以下高阶方法:
static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
if (amplified == null)
return null;
else
return func(amplified.Value);
}
看到你能做什么?现在,任何采用an int并返回an int或采用an int并返回a的Nullable<int>方法现在都可以应用可空语义。
此外:假设您有两种方法
Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }
而您想组成它们:
Nullable<int> Z(int s) { return X(Y(s)); }
即和Z的组成。但是您不能这样做,因为需要一个,然后返回一个。但是,由于您具有“绑定”操作,因此可以进行以下工作:XYXintYNullable<int>
Nullable<int> Z(int s) { return Bind(Y(s), X); }
对单声道的绑定操作使放大类型上的功能组合起作用。我上面挥挥手的“规则”是单子保留正常功能组成的规则。与标识函数组成的结果将产生原始函数,该组成具有关联性,依此类推。
在C#中,“绑定”称为“ SelectMany”。看一下它如何在序列monad上工作。我们需要做两件事:将一个值转换为一个序列,然后对序列进行绑定操作。作为奖励,我们还具有“将序列变回值”的功能。这些操作是:
static IEnumerable<T> MakeSequence<T>(T item)
{
yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
// let's just take the first one
foreach(T item in sequence) return item;
throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
foreach(T item in seq)
foreach(T result in func(item))
yield return result;
}
可为空的monad规则是“将产生可为空的两个函数组合在一起,检查内部函数是否为null;如果满足,则产生null,否则为null,然后用结果调用外部函数”。这是可为空的所需语义。
序列monad规则是“将产生序列的两个函数组合在一起,将外部函数应用于内部函数产生的每个元素,然后将所有结果序列连接在一起”。Bind/ SelectMany方法捕获了monad的基本语义;这是告诉您monad真正含义的方法。
我们可以做得更好。假设您有一个整数序列,以及一个采用整数并产生字符串序列的方法。我们可以对绑定操作进行一般化,以允许接受和返回不同放大类型的函数的组合,只要其中一个的输入与另一个的输出匹配即可:
static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
foreach(T item in seq)
foreach(U result in func(item))
yield return result;
}
因此,现在我们可以说:“将这组单个整数放大为整数序列。将该特定整数转换为一串字符串,放大为一系列字符串。现在将这两个操作放在一起:将这组整数放大为”所有的字符串序列。” Monads使您可以构成放大。
它解决了什么问题,最常使用的地方是什么?
这就好比问“单例模式能解决什么问题?”,但我会给它一个机会。
Monad通常用于解决以下问题:
我需要为此类型创建新功能,并且仍然要组合此类型上的旧功能以使用新功能。
我需要捕获一堆关于类型的操作,并将这些操作表示为可组合的对象,构建越来越大的合成,直到我正确地表示了一系列操作,然后才需要从事情中得到结果。
我需要用一种讨厌副作用的语言清晰地表示副作用操作
C#在其设计中使用了monad。如前所述,可为空的模式与“也许是单子”非常相似。LINQ完全由monad组成。该SelectMany方法是操作组合的语义工作。(Erik Meijer喜欢指出,每个LINQ函数实际上都可以由实现SelectMany;其他一切只是为了方便。)
为了阐明我正在寻找的理解类型,假设您正在将具有monad的FP应用程序转换为OOP应用程序。您将如何将Monad的责任移植到OOP应用程序中?
大多数OOP语言没有足够丰富的类型系统来直接表示monad模式本身。您需要一个类型系统,该系统支持比通用类型更高类型的类型。所以我不会尝试这样做。相反,我将实现代表每个monad的泛型类型,并实现代表您需要的三个操作的方法:将值转换为放大的值,(也许)将放大的值转换为一个值,并将未放大的值的函数转换为放大值的函数。
一个很好的起点是我们如何在C#中实现LINQ。研究SelectMany方法;这是理解序列monad如何在C#中工作的关键。这是一种非常简单的方法,但功能非常强大!
建议进一步阅读:
有关C#中monad的更深入和理论上合理的解释,我强烈建议我(Eric Lippert的同事)Wes Dyer关于该主题的文章。本文是monads最终为我“点击”时向我解释的内容。
Monads的奇迹
一个很好的说明,为什么您可能想要一个monad (在示例中使用Haskell)。
您可能已经发明了Monad!(也许你已经拥有了。)丹·皮波尼
上一篇文章到JavaScript的“翻译”。
James Coglan 所读过的有关Monad 最佳入门的精选部分的从Haskell到JavaScript的翻译
TA贡献1827条经验 获得超8个赞
为什么我们需要单子?
我们只想使用函数进行编程。(毕竟-FP是“功能性编程”)。
然后,我们有第一个大问题。这是一个程序:
f(x) = 2 * x
g(x,y) = x / y
我们如何说 要先执行什么?我们如何仅使用函数就可以形成一个有序的函数序列(即程序)?
解决方案:编写函数。如果先要g然后f再写就可以f(g(x,y))。好的但是 ...
更多问题:某些功能可能会失败(即g(2,0),除以0)。我们在FP没有“例外”。我们该如何解决?
解决方案:让函数让函数返回两种东西:g : Real,Real -> Real让我们g : Real,Real -> Real | Nothing(函数从两个实数转换为(实数或虚无))而不是让(函数从两个实数转换为实数)。
但是函数(为了简化)应该只返回一件事。
解决方案:让我们创建一种新的要返回的数据类型,即“ 装箱类型 ”,其中可能包含实数或仅是空值。因此,我们可以拥有g : Real,Real -> Maybe Real。好的但是 ...
现在会发生什么f(g(x,y))?f还没准备好消费Maybe Real。而且,我们不想更改可以连接的每个函数g来消耗Maybe Real。
解决方案:让我们有一个特殊的功能来“连接” /“组合” /“链接”功能。这样,我们可以在幕后调整一项功能的输出以提供下一项功能。
在我们的例子中:( g >>= f连接/组成g到f)。我们要>>=获取g的输出,对其进行检查,以防万一它Nothing不调用f并返回Nothing;或者相反,提取盒装Real并f用它喂食。(此算法只是>>=针对该Maybe类型的实现)。
使用此相同的模式可以解决许多其他问题:1.使用“框”来整理/存储不同的含义/值,并具有g返回这些“框值”的函数。2.让作曲者/链接g >>= f者帮助将g的输出连接到f的输入,因此我们完全不需要更改f。
使用此技术可以解决的显着问题是:
具有全局状态,函数序列中的每个函数(“程序”)可以共享:solution StateMonad。
我们不喜欢“不纯函数”:对于相同输入产生不同输出的函数。因此,让我们标记这些函数,使它们返回标记/装箱的值:monad。IO
完全幸福!
TA贡献1811条经验 获得超6个赞
我会说与单子最接近的OO类比是“ 命令模式 ”。
在命令模式中,将普通语句或表达式包装在命令对象中。该命令对象公开了一个执行方法,该方法执行包装的语句。因此,语句变成了可以随意传递和执行的一流对象。可以组合命令,因此您可以通过链接和嵌套命令对象来创建程序对象。
这些命令由单独的对象(调用程序)执行。使用命令模式(而不是仅执行一系列普通语句)的好处是,不同的调用者可以对应如何执行命令应用不同的逻辑。
命令模式可用于添加(或删除)宿主语言不支持的语言功能。例如,在无例外的假设OO语言中,可以通过向命令公开“ try”和“ throw”方法来添加例外语义。当命令调用throw时,调用者将在命令列表(或树)中回溯,直到最后一次“ try”调用为止。相反,您可以通过捕获每个命令抛出的所有异常并将其转换为错误代码,然后将其传递给下一个命令,从某种语言中删除异常语义(如果您认为异常不好)。
像这样的更加花哨的执行语义,例如事务,非确定性执行或连续性,都可以用本机不支持的语言来实现。如果您考虑一下,这是一个非常强大的模式。
现在,实际上,命令模式并未像这样被用作通用语言功能。将每个语句转换为单独的类的开销将导致大量的样板代码。但是从原理上讲,它可以用来解决与单子在fp中解决相同的问题。
添加回答
举报