首页 > 代码库 > 如何写Clojure程序

如何写Clojure程序

一、clojure是什么?

    clojure官网上有详细的说明,虽然能够理解英文,但脑子里面总是找不到对应的中文来翻译,与其翻译的不伦不类,不如不翻译了。这里我用一个类比来初略的说明,不能保证原意的准确度,但你入门之后,自然会发现其中的差异,这种不准确不会损害你对该语言的理解和深入。

xx.json -> Gson | Jackson -> Java 数据结构

xx.clj -> core/read -> Clojure 数据结构

上面的示例中,xx.clj是一个clojure源程序代码文件,它相当于一个json文件,这就有别于其它类型的语言了,clojure程序就是数据结构。

关于Clojure是什么,就说这最重要的一点。

二、clojure处理问题,就像unix对于“文件”,趋向于一种统一的,高度概括的模式

如果你尝试用bash写代码,会发现所有的工具都采用类似的方法解决问题,比如sed,你面对的是“行”,你的代码看起来只处理了一行,其实是全部的行。

sed -i "s/java/clojure/" /etc/xxx/default.conf

虽然这个类比有点牵强,但是如果你不注意这一点,就用不好sed。同样,对于Clojure,你需要有clojure的思维。Clojure通过Sequence抽象绝大部分的问题,将处理许多对象变成处理一个对象。你的绝大部分思考是如何获得序列,然后转换,转化,直到你想要的结果。

顺着这个思路,我们来看如何解决刚才的sed解决的问题。这里先说说Clojure和Java的关系,Clojure在jvm中运行,可以调用几乎所有的java库,可以这样理解,clojure是老板,java是具有超人能力的打工仔,java能做的clojure都能做。为了代码看起来更像clojure,clojure将一些非常常用的java功能包裹了一下,比如io,当然不是全部,是最常用的。(这个编辑器没有clojure语法)

(with-open [rdr (io/reader (io/file "/etc/xxx/default.conf"))]
    (这里开始处理问题))

注意,不要去思考如何循环(如果你习惯其它程序语言),思考如何得到序列。

(with-open [rdr (io/reader (io/file "/etc/xxx/default.conf"))]
    (接下来 (line-seq rdr)))

注意,从最初的序列开始,括号一层一层向外扩展的过程,就是变换的过程。

(with-open [rdr (io/reader (io/file "/etc/xxx/default.conf"))]
    (下一步 (map #(str/replace %1 "java" "clojure") (line-seq rdr)))))

现在我们得到的是经过替换之后的所有的行,但这些行必须要消费,我们把它输出到一个新的文件中,比如/etc/xxx/default.conf.clojuretmp

(with-open [rdr (io/reader (io/file "/etc/xxx/default.conf")
            wrt (io/writer (io/file "/etc/xxx/default.conf.clojuretmp"))]
  (doseq [line (map #(str/replace % "java" "clojure") (line-seq rdr))]
    (.write wrt (str line "\n"))))

这是什么呀,这么简单的问题弄得这么复杂?

其实文件处理会产生clojure所说的副作用,而且刚才的例子是lazy(懒)序列,可以处理任意大小的文件,不会造成内存不够。如果不考虑这个因素,那么看起来就更clojure了。

先定义一个fn,得到所有行的序列。

(defn read-all-lines [& paths]
  (with-open [rdr (io/reader (apply io/file paths))]
    (doall (line-seq rdr))))

总共有几行?

(count (read-all-lines "c:" "test" "test.txt"))

列出包含java的行?

(filter #(re-find #"java" %) (read-all-lines "c:" "test" "test.txt"))

包含java的有几行?

(count (filter #(re-find #"java" %) (read-all-lines "c:" "test" "test.txt")))

如果你不断的用clojure的方式去思考,慢慢的会从最开始的别扭变得非常自然流畅。

三、reader forms,这个真不好翻译

随着学习的深入,你肯定会回顾整个问题。用初看clojure写的程序代码看起毫无规律,真的如此吗?请参阅:http://clojure.org/reader

上面出现的任意一行代码都可以归到仅有的几个类别中。下面我翻译一下:http://clojure.org/evaluation ,翻译涉及到专业术语的规范,这里可能没有遵循。

我的词汇对应:

Evaluation 求值

expression 表达式

symbol 符号

form 不知道怎么翻译

意译开始:

clojure的代码由表达式组成,除了special form 和 macro,都是表达式。这话有点矛盾,不过special form 和 macro 经过处理之后就是表达式了,这样一来也不算矛盾,只是它们俩有点特殊而已。

求值过程是统一的,没有例外。表达式作为一个整体 ,求值,返回结果。

“abc" \a 1 nil true false :keywords 计算的结果都是自己。

注意,你在代码中加入这样一行也是可以的,虽然没有什么意义:abc" \a 1 nil true false :keywords

接下来就是符号。如果你的代码中有这样一行:

whoami

clojure如何处理呢?判断循序如下:

1、是special form吗?是special form的话,由read会特殊处理,这个special form 不多,没几个。 http://clojure.org/special_forms

2、是指向java类吗? (String. "abc"),这里的String就是,当然这种写法不过是一个macro,clojure会完成转换

3、在本地上下文中有绑定吗? (let [whoami "name"] whoami),那么前面一个whoami怎么回事?let是special form,所以clojure会处理。

4、当前名字空间中是否有绑定?

5、以上都不是,那就是出错了。 

现在是list,规则如下

1、空的list就是自己

2、不空的list,是:special form,macro,function,三者之一。

按照这个规则,以下代码:

(count (filter #(re-find #"java" %) (read-all-lines "c:" "test" "test.txt")))

最外围的list,count是一个函数,带一个参数,内嵌的filter也是函数,带2个参数。#(re-find #"java" %) 是一个macro,re-find是一个函数,带2个参数,#"java"是macro,正则表达式,%由macro来解释,所以不用担心。read-all-lines是在当前名字空间中定义的,这个符号绑定到fn。后面的"c:"这样的表达式求值结果都是自己。

就先写到这里吧。有错误,请不用客气,直接指出。



如何写Clojure程序