学习一门语言最好的方法就是去使用它。我们就从一个小例子来学习
Clojure语法.
首先先来看一下Clojure的核心语法。
Clojure的使用的是Lisp语法,又叫S表达式。核心语法非常的简单。但是对于熟悉c系语法
(c,c++,java)的朋友来说,第一次接触会非常的不习惯。
以前面的hello world程序为例。
(println "Hello World")
它以”(“开始,后面跟的是函数println,接着空格跟的是参数”Hello World”,最后”)”结尾。
了解了如上规则,你就基本学会了Clojure的大部分语法。下面比较一下
Clojure,Java,Python,Ruby的一些语法.
Clojure 表达式 | 对应的 Java 语法 | 对应的 Python 语法 | 对应的 Ruby 语法 |
---|---|---|---|
(not k) | !k | not k | not k or !k |
(inc a) | a++、++a、a += 1、a + 1a | a += 1、a + 1 | a += 1 |
(/ (+ x y) 2) | (x + y) / 2 | (x + y) / 2 | (x + y) / 2 |
(instance? java. util.List al) | al instanceof java.util.List | isinstance(al,list) | al.isa? Array |
(if (not a) (inc b) (dec b)) | !a ? b + 1 : b – 1 | b + 1 if not a else b-1 | !a ? b + 1 : b – 1 |
(Math/pow 2 10)c | Math.pow(2, 10) | pow(2, 10) | 2 ** 10 |
(.someMethod someObj “foo” (.otherMethod otherObj 0)) | someObj.someMethod(“foo” , otherObj.otherMethod(0)) | someObj.someMethod(“foo” , otherObj.otherMethod(0)) | someObj.someMethod(“foo” , otherObj.otherMethod(0)) |
可以看出Clojure的语法有高度的一致性,即使你不熟悉S表达式,但是依据上面
的原则,可以看懂它想表达的是一个什么意思。而对于其他三门语言,如果你没
有一个个的学习相应的语法,你还是比较难理解它的意思的。
了解了核心语法,我们就可以来编写代码了。我们要编写的代码功能很简单,进
行简繁翻译,其中过滤不需要翻译以及需要特殊翻译的文字。我们将分几步来完成:
我们先看第一个功能。我们要读取简繁字典。简繁字典其实就是简繁对照的文件,
我这里叫jfmap.clj
格式如下:
万 萬 与 與 丑 醜 专 專 业 業 丛 叢 东 東 丝 絲 丢 丟 两 兩 严 嚴 丧 喪 个 個 丬 爿
这里只是简单的列了一点。具体内容请见附件。有了这个文件,我们如何把内容
读出来呢?熟悉Java的都知道,我们要创建文件流来读取,然后要打开流,循环
读取,最后关闭流,还要抓异常。很繁琐。在Clojure中如何处理呢?Clojure提
供了slurp函数,可以根据提供的路径将文件内容读入。API如下:
clojure.core/slurp ([f & opts]) Opens a reader on f and reads all its contents, returning a string. See clojure.java.io/reader for a complete list of supported arguments.
OK。我们知道了要用什么函数。那么根据API和上面说的总规则,我们来写代码.
(slurp "jfmap-path")
你可以在REPL里面去实验这行代码的执行结果。执行此行代码,clojure会将jfmap.clj的内容以字符串的形式全部读
入。接着呢?要做简繁翻译,字符串肯定不方便我们的操作。很明显map才是最
适合的数据结构。那么我们如何将字符串变成map呢?
我们只能求助于API了,你可以在Clojure的Index页面搜索map,可以找到
hash-map函数。它的API说明如下:
hash-map function Usage: (hash-map) (hash-map & keyvals) keyval => key val Returns a new hash map with supplied mappings. If any keys are equal, they are handled as if by repeated uses of assoc.
根据提供的映射关系返回一个新的hashmap。而这里我们是一个字符串,如何提
供映射关系呢?按照空格将文字切开就行了嘛!!继续找API。Java里有split方
法,Clojure里有没有相应的函数呢?试试再说。。有了!在
clojure.string的Namespace1中,我们找到了叫split的函数!
split function Usage: (split s re) (split s re limit) Splits string on a regular expression. Optional argument limit is the maximum number of splits. Not lazy. Returns vector of the splits.
通过正则表达式来切割字符串。看着挺像,先用再说!我们有repl嘛!直接在
repl里面输入
(split "万 萬" #" ")
执行!Oops,报错了!
CompilerException java.lang.RuntimeException: Unable to resolve symbol: split in this context
找不到split?!如果在Java中报类似的错误,你会想到什么?没有引入包阿!这
里也是。在Java中会默认引入java.lang包,同理在Clojure中会引入
clojure.core和java.lang包。其他包则要自己引入,这里split在clojure.string包中。所
以你需要引入clojure.string包。
(require 'clojure.string)
这也就是调用了require函数来进行引入!为什么clojure.string前面有个单引
号呢?想想核心语法!这里暂不展开说!给大家留个思考题!!后续会专门对命名空间引入做详细介绍!
光引入还没用!调用代码也需要修改!
(clojure.string/split "万 萬" #" ")
你可能要吐槽了!既然引入了,为什么还要加Namespace前缀?!我们可以和Java作个
比较!如果这里是Java的话,那么我们在调用split的时候,实际上是需要一个
类作为前缀的,比如StringUtils.split()!但是在clojure中并没有类的概念!
Namespace下面只有函数,所以它使用命名空间来确保函数的唯一性引用!
当然了每次都要写这么长的命名空间的名字也是挺烦人的。Clojure提供了简写.
(require ['clojure.stirng :as 'cstr]) (cstr/split "万 萬" #" ")
:as是Keyword,是Clojure字面量的一种。它和String很类似,不过有些区别,它比String有更多的功能。
下表是Clojure所包含的字面量。
Type | Example(s) |
---|---|
Boolean | true,false |
Character | \a |
Keyword | :tag,:doc |
List | (1 2 3),(println “foo”) |
Map | {:name “Bill”,:age 42} |
Nil | nil |
Number | 1,4.2 |
Set | #{:snap :crackle :pop} |
String | “hello” |
Symbol | user/foo,java.lang.String |
Vector | [1 2 3] |
ok.终于得到了我们要的结果。这里的#” “是正则表达式(这是你遇到的第一个特殊语
法,学习方法—死记!!),它构建了Java中的Pattern,所以正则表达式内容
和Java完全相同,这里就不废话了。你只需要记住其语法就行了。
切开了字符串,我们来生成map吧!如何生成呢?你应该有答案了吧?
(hash-map (cstr/split "万 萬" #" "))
又报错了!
IllegalArgumentException No value supplied for key: ["万" "萬"] clojure.lang.PersistentHashMap.create (PersistentHashMap.java:77)
不合法的参数!!split得到的是个Vector([]包裹的数据结构是Vector),而
hash-map要的参数类似于Java中的可变参数!如何匹配这两者呢?Clojure中提
供了apply函数!API如下
clojure.core/apply ([f args] [f x args] [f x y args] [f x y z args] [f a b c d & args]) Applies fn f to the argument list formed by prepending intervening arguments to args.
此函数有点特别!它的第一个参数是函数,后面是该函数所需要的参数!知道
怎么调用吗?
(apply hash-map (cstr/split "万 萬" #" "))
终于成功了!!我们看到了结果
{"万" "萬"} ;以{}包裹的数据结构是map
最后呢!我们需要对jfmap.clj的内容进行处理!So easy!
(apply hash-map (cstr/split (slurp "jfmap-path") #" "))
上一节我们完成了对jfmap.clj的读取,并生成了hashmap。但是呢,如果我们
每次要使用jfmap.clj的时候都要写
(apply hash-map (cstr/split (slurp "jfmap-path") #" "))
太麻烦了。在Java中的可以将其封装为一个方法来进行调用。Clojure也可以将
其封装为函数:
(defn read-map "Read trans map from file" [path] (apply hash-map (clstr/split (slurp path) #" ")))
defn是个宏,宏的定义后面讨论。defn是用来定义有名函数的。read-map就是函
数名,和Java中的驼峰式取名不同,List系的取名方式一般是使用-。给出的原
因是大写字母要按两个按键,-只需要按一个按键!!紧接着函数名的是注释,
类似Java中的注释,不同的是其双引号内的内容可以多行。后面的vector是参数
列表,最后就是函数体了。
现在当你再读取jfmap的时候。只需要这样调用:
(read-map "jfmap-path")
比刚才简单了很多。但是每次都要去读取,还是不够方便。在Java中会将其赋值
给一个变量,然后去调用。Clojure当然也可以。
(def jfmap (read-map "jfmap-path"))
然后你直接操作jfmap就可以了。
现在我们就来按照jfmap来进行简繁翻译吧!
你可能会想,很简单吧?只需要遍历需要翻译的字符串,然后到jfmap中去找对应的翻译,然
后将翻译组装成字符串就行了。so easy!但当你这么想的时候,你已经陷入到了
实现的细节中了!Clojure让你能够更加的关注业务而不是实现细节!
这里给出的建议是:
请先查找Clojure是否可通过函数组合来解决问题?如果不能再考虑自己编写函 数!
为什么这么说呢?因为在Clojure中大部分的问题都可以通过其提供的函数组合
来解决。你需要做的就是根据需要来组合函数!这使你能更多的思考业务而非实
现细节!比如这里的问题!
我们需要一个函数:
很简单,我们先编写函数的定义。这里我们叫translate。
(defn translate "Trans string by map" [s] )
然后呢?需要翻译字符串。这里叫翻译字符串,实际上就是根据map的key找到
value而已。在Clojure中找到这样的函数就可以了。你可以找到get函数。
(get map k default)
很好理解吧?根据k从map中查找value,如果找不到则返回default。其实这就是
核心代码了!我们知道map就是我们这里的jfmap,k实际上是获取的字符串的每
个字符,而default呢?这里如果我们根据k找不到的话,就直接返回k,也就是
不翻译了。所以代码修改为:
(get jfmap k k)
现在只需要解决k就行了!继续寻找函数!现在需要的是在Clojure中用得还比较
多的一个函数—map!
clojure.core/map ([f coll] [f c1 c2] [f c1 c2 c3] [f c1 c2 c3 & colls]) Returns a lazy sequence consisting of the result of applying f to the set of first items of each coll, followed by applying f to the set of second items in each coll, until any one of the colls is exhausted. Any remaining items in other colls are ignored. Function f should accept number-of-colls arguments.
看到作用了吗?将函数应用到序列的每个元素上去!并返回一个由结果组成的
lazy序列!你可能会问了:这是针对序列的,对字符串有效吗?试试不就知道了?
(map class "aaa")
在repl里面输入如上的代码!你看到了什么?java.lang.Character?!没错,字
符串会被当作字符序列来操作!但是我们需要的是字符串啊?没关系,我们有
str函数!
(map (comp class str) "aaa")
comp是个什么东东?它的作用是将多个函数组合起来,从右向左的执行!!这里
就是先执行str在执行class,可以看到结果打印的是java.lang.String,正是我们
需要的。
OK,现在我们来组合这两个函数就行了.但是问题又来了!map的第二个参数是个函数,
我们怎么办呢?既然它需要函数,那我们就定义一个给它咯!
(defn tmp [k] (get jfmap k k)) (defn translate "Trans string by map" [s] (map tmp s) )
你可能要抱怨了!取了个什么烂名字!!居然叫tmp?!呵呵,别急!我把函数定
义为tmp,是因为我要将优化掉!可以看出,这个功能非常的简单,而且只会给
translate使用,那么我们需要特意定义一个单独的函数吗?
不需要吧?我们直接将两个函数合并好了!
(defn translate "Trans string by map" [s] (map (defn tmp [k] (get jfmap k k)) s))
很简单,但是呢!既然都放到函数内部了!还需要函数名吗?就像Java中的匿名
内部类一样,直接定义直接使用,不需要名字!在Clojure中有fn这个special
form来定义匿名函数!
上面说defn是个宏!它的功能就类似于(还有其他功能,比如注释):
(def tmp (fn [k] (get jfmap k k)))
我们使用fn来简化一下代码!
(defn translate "Trans string by map" [s] (map (fn [k] (get jfmap k k)) s))
舒服很多!还能更简单吗?当然!Clojure提供了#这个语法糖来定义匿名函数!
(defn translate "Trans string by map" [s] (map #(get jfmap k k) s))
好!问题来了!这里没有了参数列表!那get函数怎么知道k是个什么东西呢?所
以,这里使用%来替换,第一个参数用%或者%1替换,第二个则是%2,依次类推!
(defn translate "Trans string by map" [s] (map #(get jfmap % %) s))
搞定了吗?没有!translate需要返回一个字符串,而map返回的是个lazy序列!
需要将序列转化为字符串!到clojure.string找找!有个join函数
(defn translate "Trans string by map" [s] (clstr/join (map #(get jfmap % %) s)))
不废话了!记得要引入Namespace哦!!
实际上一行代码我们就搞定了基本的翻译了!很简单吧?
翻译的字,是使用的map来存储的?那不需要翻译的文字该如何存储呢?这要看
你如何处理了!我这里采用的是一个很简单的方式!
比如说,”阿里山”直接翻译的话,那么就会变成”阿裡山”,但是”里”字是不需要
翻译的。那么我就新建一个map,保存”阿裡山”->”阿里山”,将文字再翻回来!
那么这里我就再需要一个类似jfmap.clj的文件就可以了。我这里叫ntmap.clj。
格式和jfmap.clj类似!只不过里面存储的是需要反翻译的文字!
如何读取和组装map?不需要我废话吧?
过滤不翻译的文字,实际上就是反翻译!如何进行呢?原来在翻译的过程中我们
是一个字符一个字符的匹配的!但是这里是一个一个的字符串!这就比较难办了!
不管怎么说我们先定义函数!
(defn do-trans [s tmap ntmap] )
首先第一件事就是去全文翻译!然后获得结果!提供给后续函数使用!Java中有
局部变量!只需要将变量写在方法里就可以了!但是在Clojure中不同,它需要
通过let这个Special Form来处理。
(defn do-trans [s tmap ntmap] (let [re (translate s tmap)] ))
let后面是个Vector,用来进行var绑定,这里translate翻译的结果会被绑定到
re这个var上!而re的作用范围就只在let这个括号内部!接着呢!就是对re进行
反翻译!没啥现成的函数了!只能自己处理!代码如下!
(defn do-trans [s tmap ntmap] (let [re (translate s tmap)] (loop [result re k (keys ntmap)] (if (seq k) (recur (clstr/replace result (first k) (ntmap (first k))) (rest k)) result))))
loop又是个Special Form,看起来像循环!实际上它是个递归!loop后面也是
个参数Vector,功能和let的相同!这里将re绑定到了result,以及ntmap的key
绑定到了k上!
然后是if判断,还是个Special Form,(seq k)判断k是否是个序列!如果是则执
行recur,如果不是则返回result.其实seq并不是判断函数!它是用来构建序列
的!这里之所以能用来判断,基于两个原因:
然后seq就可以用来作为判断条件了!
recur是递归调用!这里调用的是loop,传递的参数是替换后的s和剩余的k.
实际功能就是,遍历ntmap中的key,如果找到了,则使用value替换掉!
至此,就完成了翻译的所有功能!考虑下如果使用Java需要多少行代码??
测试翻译一个页面的时间为70毫秒左右,速度还是不错的!
Clojure提供了将函数给Java调用的功能!
首先,在core.clj文件中编写函数,比如下面的翻译:
(defn -transAll "翻译所有" [source mapPath] (trans/translate source (trans/read-map mapPath) ""))
函数名前面一定要有个”-”。
然后在命名空间里添加如下代码
(ns jft.core ;这里开始 (:gen-class :name jft.core.Trans :methods [#^{:static true} [transAll [String String] String]]) ;这里结束 (:require [clojure.string :as cstr] [jft.trans :as trans]) )
应该不难理解吧?name是在Java中import的时候的名字!methods是可以调用的
方法,这里注解为静态方法!注意这里的transAll前面是没有”-”的,后面是参数
和返回类型!
需要给Java调用,那就要先封装为jar包。非常简单
lein jar
然后就可以提供给Java调用了!
jft
1 Namespace和Java中的包类似,但是在Clojure中叫Namespace。这里没有
将其翻译为命名空间,主要是怕有误解。