就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
当前回答
更新:这个问题是一个非常长的博客系列的主题,你可以在Monads上阅读-谢谢你提出的问题!
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
monad是一种类型的“放大器”,它遵守某些规则,并提供某些操作。
首先,什么是“类型放大器”?我指的是某种系统,它可以让你选择一种类型,并将其转换为更特殊的类型。例如,在C#中,考虑Nullable<T>。这是一种类型的放大器。它允许您接受一个类型,比如int,并为该类型添加一个新的功能,即现在它可以在以前不能为null时为null。
作为第二个例子,考虑IEnumerable<T>。这是一种类型的放大器。它允许您获取一个类型,例如字符串,并为该类型添加一个新功能,即您现在可以从任意数量的单个字符串中创建一个字符串序列。
什么是“特定规则”?简言之,对于基础类型上的函数来说,有一种合理的方法来处理放大的类型,从而使它们遵循函数组合的正常规则。例如,如果你有一个关于整数的函数,比如
int M(int x) { return x + N(x * 2); }
则Nullable<int>上的相应函数可以使其中的所有运算符和调用“以与之前相同的方式”一起工作。
(这是难以置信的模糊和不精确;你要求的解释没有假设任何关于功能成分的知识。)
什么是“操作”?
有一个“单元”操作(有时被混淆地称为“返回”操作),它从普通类型中获取值并创建等效的一元值。本质上,这提供了一种获取未放大类型值并将其转换为放大类型值的方法。它可以作为OO语言中的构造函数实现。有一个“绑定”操作,它接受一个一元值和一个可以转换该值的函数,并返回一个新的一元值。Bind是定义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);
}
通过Value属性可以将Nullable<int>转换回int。
关键是函数转换。请注意,在转换中如何捕获可为null的操作的实际语义(即对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,或者接受一个整型并返回一一个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#中,“Bind”被称为“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;
}
可为null的monad规则是“将产生可为null值的两个函数组合在一起,检查内部函数是否产生null值;如果产生null值,则产生null值。如果没有,则调用外部函数产生null值”。这就是nullable的理想语义。
序列monad规则是“将两个生成序列的函数组合在一起,将外部函数应用于内部函数生成的每个元素,然后将所有生成的序列连接在一起”。monad的基本语义在Bind/SelectMany方法中被捕获;这是告诉monad真正含义的方法。
我们可以做得更好。假设您有一个int序列,以及一个接受int并生成字符串序列的方法。我们可以推广绑定操作,以允许组合接受和返回不同放大类型的函数,只要其中一个的输入与另一个的输出匹配:
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;
}
现在我们可以说“将这一组单独的整数放大成一系列整数。将这一特定的整数转换成一系列字符串,放大成一组字符串。现在将这两个操作放在一起:将这一系列整数放大成所有字符串序列的串联。”单子允许您合成放大。
它解决了什么问题,最常用的地方是什么?
这相当于问“单例模式解决了什么问题?”,但我会尝试一下。
Monad通常用于解决以下问题:
我需要为这种类型创建新的功能,并且仍然在这种类型上组合旧的功能以使用新的功能。我需要在类型上捕获一堆操作,并将这些操作表示为可组合对象,构建越来越大的组合,直到表示出正确的一系列操作,然后我需要开始获得结果我需要用讨厌副作用的语言清晰地表示副作用操作
C#在设计中使用monad。如前所述,可空模式与“可能是monad”非常相似。LINQ完全由monad构建;SelectMany方法是操作组合的语义工作。(Erik Meijer喜欢指出,每个LINQ函数实际上都可以由SelectMany实现;其他一切都只是为了方便。)
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序中?
大多数OOP语言没有足够丰富的类型系统来直接表示monad模式本身;您需要一个支持高于泛型类型的类型的类型系统。所以我不会这么做。相反,我将实现表示每个monad的泛型类型,并实现表示您需要的三个操作的方法:将值转换为放大值,(可能)将放大值转换为值,以及将未放大值上的函数转换为放大的值上的函数。
一个好的开始是我们如何在C#中实现LINQ。研究SelectMany方法;这是理解序列monad如何在C#中工作的关键。这是一个非常简单的方法,但非常强大!
建议进一步阅读:
为了对C#中的单子进行更深入、理论上更合理的解释,我强烈推荐我(埃里克·里佩尔)的同事韦斯·戴尔(Wes Dyer)关于这个主题的文章。这篇文章是当他们最终为我“点击”时向我解释单子的原因。蒙得斯的奇迹这是一个很好的例子,说明了为什么您可能需要一个monad(在示例中使用Haskell)。你本可以发明修道院的!(也许你已经有了。)丹·皮波尼有点像,将上一篇文章“翻译”为JavaScript。詹姆斯·科格兰(James Coglan)所读过的monads最佳简介精选部分从Haskell翻译成JavaScript
其他回答
我将尝试使用OOP术语做出最简短的定义:
如果一个泛型类CMonadic<T>至少定义了以下方法,那么它就是一个monad:
class CMonadic<T> {
static CMonadic<T> create(T t); // a.k.a., "return" in Haskell
public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}
如果以下定律适用于所有类型T及其可能的值T
左标识:
CMonadic<T>.create(t).flatMap(f) == f(t)
权利认同
instance.flatMap(CMonadic<T>.create) == instance
关联性:
instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))
示例:
列表monad可能具有:
List<int>.create(1) --> [1]
列表[1,2,3]上的flatMap可以这样工作:
intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]
Iterables和Observables也可以是monadic,以及Promise和Task。
评论:
修道院没有那么复杂。flatMap函数与常见的map非常相似。它接收一个函数参数(也称为委托),可以使用来自泛型类的值调用(立即或稍后,零次或多次)。它希望传递的函数也将其返回值包装在同一类泛型类中。为了帮助实现这一点,它提供了create,一个构造函数,可以从值创建该泛型类的实例。flatMap的返回结果也是相同类型的泛型类,通常将flatMap一个或多个应用程序的返回结果中包含的相同值打包到先前包含的值。这允许您尽可能多地链接flatMap:
intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
.flatMap(x => x % 3 == 0
? List<string>.create("x = " + x.toString())
: List<string>.empty())
恰好这种泛型类作为大量事物的基础模型非常有用。这(加上范畴理论的对立)是莫纳斯看起来如此难以理解或解释的原因。它们是一个非常抽象的东西,只有在它们被专门化之后才会变得明显有用。
例如,可以使用一元容器对异常进行建模。每个容器将包含操作结果或发生的错误。flatMap回调链中的下一个函数(委托)只有在前一个函数将值打包到容器中时才会被调用。否则,如果打包了错误,错误将继续在链接的容器中传播,直到找到通过名为.orElse()的方法附加了错误处理程序函数的容器(这样的方法将是允许的扩展)
注意:函数式语言允许您编写可以对任何类型的一元泛型类进行操作的函数。要实现这一点,必须为monad编写一个通用接口。我不知道是否有可能用C#编写这样的接口,但据我所知,这不是:
interface IMonad<T> {
static IMonad<T> create(T t); // not allowed
public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
// because the function must return the same kind of monad, not just any monad
}
从实践的角度来看(总结了之前许多回答和相关文章中所说的内容),在我看来,monad的一个基本“目的”(或有用性)是利用递归方法调用(即函数组合)中隐含的依赖关系(即,当f1调用f2调用f3时,f3需要在f1之前的f2之前求值),以自然的方式表示顺序组合,特别是在惰性评估模型的上下文中(即,作为一个普通序列的顺序合成,例如C中的“f3();f2();f1();”),如果你想到f3、f2和f1实际上什么都不返回的情况(它们作为f1(f2(f3))的链接是人为的,纯粹是为了创建序列),那么这个技巧就特别明显了。
当涉及到副作用时,这一点尤其重要,即当某些状态被改变时(如果f1、f2、f3没有副作用,那么它们的求值顺序无关紧要;这是纯函数语言的一个很好的特性,例如能够并行化这些计算)。函数越纯越好。
我认为,从这个狭隘的角度来看,monad可以被视为支持惰性求值的语言的语法糖(只有在绝对必要时才求值,遵循不依赖于代码表示的顺序),并且没有其他表示顺序合成的方法。最终的结果是,“不纯”(即确实有副作用)的代码段可以以命令式的方式自然呈现,但与纯函数(没有副作用)完全分离,纯函数可以延迟求值。
正如这里所警告的,这只是一个方面。
monad是一个函数数组
(Pst:函数数组只是一个计算)。
实际上,这些函数不是真正的数组(一个单元格数组中的一个函数),而是由另一个函数>>=链接。>>=允许调整函数i的结果以馈送函数i+1,并在它们之间执行计算或者甚至不调用函数i+1。
这里使用的类型是“带上下文的类型”。这是一个带有“标记”的值。被链接的函数必须采用“裸值”并返回标记结果。>>=的职责之一是从上下文中提取裸值。还有一个函数“return”,它接受一个裸值并将其与一个标记一起放置。
Maybe的一个例子。让我们使用它来存储一个简单的整数,以便进行计算。
-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return (a*b)
-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom
-- tagged value
val1 = Just 160
-- array of functions feeded with val1
array1 = val1 >>= divideBy 2 >>= multiply 3 >>= divideBy 4 >>= multiply 3
-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
v <- divideBy 2 n
v <- multiply 3 v
v <- divideBy 4 v
v <- multiply 3 v
return v
-- array of functions,
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0 >>= multiply 3 >>= divideBy 4 >>= multiply 3
main = do
print array1
print (array2 160)
print array3
为了说明monad是带有助手操作的函数数组,请考虑与上述示例等效,仅使用一个实函数数组
type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions
myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]
-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs
它的用法如下:
print (runMyMonad (Just 160) myArray1)
快速解释:
单体(在函数式编程中)是具有上下文相关行为的函数。
上下文作为参数传递,从先前的monad调用返回。它使它看起来像是同一个参数在后续调用中产生了不同的返回值。
等效值:Monad是其实际参数取决于调用链的过去调用的函数。
典型示例:有状态函数。
FAQ
等等,你说的“行为”是什么意思?
行为是指特定输入的返回值和副作用。
但它们有什么特别之处?
在过程语义中:没有。但它们仅使用纯函数进行建模。这是因为像Haskell这样的纯函数编程语言只使用本身没有状态的纯函数。
但是,国家从何而来?
状态性来自函数调用执行的顺序性。它允许嵌套函数通过多个函数调用拖动某些参数。这将模拟状态。monad只是一种软件模式,它将这些附加参数隐藏在光鲜亮丽的函数的返回值后面,通常称为return和bind。
为什么在Haskell中输入/输出是monad?
因为显示的文本是操作系统中的一种状态。如果多次读取或写入同一文本,则每次调用后操作系统的状态将不相同。相反,输出设备将显示文本输出的3倍。为了对操作系统做出正确的反应,Haskell需要将操作系统状态建模为monad。
从技术上讲,你不需要monad的定义。纯粹的函数式语言可以将“唯一性类型”的概念用于相同的目的。
单子在非功能语言中存在吗?
是的,基本上,解释器是一个复杂的monad,解释每个指令并将其映射到操作系统中的一个新状态。
详细说明:
monad(在函数式编程中)是一种纯函数式软件模式。monad是一个自动维护的环境(一个对象),可以在其中执行一系列纯函数调用。函数结果修改或与该环境交互。
换句话说,monad是一个“函数中继器”或“函数链接器”,它在自动维护的环境中链接和评估参数值。链接的参数值通常是“更新函数”,但实际上可以是任何对象(具有组成容器的方法或容器元素)。monad是在每个求值参数前后执行的“粘合代码”。这个粘合代码函数“bind”应该将每个参数的环境输出集成到原始环境中。
因此,monad以特定于特定monad的实现方式连接所有参数的结果。控制和数据是否或如何在参数之间流动也是特定于实现的。
这种交织执行允许模拟完整的命令式控制流(如GOTO程序中的)或并行执行,仅使用纯函数,还可以在函数调用之间进行副作用、临时状态或异常处理,即使应用的函数不知道外部环境。
编辑:请注意,monads可以以任何类型的控制流图来评估功能链,甚至是非确定性NFA式的方式,因为剩余的链是延迟评估的,可以在链的每个点进行多次评估,这允许在链中进行回溯。
使用monad概念的原因是纯函数范式,它需要一个工具来以纯方式模拟典型的无可指责的建模行为,而不是因为它们做了一些特殊的事情。
面向OOP人群的修道院
在OOP中,monad是一个典型的对象
通常称为return的构造函数,它将值转换为环境的初始实例一种可链接的参数应用程序方法,通常称为bind,它使用作为参数传递的函数的返回环境来维护对象的状态。
有些人还提到了第三个函数join,它是bind的一部分。因为“参数函数”在环境中求值,所以它们的结果嵌套在环境本身中。join是“取消嵌套”结果(使环境变平)的最后一步,用新环境替换环境。
monad可以实现Builder模式,但允许更广泛的使用。
示例(Python)
我认为monad最直观的例子是Python中的关系运算符:
result = 0 <= x == y < 3
您可以看到它是一个monad,因为它必须携带一些布尔状态,而这些状态是单个关系运算符调用所不知道的。
如果您考虑如何在低级别上实现它而不发生短路行为,那么您将得到一个monad实现:
# result = ret(0)
result = (0, true)
# result = result.bind(lambda v: (x, v <= x))
result[1] = result[1] and result[0] <= x
result[0] = x
# result = result.bind(lambda v: (y, v == y))
result[1] = result[1] and result[0] == y
result[0] = y
# result = result.bind(lambda v: (3, v < 3))
result[1] = result[1] and result[0] < 3
result[0] = 3
result = result[1] # not explicit part of a monad
真正的monad最多只能计算一次每个参数。
现在考虑一下“result”变量,就会得到这个链:
ret(0) .bind (lambda v: v <= x) .bind (lambda v: v == y) .bind (lambda v: v < 3)
这里有一个简单的Monads解释和Marvel的案例研究。
单子是用于对有效的依赖函数进行排序的抽象。这里的有效意味着它们以F[a]的形式返回一个类型,例如Option[a],其中Option是F,称为类型构造函数。让我们通过两个简单步骤来了解这一点
下面的函数组合是可传递的。所以从A到C,我可以组成A=>B和B=>C。
A => C = A => B andThen B => C
然而,如果函数返回一个像Option[A]这样的效果类型,即A=>F[B],则合成不起作用,因为我们需要A=>B,但我们有A=>F[B]。我们需要一个特殊的运算符“bind”,它知道如何融合这些返回F[a]的函数。
A => F[C] = A => F[B] bind B => F[C]
“bind”函数是为特定的F定义的。
对于任何A,也有“return”,类型A=>F[A],也为特定的F定义。要成为Monad,F必须定义这两个函数。
因此,我们可以从任何纯函数A=>B构造有效函数A=>F[B],
A => F[B] = A => B andThen return
但给定的F也可以定义自己不透明的“内置”特殊函数,这些函数的类型是用户无法自行定义的(纯语言),例如
“随机”(范围=>随机[Int])“print”(字符串=>IO[()])“尝试…捕捉”等。