就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
当前回答
看到我对“什么是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;
}
其他回答
我能想到的最简单的解释是,单声道是一种用符号化结果组成函数的方式(也称为克莱斯利合成)。“embelished”函数具有签名a->(b,smth),其中a和b是可能彼此不同但不一定不同的类型(想想Int,Bool),smth是“上下文”或“embelisement”。
这种类型的函数也可以写成a->m b,其中m相当于“embelisation”smth。因此,这些是在上下文中返回值的函数(想想记录其操作的函数,其中smth是日志消息;或者执行输入/输出的函数,其结果取决于IO操作的结果)。
monad是一个接口(“typeclass”),它让实现者告诉它如何组合这样的函数。实现者需要为任何想要实现接口的m类型定义一个组合函数(a->mb)->(b->mc)->(a->mc)(这是Kleisli组合)。
所以,如果我们说我们有一个元组类型(Int,String),它表示Int上的计算结果,(_,String)是“embelisation”-动作的日志-和两个函数increment::Int->(Int,String)和twoTimes::Int->(Int、String),我们希望获得一个函数incamentThenDouble::Int->(Int),这是两个函数的组合,也考虑了日志。
在给定的示例中,两个函数的monad实现应用于整数值2增量ThenDouble 2(等于2倍(增量2))将返回(6,“加法1”。中间结果的增量2等于(3,“加1”),2乘以3等于(6,“加3”)
从这个Kleisli合成函数可以导出通常的一元函数。
从实践的角度来看(总结了之前许多回答和相关文章中所说的内容),在我看来,monad的一个基本“目的”(或有用性)是利用递归方法调用(即函数组合)中隐含的依赖关系(即,当f1调用f2调用f3时,f3需要在f1之前的f2之前求值),以自然的方式表示顺序组合,特别是在惰性评估模型的上下文中(即,作为一个普通序列的顺序合成,例如C中的“f3();f2();f1();”),如果你想到f3、f2和f1实际上什么都不返回的情况(它们作为f1(f2(f3))的链接是人为的,纯粹是为了创建序列),那么这个技巧就特别明显了。
当涉及到副作用时,这一点尤其重要,即当某些状态被改变时(如果f1、f2、f3没有副作用,那么它们的求值顺序无关紧要;这是纯函数语言的一个很好的特性,例如能够并行化这些计算)。函数越纯越好。
我认为,从这个狭隘的角度来看,monad可以被视为支持惰性求值的语言的语法糖(只有在绝对必要时才求值,遵循不依赖于代码表示的顺序),并且没有其他表示顺序合成的方法。最终的结果是,“不纯”(即确实有副作用)的代码段可以以命令式的方式自然呈现,但与纯函数(没有副作用)完全分离,纯函数可以延迟求值。
正如这里所警告的,这只是一个方面。
在OO术语中,monad是一个流畅的容器。
最低要求是类<a>的定义,它支持构造函数Something(a a)和至少一个方法Something<B>flatMap(函数<a,Something<B>>)
可以说,monad类是否有签名Something<B>work()的方法来保存类的规则——编译器在编译时在flatMap中烘焙。
为什么单子有用?因为它是一个允许保留语义的可链式操作的容器。例如,可选<?>为Optional<String>、Optional<Integer>、Optional<MyClass>等保留isPresent的语义。
作为一个粗略的例子,
Something<Integer> i = new Something("a")
.flatMap(doOneThing)
.flatMap(doAnother)
.flatMap(toInt)
注意,我们以字符串开头,以整数结尾。很酷。
在OO中,这可能需要一点努力,但Something上的任何方法如果返回Something的另一个子类,都符合返回原始类型容器的容器函数的标准。
这就是保持语义的方式——即容器的含义和操作不会改变,它们只是包装和增强容器内的对象。
我分享了我对蒙纳斯的理解,这在理论上可能并不完美。Monad是关于上下文传播的。Monad就是,您为某些数据(或数据类型)定义一些上下文,然后定义该上下文将如何在整个处理管道中与数据一起传递。定义上下文传播主要是定义如何合并多个上下文(相同类型)。使用Monads还意味着确保这些上下文不会意外地从数据中剥离。另一方面,可以将其他无上下文数据带入新的或现有的上下文中。然后,可以使用这个简单的概念来确保程序的编译时正确性。
看到我对“什么是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;
}