Monad用简单的英语? (对于没有FP背景的OOP程序员)
就OOP程序员理解(没有任何函数式编程背景)而言,什么是monad?
它解决了什么问题,它使用的最常见的地方是什么?
编辑:
为了澄清我正在寻找的那种理解,让我们假设你将具有monad的FP应用程序转换为OOP应用程序。 你会如何将monads的责任移植到OOP应用程序上?
更新:这个问题是一个非常长的博客系列的主题,你可以在Monad看到 - 谢谢你的好问题!
就OOP程序员理解(没有任何函数式编程背景)而言,什么是monad?
monad是一种类型的“放大器”,它遵守某些规则并提供某些操作。
首先,什么是“类型放大器”? 我的意思是一些系统可以让你选择一种类型并将它变成一种更特殊的类型。 例如,在C#中考虑Nullable<T>
。 这是一种类型的放大器。 它可以让你接受一个类型,比如说int
,然后为这个类型添加一个新的能力,也就是说,现在当它不能在之前它可以是null。
作为第二个例子,考虑IEnumerable<T>
。 它是一种类型的放大器。 它可以让你获取一个类型,比如string
,并为该类型添加一个新的功能,即现在可以从任意数量的单个字符串中创建一系列字符串。
什么是“某些规则”? 简而言之,对于基础类型的函数有一个明智的方式来处理放大类型,以便它们遵循功能组合的正常规则。 例如,如果你有一个整数函数,说
int M(int x) { return x + N(x * 2); }
那么Nullable<int>
上的相应函数可以使所有在那里的操作符和调用一起“以与之前相同的方式”一起工作。
(这是非常模糊和不精确的;你要求一个解释,没有任何关于功能组成的知识。)
什么是“操作”?
有一个“单位”操作(有时令人困惑地称为“返回”操作),它从一个普通类型获取一个值并创建等价的单值。 实质上,这提供了一种方法来获取未放大类型的值,并将其转化为放大类型的值。 它可以作为一个面向对象语言的构造函数来实现。
有一个“绑定”操作需要一个monadic值和一个可以转换值的函数,并返回一个新的monadic值。 绑定是定义monad语义的关键操作。 它可以让我们将非放大类型的操作转换为放大类型的操作,该操作服从前面提到的功能组合的规则。
通常有一种方法可以将未放大的类型从放大类型中取出。 严格来说这个操作不需要有monad。 (虽然如果你想拥有一个comonad,这是必要的,但我们不会在本文中进一步考虑这些)。
再次,以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>
变成一个int
Value
是通过Value
属性完成的。
这是关键位的功能转换。 请注意,可转换操作的实际语义 - 即null
上的操作传播null
- 会在转换中捕获。 我们可以概括这一点。
假设你有一个从int
到int
的函数,就像我们原来的M
。 你可以很容易地将它变成一个函数,它接受一个int
并返回一个Nullable<int>
因为你可以通过可空构造函数运行结果。 现在假设你有这个高阶方法:
static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
if (amplified == null)
return null;
else
return func(amplified.Value);
}
看看你可以用它做什么? 任何接受一个int
并返回一个int
或者接受一个int
并返回一个Nullable<int>
方法现在都可以应用可空的语义。
此外:假设你有两种方法
Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }
你想编写它们:
Nullable<int> Z(int s) { return X(Y(s)); }
也就是说, Z
是X
和Y
。 但是你不能这么做,因为X
需要一个int
,而Y
返回一个Nullable<int>
。 但是既然你有“绑定”操作,你可以做这个工作:
Nullable<int> Z(int s) { return Bind(Y(s), X); }
monad上的绑定操作是使放大类型的函数组合起作用的原因。 我上面交代的“规则”是monad保留了正常函数组成的规则; 与身份函数组成的结果是原始函数,该组合是关联的,等等。
在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,如果不是,则用结果调用外部函数。 这是可空的所需语义。
序列monad规则是“将两个产生序列的函数结合在一起,将外部函数应用于由内部函数产生的每个元素,然后将所有结果序列连接在一起”。 单元的基本语义在Bind
/ SelectMany
方法中捕获; 这是告诉你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允许你撰写你的放大。
它解决了什么问题,它使用的最常见的地方是什么?
这就像是问“单身模式解决什么问题?”,但我会给它一个镜头。
Monads通常用于解决如下问题:
C#在其设计中使用monads。 如前所述,可空模式非常类似于“可能的单子”。 LINQ完全由单子构建; SelectMany
方法是什么操作组合的语义工作。 (Erik Meijer喜欢指出每个LINQ函数实际上都可以由SelectMany
实现;其他的只是一种方便。)
为了澄清我正在寻找的那种理解,让我们假设你将具有monad的FP应用程序转换为OOP应用程序。 你会如何将monad的责任移植到OOP应用程序中?
大多数OOP语言没有足够丰富的类型系统来直接表示monad模式本身; 你需要一个支持类型比泛型更高的类型系统。 所以我不会这么做。 相反,我会实现代表每个monad的泛型类型,并实现代表您需要的三种操作的方法:将值转换为放大的值(可能)将放大的值转换为值,并将未放大的值的函数转换为放大值的函数。
一个好的开始就是我们如何在C#中实现LINQ。 研究SelectMany
方法; 了解序列monad如何在C#中工作是关键。 这是一个非常简单的方法,但非常强大!
建议,进一步阅读:
为什么我们需要monads?
那么,我们有第一个大问题。 这是一个程序:
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 -> 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
。
使用这种技术可以解决的显着问题是:
具有全局状态,即函数序列中的每个函数(“程序”)都可以共享:解决方案StateMonad
。
我们不喜欢“不纯的功能”:对同一输入产生不同输出的功能。 因此,让我们标记这些函数,使它们返回一个标记/盒装值: IO
monad。
总幸福!!!!
就OOP程序员理解(没有任何函数式编程背景)而言,什么是monad?
它解决了什么问题以及它使用的最常见的地方是它使用的最常见的地方?
就OO编程而言,monad是一个接口(或者更可能是一个mixin),通过一个类型进行参数化,使用两种方法return
和bind
:
它解决的问题与你期望从任何接口出现的问题类型相同,即“我有许多不同的类能够完成不同的事情,但似乎以具有基本相似性的方式完成不同的事情。我能否描述它们之间的相似性,即使这些类本身并不是真正的比'Object'类本身更接近的任何子类型?“
更具体地说, Monad
“接口”类似于IEnumerator
或IIterator
,因为它需要一个类型,它本身需要一个类型。 Monad
的主要“观点”是能够根据内部类型连接操作,甚至可以连接到具有新“内部类型”的位置,同时保持甚至增强主类的信息结构。
上一篇: Monad in plain English? (For the OOP programmer with no FP background)