就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?

它解决了什么问题,最常用的地方是什么?

使现代化

为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?


当前回答

可选/可能是最基本的一元类型

单子是关于功能组成的。如果函数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));
      });
    });
  });

其他回答

在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的例子,并正式定义“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;
}

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)

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

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

快速解释:

单体(在函数式编程中)是具有上下文相关行为的函数。

上下文作为参数传递,从先前的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)