就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
当前回答
从实践的角度来看(总结了之前许多回答和相关文章中所说的内容),在我看来,monad的一个基本“目的”(或有用性)是利用递归方法调用(即函数组合)中隐含的依赖关系(即,当f1调用f2调用f3时,f3需要在f1之前的f2之前求值),以自然的方式表示顺序组合,特别是在惰性评估模型的上下文中(即,作为一个普通序列的顺序合成,例如C中的“f3();f2();f1();”),如果你想到f3、f2和f1实际上什么都不返回的情况(它们作为f1(f2(f3))的链接是人为的,纯粹是为了创建序列),那么这个技巧就特别明显了。
当涉及到副作用时,这一点尤其重要,即当某些状态被改变时(如果f1、f2、f3没有副作用,那么它们的求值顺序无关紧要;这是纯函数语言的一个很好的特性,例如能够并行化这些计算)。函数越纯越好。
我认为,从这个狭隘的角度来看,monad可以被视为支持惰性求值的语言的语法糖(只有在绝对必要时才求值,遵循不依赖于代码表示的顺序),并且没有其他表示顺序合成的方法。最终的结果是,“不纯”(即确实有副作用)的代码段可以以命令式的方式自然呈现,但与纯函数(没有副作用)完全分离,纯函数可以延迟求值。
正如这里所警告的,这只是一个方面。
其他回答
你最近有一篇演讲《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程序员将理解(没有任何功能编程背景),什么是莫纳德?它解决了什么问题是最常用的地方吗?是最常用的地方吗?
就OO编程而言,monad是一个接口(或者更可能是一个mixin),由一个类型参数化,具有两个方法,return和bind,它们描述:
如何注入值以获得注入值的一元值类型如何使用从非一元值。
它解决的问题与您期望的任何接口的问题类型相同,“我有很多不同的类,它们做不同的事情,但似乎以一种具有潜在相似性的方式来做这些不同的事情。即使这些类本身不是比‘Object’类本身更接近的子类,我如何描述它们之间的相似性?”
更具体地说,Monad“接口”与IEnumerator或IIterator相似,因为它采用的类型本身也采用的类型。然而,Monad的主要“点”是能够连接基于内部类型的操作,甚至可以连接到具有新的“内部类型”的点,同时保持-甚至增强-主类的信息结构。
从实践的角度来看(总结了之前许多回答和相关文章中所说的内容),在我看来,monad的一个基本“目的”(或有用性)是利用递归方法调用(即函数组合)中隐含的依赖关系(即,当f1调用f2调用f3时,f3需要在f1之前的f2之前求值),以自然的方式表示顺序组合,特别是在惰性评估模型的上下文中(即,作为一个普通序列的顺序合成,例如C中的“f3();f2();f1();”),如果你想到f3、f2和f1实际上什么都不返回的情况(它们作为f1(f2(f3))的链接是人为的,纯粹是为了创建序列),那么这个技巧就特别明显了。
当涉及到副作用时,这一点尤其重要,即当某些状态被改变时(如果f1、f2、f3没有副作用,那么它们的求值顺序无关紧要;这是纯函数语言的一个很好的特性,例如能够并行化这些计算)。函数越纯越好。
我认为,从这个狭隘的角度来看,monad可以被视为支持惰性求值的语言的语法糖(只有在绝对必要时才求值,遵循不依赖于代码表示的顺序),并且没有其他表示顺序合成的方法。最终的结果是,“不纯”(即确实有副作用)的代码段可以以命令式的方式自然呈现,但与纯函数(没有副作用)完全分离,纯函数可以延迟求值。
正如这里所警告的,这只是一个方面。
看到我对“什么是monad?”的回答了吗
它从一个激励性的例子开始,通过这个例子,导出一个monad的例子,并正式定义“monad”。
它假设不了解函数式编程,并且使用带有函数(参数):=表达式语法的伪代码和最简单的表达式。
这个C++程序是伪代码monad的一个实现。(仅供参考:M是类型构造函数,feed是“绑定”操作,wrap是“返回”操作。)
#include <iostream>
#include <string>
template <class A> class M
{
public:
A val;
std::string messages;
};
template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
M<B> m = f(x.val);
m.messages = x.messages + m.messages;
return m;
}
template <class A>
M<A> wrap(A x)
{
M<A> m;
m.val = x;
m.messages = "";
return m;
}
class T {};
class U {};
class V {};
M<U> g(V x)
{
M<U> m;
m.messages = "called g.\n";
return m;
}
M<T> f(U x)
{
M<T> m;
m.messages = "called f.\n";
return m;
}
int main()
{
V x;
M<T> m = feed(f, feed(g, wrap(x)));
std::cout << m.messages;
}
可选/可能是最基本的一元类型
单子是关于功能组成的。如果函数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));
});
});
});