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

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

使现代化

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


当前回答

快速解释:

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

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

其他回答

monad在OO中是否具有“自然”解释取决于monad。在像Java这样的语言中,您可以将may monad转换为检查空指针的语言,这样失败的计算(即,在Haskell中生成Nothing)会将空指针作为结果发出。您可以将状态monad转换为通过创建可变变量和更改其状态的方法生成的语言。

monad是内函子范畴中的幺半群。

这句话所表达的信息非常深刻。你在一个monad中使用任何命令式语言。monad是一种“有序”的领域特定语言。它满足某些有趣的财产,这些属性使单子函数成为“命令式编程”的数学模型。Haskell使定义小型(或大型)命令式语言变得容易,这些语言可以以多种方式组合。

作为一名OO程序员,您使用语言的类层次结构来组织可以在上下文中调用的函数或过程的类型,即您所称的对象。monad也是对这个概念的抽象,因为不同的monad可以以任意方式组合,有效地将所有子monad的方法“导入”到范围中。

从体系结构上讲,然后使用类型签名来明确表示可以使用哪些上下文来计算值。

可以使用monad转换器来实现这一目的,并且有一个高质量的所有“标准”monad集合:

列表(通过将列表视为域进行非确定性计算)可能(计算可能失败,但报告不重要)错误(可能失败并需要异常处理的计算Reader(可以由普通Haskell函数组合表示的计算)编写器(使用顺序“渲染”/“记录”(到字符串、html等)进行计算)续(续)IO(取决于底层计算机系统的计算)状态(上下文包含可修改值的计算)

具有相应的monad变压器和类型类别。类型类允许通过统一monad的接口来组合monad,从而使具体monad可以实现monad“类”的标准接口。例如,模块Control.Monad.State包含一个类MonadState s m,(State s)是表单的一个实例

instance MonadState s (State s) where
    put = ...
    get = ...

长话短说,monad是一个函子,它将“上下文”附加到一个值上,它可以向monad中注入一个值,并且可以根据附加到它上的上下文来评估值,至少是以受限的方式。

So:

return :: a -> m a

是一个函数,它将a类型的值注入到m类型的monad“action”中。

(>>=) :: m a -> (a -> m b) -> m b

是一个执行monad操作、评估其结果并将函数应用于结果的函数。(>>=)的妙处在于结果在同一个monad中。换句话说,在m>>=f中,(>>=)从m中提取结果,并将其绑定到f,这样结果就在monad中。(或者,我们可以说(>>=)将f拉入m,并将其应用于结果。)因此,如果我们有f::a->m b和g::b->m c,我们可以“排序”动作:

m >>= f >>= g

或者,使用“do符号”

do x <- m
   y <- f x
   g y

(>>)的类型可能是发光的。它是

(>>) :: m a -> m b -> m b

它对应于C等过程语言中的(;)运算符。它允许使用以下表示法:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

在数学和哲学逻辑中,我们有框架和模型,这些框架和模型“自然”地用单子主义建模。解释是一种函数,它查看模型的域,并计算命题(或公式,在推广下)的真值(或推广)。在必要性的模态逻辑中,我们可能会说,如果命题在“每个可能的世界”中都是真的,那么它是必要的——如果它在每个可容许的域中都是真实的。这意味着命题语言中的模型可以具体化为一个模型,其域由不同模型的集合组成(一个对应于每个可能的世界)。每个monad都有一个名为“join”的方法,该方法将分层,这意味着结果为monad动作的每个monad动作都可以嵌入到monad中。

join :: m (m a) -> m a

更重要的是,这意味着monad在“层堆叠”操作下关闭。这就是monad转换器的工作原理:它们通过为以下类型提供“类联接”方法来组合monad:

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

这样我们就可以将(MaybeT m)中的作用转换为m中的作用,有效地折叠层。在本例中,runMaybeT::MaybeT m a->m(Maybe a)是我们的类联接方法。(MaybeT m)是一个monad,MaybeT::m(Maybe a)->MaybeT ma实际上是m中一个新类型monad动作的构造函数。

函子的自由单元是通过堆叠f生成的单元,这意味着f的每个构造函数序列都是自由单元的元素(或者更确切地说,是与f的构造函数序列树形状相同的元素)。自由单体是一种用最少的锅炉板构建柔性单体的有用技术。在Haskell程序中,我可能会使用自由monad来为“高级系统编程”定义简单monad,以帮助维护类型安全(我只是在使用类型及其声明。使用组合子可以直接实现):

data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a


type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom     :: Random r r
runRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

Monastem是您可能称之为“解释器”或“命令”模式的基础架构,抽象为最清晰的形式,因为每个单元计算都必须“运行”,至少是微不足道的。(运行时系统为我们运行IO monad,是任何Haskell程序的入口点。IO通过按顺序运行IO操作来“驱动”其余的计算)。

join的类型也是我们得到monad是内函子范畴中的幺半群的陈述的地方。由于其类型,联接对于理论目的来说通常更为重要。但了解类型意味着了解单子。Join和monad变换器的类Join类型在函数组合的意义上是内函子的有效组合。把它放在类似Haskell的伪语言中,

Foo::m(m a)<->(m.m)a

为了尊重快速阅读者,我首先从精确的定义开始,继续快速的“简单的英语”解释,然后转到示例。

这里有一个简洁而精确的定义,稍微改写了一下:

monad(在计算机科学中)是一个正式的地图,它:将某些给定编程语言的每个类型X发送到一个新的类型T(X)(称为“值为X的T计算类型”);配备有一个规则,用于组合表单的两个功能f: X->T(Y)和g:Y->T(Z)到函数g∘f:X->T;以一种在明显意义上是关联的并且与称为pure_X:X->T(X)的给定单位函数是单位的方式,可以被认为是将一个值带到纯计算,而纯计算只是返回该值。

因此,简单地说,monad是从任何类型X传递到另一类型T(X)的规则,也是从两个函数f:X->T(Y)和g:Y->T(Z)(您想合成但不能合成)传递到新函数h:X->T(Z)的规则。然而,这并不是严格数学意义上的作文。我们基本上是在“弯曲”函数的组成或重新定义函数的组成方式。

此外,我们需要monad的合成规则来满足“显而易见”的数学公理:

联想性:先用g合成f,然后用h(从外部)合成,应该与先用h合成g,然后用f(从内部)合成相同。酉性质:用任意一侧的单位函数合成f应得到f。

同样,简单地说,我们不能随心所欲地重新定义函数组合:

我们首先需要关联性,以便能够在一行中组合多个函数,例如f(g(h(k(x))),而不用担心指定组合函数对的顺序。由于monad规则只规定了如何组成一对函数,如果没有这个公理,我们需要知道哪个函数对是首先组成的,依此类推。第二,我们需要单位性质,也就是简单地说,恒等式以我们期望的方式构成。因此,只要可以提取这些标识,我们就可以安全地重构函数。

简单地说:monad是类型扩展和组合函数的规则,满足两个公理——结合性和单位性质。

实际上,您希望monad由负责编写函数的语言、编译器或框架为您实现。因此,您可以专注于编写函数的逻辑,而不用担心它们的执行是如何实现的。

简而言之,这就是它的本质。


作为一名职业数学家,我倾向于避免将h称为f和g的“组合”,因为在数学上,它不是。将其称为“组成”错误地假定h是真正的数学组成,但它不是。它甚至不是由f和g唯一决定的,而是我们的monad新的“组成函数的规则”的结果。这可能与实际的数学组成完全不同,即使后者存在!


为了使它不那么干燥,让我试着用例子来说明我正在用小部分进行注释,因此您可以直接跳到要点。

作为Monad示例引发异常

假设我们要组成两个函数:

f: x -> 1 / x
g: y -> 2 * y

但未定义f(0),因此引发异常e。那么如何定义组成值g(f(0))?当然,再次抛出异常!可能是相同的e.可能是新更新的异常e1。

这里到底发生了什么?首先,我们需要新的异常值(不同或相同)。你可以称它们为nothing或null或其他任何东西,但本质保持不变——它们应该是新的值,例如,在我们这里的示例中,它不应该是数字。我宁愿不将它们称为null,以避免混淆null如何在任何特定语言中实现。同样,我更倾向于避免任何事情,因为它经常与null联系在一起,原则上,这是null应该做的,然而,无论出于什么实际原因,这个原则经常会被扭曲。

例外到底是什么?

这对任何有经验的程序员来说都是一件小事,但我想说几句话来消除任何困惑:

异常是一个封装有关执行的无效结果如何发生的信息的对象。

这可以包括丢弃任何细节并返回单个全局值(如NaN或null),或者生成一个长日志列表或发生了什么,将其发送到数据库并在分布式数据存储层上进行复制;)

这两种极端例外情况之间的重要区别在于,在第一种情况下,没有副作用。第二个是。这就引出了(千美元)问题:

纯函数中是否允许异常?

简短的回答:是的,但前提是它们不会导致副作用。

更长的答案。为了保持纯粹,函数的输出必须由其输入唯一确定。因此,我们通过将0发送到我们称为异常的新抽象值e来修改函数f。我们确保值e不包含外部信息,这些信息不是由我们的输入(即x)唯一确定的。因此,这里是一个没有副作用的异常示例:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

这里有一个副作用:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

事实上,只有当这种信息在未来可能改变时,它才会产生副作用。但如果它被保证永远不会改变,那么这个值就会变得唯一可预测,因此不会有副作用。

让它更傻。返回42的函数显然是纯的。但如果有人疯狂地决定将42作为一个变量,这个值可能会改变,那么在新的条件下,同样的函数就不再是纯函数。

注意,为了简单起见,我使用了对象文字符号来演示其本质。不幸的是,在JavaScript这样的语言中,错误并不是一种我们想要的函数组合方式,而像null或NaN这样的实际类型并不是这种方式,而是经过一些人工的、不总是直观的类型转换。

类型扩展名

当我们想要改变异常内部的消息时,我们实际上是在为整个异常对象声明一个新的类型E,然后这就是may数的作用,除了它令人困惑的名称,它要么是类型number,要么是新的异常类型E,所以它实际上是number和E的并数|E。特别是,它取决于我们想要如何构造E,这既不是建议的,也不是反映在名称may数中。

什么是功能成分?

这是取函数的数学运算f: X->Y和g:Y->Z和构造它们的组成作为满足h(X)=g(f(X))的函数h:X->Z。当结果f(x)不允许作为g的参数时,就会出现这个定义的问题。

在数学中,没有额外的工作,这些函数是无法合成的。对于我们上面的f和g的例子,严格的数学解决方案是从f的定义集合中删除0。有了新的定义集合(新的更严格的x类型),f变得可以与g组合。

然而,在编程中这样限制f的定义集是不太实际的。相反,可以使用异常。

或者作为另一种方法,人工值被创建为NaN、undefined、null、Infinity等。因此,您可以计算1/0到Infinity和1/-0到Infinity。然后将新值强制返回到表达式中,而不是引发异常。导致您可能会或可能无法预测的结果:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

我们又回到了常规数字,准备继续前进;)

JavaScript允许我们以任何代价继续执行数值表达式,而不会像上面的示例那样抛出错误。这意味着,它还允许组合函数。这正是monad的意义所在——这是一条规则,可以组成满足本答案开头定义的公理的函数。

但是,由JavaScript处理数字错误的实现所产生的组成函数的规则是一个monad吗?

要回答这个问题,你只需要检查公理(这里作为练习而不是问题的一部分;)。

抛出异常可以用于构造monad吗?

事实上,一个更有用的monad应该是规定如果f对某个x抛出异常,那么它与任何g的组合也是如此。Plus使异常E全局唯一,只有一个可能的值(范畴理论中的终端对象)。现在这两个公理可以立即检查,我们得到了一个非常有用的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是一种抽象数据类型,用于表示计算(而不是域模型中的数据)。Monads公司允许程序员链接动作一起构建管道,其中每个动作都用提供了其他处理规则莫纳德。编写的程序功能性风格可以利用monads来构造程序包括顺序操作,1[2]或定义任意控制流(如处理并发,延续或例外)。形式上,monad由定义两个操作(bind和return)和类型构造函数M必须满足几个财产才能允许正确组成一元函数(即使用monad中的值作为参数)。返回操作需要一个普通类型的值,并将其放入装入M型一元容器中。绑定操作执行反向处理,提取集装箱的原始价值,以及将其传递给关联的下一个函数。程序员将编写monadic定义数据处理的函数管道monad充当框架,因为它是一种可重用的行为这决定了调用管道,并管理所有需要的卧底工作计算[3] 绑定和返回管道中交错的运算符将在每个monadic之后执行函数返回控制,并将注意特定方面由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;
}