看到函数式编程相关的资料的时候, 总是看到 Monad 这个词, 一直想了解一下, 然而查资料对 Monad 的定义往往是上来一大堆数学概念:
Monad 是一个自函子范畴上的幺半群
鉴于本人数学基础实在太差, 一直没能理解. 其实撇开这些数学概念来说, Monad 本身是一个非常简 单的东西, 像是 Rust 中的 Option 一样, 一旦理解, 就发现再也回不去之前没有他的世界了. 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 + 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)
这种方法简直太丑陋了, 首先我们重复编写了好多胶水代码, 而且如果我们要再添加一个函数 f4 的 话, 就得再多些两行胶水代码. 更糟糕的是, 不断改变 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 函数把参数 x 变成了 (int, str) 构成的 tuple. 接下来的 bind 函数调用了他的参数 f 函 数, 同时把结果累加到了形参 t 上.
这种方法避免了第一种方法的缺点, 因为所有的胶水代码都在 bind 函数中, 当我们要添加一个新的 函数的时候, 只需要接着链式调用就可以了.
print(bind(bind(bind(bind(unit(x), f1), f2), f3), f4))
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 的话, 又得再添加一些新的胶水代码了.