就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
当前回答
你最近有一篇演讲《Monadologie——关于类型焦虑的专业帮助》(Christopher League,2010年7月12日),这篇演讲对延续和monad的话题非常有趣。这个(幻灯片)演示的视频实际上可以在vimeo上获得。Monad部分开始于37分钟左右,在这段一小时的视频中,从58张幻灯片中的第42张幻灯片开始。
它被称为“函数式编程的主要设计模式”,但示例中使用的语言是Scala,它既是面向对象的又是函数式的。您可以在Debasish Ghosh(2008年3月27日)的博客文章“Monads-在Scala中抽象计算的另一种方法”中阅读更多关于Monad的内容。
如果类型构造函数M支持以下操作,那么它就是monad:
# the return function
def unit[A] (x: A): M[A]
# called "bind" in Haskell
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]
# Other two can be written in term of the first two:
def map[A,B] (m: M[A]) (f: A => B): M[B] =
flatMap(m){ x => unit(f(x)) }
def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
flatMap(ma){ x => mb }
例如(在Scala中):
选项是monad
def unit[A] (x: A): Option[A] = Some(x) def flatMap[A,B](m:Option[A])(f:A =>Option[B]): Option[B] = m match { case None => None case Some(x) => f(x) }
列表为Monad
def unit[A] (x: A): List[A] = List(x) def flatMap[A,B](m:List[A])(f:A =>List[B]): List[B] = m match { case Nil => Nil case x::xs => f(x) ::: flatMap(xs)(f) }
Monad在Scala中非常重要,因为它是为了利用Monad结构而构建的方便语法:
对于Scala的理解:
for {
i <- 1 to 4
j <- 1 to i
k <- 1 to j
} yield i*j*k
由编译器翻译为:
(1 to 4).flatMap { i =>
(1 to i).flatMap { j =>
(1 to j).map { k =>
i*j*k }}}
关键抽象是flatMap,它通过链接绑定计算。flatMap的每次调用都返回相同的数据结构类型(但值不同),作为链中下一个命令的输入。
在上面的代码段中,flatMap将闭包(SomeType)=>List[AanotherType]作为输入,并返回List[Aanother Type]。需要注意的一点是,所有flatMap都采用相同的闭包类型作为输入,并返回与输出相同的类型。
这就是“绑定”计算线程的原因——为了理解,序列中的每一项都必须遵守相同的类型约束。
如果您执行两个操作(可能失败)并将结果传递给第三个,例如:
lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]
但如果不利用Monad,你会得到复杂的OOP代码,比如:
val user = getLoggedInUser(session)
val confirm =
if(!user.isDefined) None
else lookupVenue(name) match {
case None => None
case Some(venue) =>
val confno = reserveTable(venue, user.get)
if(confno.isDefined)
mailTo(confno.get, user.get)
confno
}
而使用Monad,您可以像所有操作一样使用实际类型(地点、用户),并隐藏选项验证内容,这都是因为for语法的平面图:
val confirm = for {
venue <- lookupVenue(name)
user <- getLoggedInUser(session)
confno <- reserveTable(venue, user)
} yield {
mailTo(confno, user)
confno
}
只有当所有三个函数都具有Some[X]时,才会执行屈服部分;任何“无”将直接返回以确认。
So:
Monad允许在函数编程中进行有序计算,这允许我们以一种很好的结构化形式(有点像DSL)对动作序列进行建模。最大的能力来自于将服务于不同目的的monad组合成应用程序中的可扩展抽象的能力。monad对动作的排序和线程化由语言编译器完成,该语言编译器通过闭包的魔力进行转换。
顺便说一句,Monad不是FP中使用的唯一计算模型:
范畴理论提出了许多计算模型。其中计算的Arrow模型莫纳德计算模型计算的应用模型
其他回答
我将尝试使用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
}
更新:这个问题是一个非常长的博客系列的主题,你可以在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
这里有一个简单的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[()])“尝试…捕捉”等。
我分享了我对蒙纳斯的理解,这在理论上可能并不完美。Monad是关于上下文传播的。Monad就是,您为某些数据(或数据类型)定义一些上下文,然后定义该上下文将如何在整个处理管道中与数据一起传递。定义上下文传播主要是定义如何合并多个上下文(相同类型)。使用Monads还意味着确保这些上下文不会意外地从数据中剥离。另一方面,可以将其他无上下文数据带入新的或现有的上下文中。然后,可以使用这个简单的概念来确保程序的编译时正确性。
可选/可能是最基本的一元类型
单子是关于功能组成的。如果函数f:可选<A>->可选<B>,g:可选<B>->可选<C>,h:可选<C>->可选<D>。然后你可以创作它们
optional<A> opt;
h(g(f(opt)));
monad类型的好处是,您可以改为组合f:A->可选<B>、g:B->可选<C>、h:C->可选<D>。他们可以这样做,因为monadic接口提供了绑定运算符
auto optional<A>::bind(A->optional<B>)->optional<B>
并且可以写作文
optional<A> opt
opt.bind(f)
.bind(g)
.bind(h)
monads的好处是我们不再需要处理if(!opt)return nullopt的逻辑;在f、g、h中的每一个中,因为该逻辑被移动到绑定运算符中。
ranges/lists/iterables是第二种最基本的monad类型。
范围的一元特征是我们可以变换然后变平,即从一个整数范围内编码的表示开始[36,98]
我们可以转换为[[m','a','c','h','i','n','e',''],['l','','r','n','i','n','g','.']]
然后压平[am','a','c','h','i','n','e','l',''e'
而不是编写此代码
vector<string> lookup_table;
auto stringify(vector<unsigned> rng) -> vector<char>
{
vector<char> result;
for(unsigned key : rng)
for(char ch : lookup_table[key])
result.push_back(ch);
result.push_back(' ')
result.push_back('.')
return result
}
我们可以写这个
auto f(unsigned key) -> vector<char>
{
vector<char> result;
for(ch : lookup_table[key])
result.push_back(ch);
return result
}
auto stringify(vector<unsigned> rng) -> vector<char>
{
return rng.bind(f);
}
monad将for循环(无符号键:rng)向上推到绑定函数中,从而允许理论上更容易推理的代码。毕达哥拉斯三元组可以在范围-v3中使用嵌套绑定生成(而不是我们看到的可选的链式绑定)
auto triples =
for_each(ints(1), [](int z) {
return for_each(ints(1, z), [=](int x) {
return for_each(ints(x, z), [=](int y) {
return yield_if(x*x + y*y == z*z, std::make_tuple(x, y, z));
});
});
});