就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;
}

其他回答

我想说,与monads最接近的OO类比是“命令模式”。

在命令模式中,将普通语句或表达式包装在命令对象中。命令对象公开执行包装语句的执行方法。所以,语句被转换为可以随意传递和执行的第一类对象。可以组合命令,以便通过链接和嵌套命令对象来创建程序对象。

命令由单独的对象调用程序执行。使用命令模式(而不仅仅是执行一系列普通语句)的好处是,不同的调用程序可以将不同的逻辑应用于如何执行命令。

命令模式可用于添加(或删除)宿主语言不支持的语言功能。例如,在没有异常的假设OO语言中,可以通过向命令公开“try”和“throw”方法来添加异常语义。当命令调用throw时,调用程序会回溯到命令列表(或树),直到最后一次“try”调用。相反,您可以通过捕获每个单独命令抛出的所有异常,并将它们转换为错误代码,然后传递给下一个命令,从而从语言中删除异常语义(如果您认为异常是坏的)。

甚至更花哨的执行语义(如事务、非确定性执行或延续)也可以用本机不支持的语言实现。如果你仔细想想,这是一个非常强大的模式。

实际上,命令模式并没有像这样作为通用语言特性使用。将每个语句转换为单独的类的开销将导致无法忍受的样板代码。但原则上,它可以用于解决与在fp中使用monad解决的问题相同的问题。

如果您曾经使用过Powershell,Eric描述的模式听起来应该很熟悉。Powershell cmdlet是monad;功能组成由管道表示。

杰弗里·斯诺弗对埃里克·梅杰的采访更为详细。

我将尝试使用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
}

在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是一个函数数组

(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)