Monad在简单的英语中是什么意思?(适用于没有函数式编程背景的面向对象编程员)

21 浏览
0 Comments

Monad在简单的英语中是什么意思?(适用于没有函数式编程背景的面向对象编程员)

以面向对象编程的术语解释,不涉及任何函数式编程背景,什么是Monad?它解决了什么问题?在哪些场景下最常用?\n更新:\n为了澄清我所寻求的理解,假设您要将一个具有monads的函数式编程应用转换为面向对象的应用程序。您将如何将monads的职责转移到面向对象的应用程序中?

0
0 Comments

“Monad in plain English? (For the OOP programmer with no FP background)”这个问题的出现是因为很多OOP程序员对于Monad这个概念并不熟悉,而现有的解释通常都是基于函数式编程的概念进行的,对于没有函数式编程背景的程序员来说并不易于理解。因此,需要一个用面向对象编程的术语来解释Monad的解释。

解决方法是通过引入“命令模式”来解释Monad。在命令模式中,将一个普通的语句或表达式包装在一个“命令”对象中。命令对象暴露一个“执行”方法,用于执行包装的语句。因此,语句被转换为可以传递和随意执行的一等对象。命令可以进行组合,因此可以通过链接和嵌套命令对象创建程序对象。

命令由一个独立的对象,即“调用者”,执行。使用命令模式(而不仅仅执行一系列普通语句)的好处在于不同的调用者可以对命令的执行逻辑应用不同的逻辑。

命令模式可以用于添加(或删除)语言中不支持的特性。例如,在假设没有异常的OO语言中,可以通过向命令暴露“try”和“throw”方法来添加异常语义。当命令调用throw时,调用者会回溯遍历命令列表(或树),直到最后一个“try”调用。相反,可以通过捕获每个单独命令抛出的所有异常,并将它们转换为错误代码传递给下一个命令,从而从语言中删除异常语义。

甚至可以使用这种方式在不支持这些特性的语言中实现更复杂的执行语义,如事务、非确定性执行或延续。如果你仔细思考的话,这是一种非常强大的模式。

然而,在实际中,命令模式并没有像这样被用作一种通用的语言特性。将每个语句转换为一个单独的类的开销会导致大量的样板代码,这是无法忍受的。但从原理上讲,它可以用来解决与Monad在函数式编程中所解决的相同问题。

这个解释是我见过的第一个不依赖于函数式编程概念并用真实的OOP术语来解释Monad的解释。非常好的回答。

这非常接近FP / Haskell中Monad的实际含义,只是命令对象本身“知道”它们属于哪个“调用逻辑”(只有兼容的命令才能链接在一起);调用者只提供第一个值。这不像“Print”命令可以由“非确定性执行逻辑”执行。不,它必须是“I / O逻辑”(即IO Monad)。除此之外,它非常接近。你甚至可以说Monad只是程序(由代码语句构建,以便以后执行)。在早期,bind被称为“可编程分号”。

对于使用FP来解释基本FP概念的答案,我确实非常怀疑,尤其是使用FP语言(如Scala)的答案。做得好,JacquesB!

是的,大多数其他答案和链接的博客文章似乎假设对Haskell及其语法有基础知识,这并不特别有帮助,所以这个解释非常受欢迎!

0
0 Comments

我们为什么需要单子?

我们希望只使用函数进行编程(毕竟是“函数式编程”-FP)。

然后,我们遇到了第一个大问题。这是一个程序:

f(x) = 2 * x
g(x,y) = x / y

我们如何确定先执行哪个函数?我们如何使用仅仅函数来形成一个有序的函数序列(即一个程序)?

解决方法:组合函数。如果你想先执行g然后执行f,只需要写成f(g(x,y))。但是...

更多问题:有些函数可能会失败(例如g(2,0),除以0)。在函数式编程中,我们没有“异常”。我们该如何解决这个问题?

解决方法:让函数可以返回两种不同的结果:不再是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;或者相反,提取盒装的实数,并将其喂给f。(此算法只是实现Maybe类型的>>=)。

使用这种模式可以解决许多其他问题:

1. 使用“盒子”来编码/存储不同的含义/值,并具有返回这些“盒装值”的函数g。

2. 有连接器g >>= f来帮助连接g的输出到f的输入,这样我们不需要改变f。

这种技术可以解决的一些重要问题包括:

- 有一个整个函数序列(“程序”)可以共享的全局状态:StateMonad解决方案。

- 我们不喜欢“非纯函数”:对于相同的输入产生不同的输出。因此,让我们标记这些函数,使它们返回一个带有标记/盒装值的结果:IO单子。

总之,单子可以解决Haskell中嵌套/链接函数以获得“计算”的常见问题。答案提到了其中一个问题,但并不是所有问题。我认为你应该阅读“Learn you haskell for great good”(http://learnyouahaskell.com/chapters)。

“Real -> Real -> Maybe(R)”是一个柯里化的R x R -> Maybe(R)吗?

是的。在Haskell中,函数签名是这样表述的。你真的了解Haskell吗?

单子是一种解决Haskell中嵌套/链接函数以获得“计算”的常见问题的方法。它通过允许函数返回带有标记/盒装值的结果,并提供特殊的函数来连接/组合/链接函数,从而解决了这些问题。单子可以解决许多不同的问题,如共享全局状态和处理不纯函数。

0
0 Comments

从OOP程序员的角度理解Monad的原因是:OOP程序员没有函数式编程背景,难以理解Monad的概念。解决方法是:将Monad的概念以OOP程序员能够理解的方式进行解释。

Monad是一种“类型放大器”,它遵循一定的规则并提供特定的操作。所谓“类型放大器”是指一种系统,它可以将某种类型转化为一种更特殊的类型。例如,在C#中考虑Nullable,它就是一种类型放大器。它可以将一种类型(例如int)添加一个新的能力,使其能够为null,而之前是不可能的。

“规则”是指函数在底层类型上的运作方式应该适用于放大的类型,并且遵循函数组合的常规规则。例如,如果有一个针对整数的函数,比如int M(int x) { return x + N(x * 2); },那么对应的Nullable上的函数可以使其中的运算符和调用“以相同的方式”协同工作。

“操作”包括以下几点:

1. 有一个“单元”操作(有时称为“返回”操作),它将一个普通类型的值转化为等效的放大类型的值。这实际上提供了一种将未放大类型的值转化为放大类型的值的方式。在面向对象的语言中,它可以被实现为一个构造函数。

2. 有一个“绑定”操作,它将放大类型的值和能够将该值转化的函数作为参数,并返回一个新的放大类型的值。绑定是定义Monad语义的关键操作。它可以将未放大类型的操作转化为放大类型的操作,并遵循之前提到的函数组合的规则。

3. 通常有一种方式可以将放大类型的值还原为未放大类型的值。严格来说,这个操作对于一个Monad来说并不是必需的(尽管如果你想要一个共同字,它是必要的)。本文不再进一步讨论这些。

对于Nullable来说,你可以使用构造函数将int转化为Nullable。C#编译器会自动处理大部分可空“升级”操作,但如果没有的话,升级转换是很简单的:一个操作int M(int x) { whatever }被转化为Nullable M(Nullable x) { if (x == null) return null; else return new Nullable(whatever); }。而将Nullable转化回int是通过Value属性来完成的。

函数转换是关键的部分。注意到可空操作的实际语义,即对null的操作会传播到null,被捕捉在转换中。我们可以将此进行泛化。

假设有一个从int到int的函数,比如我们最初的M。你可以很容易地将它转化为一个接受int并返回Nullable的函数,因为你可以通过可空构造函数来运行结果。现在假设你有这个高阶方法:

static Nullable Bind(Nullable amplified, Func> func)

{

if (amplified == null)

return null;

else

return func(amplified.Value);

}

看看你可以用它做什么?任何接受int并返回int或接受int并返回Nullable的方法现在都可以应用可空的语义。

此外,假设你有两个方法:

Nullable X(int q) { ... }

Nullable Y(int r) { ... }

并且你想要组合它们:

Nullable Z(int s) { return X(Y(s)); }

也就是说,Z是X和Y的组合。但你不能这样做,因为X接受int,而Y返回Nullable。但是由于有了“绑定”操作,你可以使它工作:

Nullable Z(int s) { return Bind(Y(s), X); }

Monad的绑定操作使放大类型上的函数组合工作起来。上面提到的规则是,Monad保留了普通函数组合的规则,使用标识函数进行组合会得到原始函数,组合是可结合的等等。

在C#中,Bind被称为SelectMany。看看它在序列Monad中的工作方式。我们需要两样东西:将一个值转化为序列以及在序列上进行绑定操作。作为额外的收获,我们还可以将序列转化回值。这些操作分别是:MakeSequence、First和SelectMany。

可空Monad的规则是“将两个产生可空值的函数组合在一起,检查内部函数是否产生了null;如果产生了null,就产生null;如果没有,就用结果调用外部函数”。这就是可空的所需语义。

序列Monad的规则是“将两个产生序列的函数组合在一起,将内部函数产生的每个元素应用于外部函数,并将所有结果序列连接在一起”。Monad的基本语义就体现在Bind/SelectMany方法中,这个方法告诉你Monad的真正含义。

我们可以做得更好。假设你有一串int值,以及一个接受int并返回字符串序列的方法。我们可以将绑定操作的泛化,允许对接受和返回不同放大类型的函数进行组合,只要一个函数的输入与另一个函数的输出相匹配。

static IEnumerable SelectMany(IEnumerable seq, Func> func)

{

foreach(T item in seq)

foreach(U result in func(item))

yield return result;

}

现在我们可以说“将这些个别整数放大为整数序列。将这个特定整数转化为一串字符串,放大为字符串序列。现在将这两个操作组合起来:将这些整数放大为所有字符串序列的串联”。Monad允许你对放大进行组合。

Monad通常用于解决以下问题:

1. 我需要为这个类型添加新的能力,并仍然能够结合旧的函数来使用新的能力。

2. 我需要将一系列类型上的操作捕捉并表示为可组合的对象,构建越来越大的组合,直到我得到所需的操作序列,然后我需要从中获得结果。

3. 我需要在不喜欢副作用的语言中清晰地表示副作用操作。

C#在设计中使用了Monad。正如前面提到的,可空模式与“maybe monad”非常类似。LINQ完全由Monad构建;SelectMany方法是组合操作的语义工作(Erik Meijer喜欢指出,每个LINQ函数实际上可以通过SelectMany来实现,其他函数只是方便之处)。

要将Monad的责任转移到OOP应用中,大多数OOP语言的类型系统并不丰富到能够直接表示Monad模式;你需要一个支持高于泛型类型的类型系统。因此,我不会尝试这样做。相反,我会实现表示每个Monad的泛型类型,并实现表示三个所需操作的方法:将值转化为放大值(可能)将放大值转化为值,以及将对未放大值的函数转化为对放大值的函数。

一个很好的起点是研究C#中如何实现LINQ。研究SelectMany方法;它是理解C#中序列Monad工作原理的关键。它是一个非常简单但非常强大的方法!

本文首先从OOP程序员的角度解释了Monad的概念,然后解释了Monad的核心概念和操作,并提供了一些示例。接着,文章讨论了Monad解决的问题和常见应用场景,并提供了一些进一步阅读的建议。最后,文章回答了如何将Monad的责任转移到OOP应用中的问题。

0