参赛作品
在
YOW! 2013年 Haskell语言的开发者之一
。 菲利普·沃德勒(Philip Wadler)展示了Monad如何允许纯函数式语言执行本质上必要的操作,例如输入输出和异常处理。 毫不奇怪,观众对这个主题的兴趣在互联网上有关单子晶体的出版物中取得了爆炸性的增长。 不幸的是,这些出版物中的大多数都使用以函数式语言编写的示例,这意味着函数式编程的新手希望了解monad。 但是monad并不特定于Haskell或功能语言,并且可以通过命令式编程语言中的示例很好地说明。 这就是本指南的目的。
本指南与其他指南有何不同? 我们将尝试仅凭直觉和一些Python代码基本示例在不超过15分钟的时间内打开monad。 因此,我们将不会开始理论化和深入研究哲学,而谈论
卷饼 ,
宇航服 ,
书桌和内窥镜。
励志例子
我们将考虑与职能组成有关的三个问题。 我们将通过两种方式解决它们:通常的命令式命令和单子命令。 然后,我们比较不同的方法。
1.记录
假设我们有三个一元函数:
f1
,
f2
和
f3
,它们采用一个数字并将其返回分别增加1、2和3。 每个功能还生成一条消息,该消息是有关已完成操作的报告。
def f1(x): return (x + 1, str(x) + "+1") def f2(x): return (x + 2, str(x) + "+2") def f3(x): return (x + 3, str(x) + "+3")
我们想将它们链接在一起以处理参数
x
,换句话说,我们想计算
x+1+2+3
。 另外,我们需要对每个函数的功能做出易于理解的解释。
我们可以通过以下方式获得所需的结果:
log = "Ops:" res, log1 = f1(x) log += log1 + ";" res, log2 = f2(res) log += log2 + ";" res, log3 = f3(res) log += log3 + ";" print(res, log)
该解决方案不理想,因为它包含大量单调的中间件。 如果要向链中添加新功能,将被迫重复此链接代码。 此外,使用
res
和
log
变量进行操作会损害代码的可读性,从而使得难以遵循程序的主要逻辑。
理想情况下,程序应看起来像简单的函数链,例如
f3(f2(f1(x)))
。 不幸的是,
f1
和
f2
返回的数据类型与参数类型
f2
和
f3
不匹配。 但是我们可以向链中添加新功能:
def unit(x): return (x, "Ops:") def bind(t, f): res = f(t[0]) return (res[0], t[1] + res[1] + ";")
现在我们可以解决以下问题:
print(bind(bind(bind(unit(x), f1), f2), f3))
下图显示了在
x=0
处发生的计算过程。 这里的
v1
,
v2
和
v3
是对
unit
和
bind
调用的结果。

unit
函数将输入参数
x
转换为数字和字符串的元组。
bind
函数调用作为参数传递给它的函数,并将结果累加到中间变量
t
。
通过将中间件放在
bind
函数中,我们能够避免重复中间件。 现在,如果我们得到函数
f4
,我们只需将其包括在链中:
bind(f4, bind(f3, ... ))
而且我们不需要进行任何其他更改。
2.中间值列表
我们还将以简单的一元函数开始此示例。
def f1(x): return x + 1 def f2(x): return x + 2 def f3(x): return x + 3
与前面的示例一样,我们需要组合这些函数才能计算
x+1+2+3
。 我们还需要获取函数工作所获得的所有值的列表,即
x
,
x+1
,
x+1+2
和
x+1+2+3
。
与前面的示例不同,我们的函数是可组合的,也就是说,它们的输入参数的类型与结果的类型一致。 因此,简单链
f3(f2(f1(x)))
将返回最终结果。 但是在这种情况下,我们会丢失中间值。
让我们解决“正面”的问题:
lst = [x] res = f1(x) lst.append(res) res = f2(res) lst.append(res) res = f3(res) lst.append(res) print(res, lst)
不幸的是,该解决方案还包含许多中间件。 如果我们决定添加
f4
,我们将再次不得不重复此代码以获取正确的中间值列表。
因此,如上例所示,我们添加了两个附加功能:
def unit(x): return (x, [x]) def bind(t, f): res = f(t[0]) return (res, t[1] + [res])
现在,我们将程序重写为调用链:
print(bind(bind(bind(unit(x), f1), f2), f3))
下图显示了在
x=0
处发生的计算过程。 同样,
v1
,
v2
和
v3
表示从
unit
和
bind
调用获得的值。

3.空值
让我们尝试给出一个更有趣的示例,这次使用类和对象。 假设我们有一个带有两个方法的
Employee
类:
class Employee: def get_boss(self):
Employee
类的每个对象都有一个管理器(
Employee
类的另一个对象)和一个薪水,可以通过适当的方法访问它们。 两种方法都可以返回
None
(员工没有经理,工资未知)。
在此示例中,我们将创建一个显示该员工的领导者薪水的程序。 如果找不到经理,或者无法确定其薪水,则程序将返回
None
。
理想情况下,我们需要编写类似
print(john.get_boss().get_wage())
但是在这种情况下,如果任何方法返回
None
,我们的程序将以错误结尾。
解决这种情况的一种明显方法如下所示:
result = None if john is not None and john.get_boss() is not None and john.get_boss().get_wage() is not None: result = john.get_boss().get_wage() print(result)
在这种情况下,我们允许额外调用
get_boss
和
get_wage
。 如果这些方法足够繁重(例如,访问数据库),那么我们的解决方案将无法解决。 因此,我们对其进行了更改:
result = None if john is not None: boss = john.get_boss() if boss is not None: wage = boss.get_wage() if wage is not None: result = wage print(result)
该代码在计算方面是最佳的,但是由于三个嵌套的
if
导致阅读效果很差。 因此,我们将尝试使用与前面的示例相同的技巧。 定义两个功能:
def unit(e): return e def bind(e, f): return None if e is None else f(e)
现在,我们可以将整个解决方案放在一行中。
print(bind(bind(unit(john), Employee.get_boss), Employee.get_wage))
您可能已经注意到,在这种情况下,我们不必编写
unit
函数:它仅返回输入参数。 但是,我们将保留它,以便我们随后更容易推广我们的经验。
还要注意,在Python中,我们可以将方法用作函数,这使我们可以编写
Employee.get_boss(john)
而不是
john.get_boss()
。
下图显示了John没有领导者时的计算过程,即
john.get_boss()
返回
None
。

结论
假设我们要组合相同类型的函数
f1
,
f2
,
…
,
fn
。 如果它们的输入参数与结果相同,则可以使用形式为
fn(… f2(f1(x)) …)
的简单链。 下图显示了具有中间结果的通用计算过程,中间结果表示为
v1
,
v2
,
…
,
vn
。

通常,此方法不适用。 与第一个示例一样,输入值和函数结果的类型可能会有所不同。 或函数可以是可组合的,但是我们希望在调用之间插入其他逻辑,如示例2和3所示,我们分别插入了中间值的集合和对空值的检查。
1.必要的决定
在所有示例中,我们首先使用最直接的方法,可以通过下图表示:

在调用
f1
之前
f1
我们进行了一些初始化。 在第一个示例中,我们初始化了一个用于存储公共日志的变量,在第二个示例中,初始化了一个中间值列表。 之后,我们在函数调用中插入了一定的连接代码:我们计算了聚合值,并检查了结果是否为
None
。
2.单子
正如我们在示例中看到的那样,命令性决策总是遭受冗长,重复和令人困惑的逻辑的困扰。 为了获得更精美的代码,我们使用了某种设计模式,根据该设计模式,我们创建了两个函数:
unit
和
bind
。 此模板称为
monad 。 当
unit
实现初始化时,
bind
函数包含中间件。 这使我们可以将最终解决方案简化为一行:
bind(bind( ... bind(bind(unit(x), f1), f2) ... fn-1), fn)
计算过程可以用图表示:

调用
unit(x)
生成
v1
的初始值。 然后
bind(v1, f1)
生成一个新的中间值
v2
,该中间值将在下一次调用
bind(v2, f2)
。 该过程一直持续到获得最终结果为止。 通过定义各种
unit
并在此模板的框架内进行
bind
,我们可以将各种功能组合到一个计算链中。 Monad库(
例如PyMonad或OSlash,大约翻译 )通常包含用于实现某些功能组合的即用型Monad(成对的
unit
和
bind
函数)。
要链接功能,
unit
和
bind
返回的值必须与
bind
输入参数的类型相同。 这种类型称为
monadic 。 根据上图,变量
v1
,
v2
,
…
,
vn
的类型必须是单子类型。
最后,考虑如何改善使用monad编写的代码。 显然,重复的
bind
调用看起来不太优雅。 为了避免这种情况,请定义另一个外部函数:
def pipeline(e, *functions): for f in functions: e = bind(e, f) return e
现在代替
bind(bind(bind(bind(unit(x), f1), f2), f3), f4)
我们可以使用以下缩写:
pipeline(unit(x), f1, f2, f3, f4)
结论
Monad是用于组合功能的简单而强大的设计模式。 在声明性编程语言中,它有助于实现命令式机制,例如日志记录或输入/输出。 用命令式语言
它有助于概括和缩短链接同一类型函数的一系列调用的代码。
本文仅提供对Monad的肤浅而直观的理解。 您可以通过以下渠道找到更多信息:
- 维基百科
- Python中的Monad(语法不错!)
- Monad教程时间表