Clojure程序员的Monad之旅(Part 1)


译者注,在一些Clojure同学的要求下,翻译了这个系列,虽然是09年的老文章了,但是讲解了FP的一些重要概念,不妨一读。 

 

Monad在函数式编程中常常跟Haskell联系在一起。在Haskell中,他们在I/O等很多应用中扮演了核心角色。目前多数对monad的介绍都是为Haskell程序员编写的,尽管如此,monad可以在任何一种函数语言中使用,甚至某些语言跟Haskell想去甚远。这里,我要使用Clojure来解释monad的概念,Clojure是一种拥有强大函数编程支持的,时髦的lisp方言。

Clojure的库:clojure.contrib.monads实现了monad

首先,在REPL中引入这个库

1 (use 'clojure.contrib.monads) 


巫云@:clojure升级到1.3以后,monads的库被移出contrib,目前在clojure.algo.monads中,可从maven下载,用leiningen的同学参考我前面的文章《64位window7下配置Clojure+Emacs开发环境》:在project1.clj的:dependencies节,增加[org.clojure/algo.monads "0.1.0"],重新运行lein deps,下载需要的库。 引入库的代码也相应改成(use 'clojure.algo.monads)

 

Monad 用来把多个运算步骤组合成一个更大的多步运算。让我们来创建一个最简单的monad,这个monadHaskell中被称为identiy monad。实际上Clojure语言已经内建了这个机制,而且你一定使用过,它就是let


看下面的代码:

 

(let [a 1
b (inc a)]
(* a b))




它可以被理解为一个3步运算:

1.    计算1(常量),结果叫做a

2.    计算(inc a),结果叫做b.

3.    计算(* a b),结果作为整个多步运算的结果


每一步都访问了前一个步骤运算结果绑定到的符号


现在假设 Clojure没有let表达式,我们可以使用什么办法来达到同样的效果呢?对,我们可以使用函数,下面的表达式实现了这个效果:

 

1 ( (fn [a] ( (fn [b] (* a b)) (inc a) ) ) 1 )




最外层参数为a的匿名函数,接受的参数是1,通过这种方式,我们把1 绑定到a。在这个函数内部,使用同样的结构,使参数为b的函数,调用 inc a)作为他的传入参数。可以在REPL中验证,结果与let形式的一致。


当然,我们不仅仅为let重新造个轮子。这个计算步骤看起来与表达的相反,而且整个结构难于理解。但是我们可以通过一个小的辅助函数bind,使运算顺序变成从右向左的。这里我们把他成为m-bindmonadbind),这也是他在Clojuremonad库中的名字。首先,我们定义函数:(注,如果已经引入了monads库,会提示名称冲突,可以换个名字)

 

 

1 (defn m-bind [value function]
2 (function value))




正如你看到的,他的工作很简单,但是却令参数值在函数左边传入成为可能。使用m-bind,我们可以这样写:

 

1 (m-bind 1 (fn [a]
2 (m-bind (inc a) (fn [b]
3 (* a b)))))




现在,这个方式,还是不如let做的漂亮,但是我们已经离目标更近了。有一个宏可以把let转换成由m-bind组合成的一个操作链,这个宏就是domonad。现在我们可以这样写我们的例子:

 

1 (domonad identity-m
2 [a 1
3 b (inc a)]
4 (* a b))




现在这已经看起来跟let方式几乎没什么差异了, 运行macroexpand-1把宏展开

 

1 (clojure.contrib.monads/with-monad identity-m
2 (m-bind 1 (fn [a] (m-bind (inc a) (fn [b] (m-result (* a b)))))))




通过结果我们看到,在一个(with-monad identity-m ...)定义的块中,通过m-bind组成了一个操作链,并调用m-result (后面解释)。对于identiy monadindentiy-m)来说,m-result就是identiy-它的名字。


你可能已经猜到,monadlet的泛型化,通过把m-bind替换为其他函数,可以创造更多的功能。每个monad定义了对m-bind的实现,并实现对应的m-result。通过一个with-monad块,把这些实现绑定起来。这样,你就可以用一单个表达式来组合多个步骤。通常情况下,我们是用domonad宏,它可以帮助我们做这些事情

在第二个例子中,我们将看到另一个简单的monad,但是它能带给你在let中你没有发现的知识。假设,你的运算有时可能失败,这时要返回nil。我们来写这样一个函数:

 

1 (defn f [x]
2 (let [a x
3 b (inc a)]
4 (* a b)))




在一个可能失败的运算中,我们想在xnil或者(inc a)产生nil的时候返回nil。当然了,(inc a)永远多不会返回nil,这里只是做个例子。无论如何,这个思想的中心就是当任何运算步骤产生的nil,最终结果就是nil,剩下的步骤将被取消。我们需要做出一些改动:

 

1 (defn f [x]
2 (domonad maybe-m
3 [a x
4 b (inc a)]
5 (* a b)))




这个maybe monad,代表着运算的结果可能是有效值,也可能是nil。他的m-result函数仍然是identiy,这里我们就略过对m-result的讨论(在第二部分中将详细介绍)。所有的神奇都在m-bind函数中:(注:每个不同的monad,差别就在于对m-bindm-result的不同实现,这里可以看到identity-mmaybe-m的区别)

 

1 (defn m-bind [value function]
2 (if (nil? value)
3 nil
4 (function value)))



value参数的值不是nil时,调用function函数,与indentiy monad做的工作一样。记住,这个函数的效果影响了所以的计算步骤。如果valuenil,那么m-bind返回nil,剩余的计算都不会被执行。当调用(f 1)返回的是2;当调用(f nil),返回nil,无需在每一步的计算中添加检测nil的代码,m-bind悄悄的为你做了这些事。

 

part2中,我将介绍更多的monad,并且讨论一些在任何用于辅助组合运算的monad中都能使用的泛型函数。


关于monad,dict.cn解释为:单位,单一体,单细胞生物,一价原子
我的理解,在函数式语言中,monad指通过一种形式,把多个运行步骤组合在一起的方式

posted on 2012-03-16 17:57  巫云  阅读(1789)  评论(1编辑  收藏  举报

导航