前言
为什么要学习一门新的语言?我的想法很简单,平时OO、PO代码写多了,却从未接触过函数式编程,不免有些遗憾。考察下来,Clojure可以用来尝尝鲜,所以就决定学一学。为了给自己的学习留下些记录,就有了这样一份教程。
Clojure已经有一些不错的教程,如Mark Volkmann的Clojure - Functional Programming for the JVM,Storm的主要贡献者徐明明也对这个教程做了全文翻译。还有一些不错的书籍,像O’Reilly的Clojure Programming,都值得一读。我是从Mark的教程开始学起的,对其中没有提到的部分则是参考了Clojure Programming这本书。Clojure的官方网站上有详尽的API参考,可以作为工具书查阅。
但是,上面提到的教程都是针对Clojure语言本身的,从 Hello, world! 开始,讲解Clojure的各种语法,关键字,结构等等。虽然Clojure的语法已经足够吸引你的眼球,在REPL中敲击Clojure代码已经是一种莫大的乐趣了,但似乎还有些不够,我们想看到一个用Clojure编写的应用程序!
因为平时都是做Web开发,所以先从一个Web框架入手会是不错的选择,因此这份教程会从使用Noir框架搭建一个博客开始,带你领略Clojure的魅力。
一句话概述Clojure
Clojure是一种运行在JVM平台上的函数式编程语言。
安装Clojure
Clojure是以一个Jar包发行的,可以到官网下载后使用java -jar命令运行。而在实际开发中,我们会选择使用Leiningen或Maven来管理Clojure项目,本教程将以Leiningen(命令行是lein)作为项目管理工具进行讲解。
安装Leiningen
lein目前有1.x和2.x两个版本,后者还在alpha阶段。使用以下命令安装lein 1.x版本:
1 | $ cd ~/bin # 假设$HOME/bin目录在系统的$PATH中 |
这样就已经安装好了lein和Clojure环境,并启动了一个REPL,可以直接运行Clojure代码:
1 | user=> (+ 1 2) |
这里出现了Clojure的两个特点:圆括号和前缀表达式。Clojure的基本语法是(fn1 arg1 (fn2 arg2 arg3))。函数是Clojure中的“一等公民”,它即是可执行的代码,又是一种数据(类似闭包的概念)。以后我们会慢慢熟悉。
新建项目
1 | $ lein new proj |
lein new命令用来创建一个Clojure项目骨架,最重要的文件是project.clj,它声明了项目的基本属性以及依赖包。
lein plugin命令可以用来管理lein的插件,我们可以通过安装lein-noir插件来生成基于Noir的项目骨架:
1 | $ lein plugin install lein-noir 1.2.1 |
我们可以直接运行这个项目:
1 | $ cd blog |
浏览 http://localhost:8080 就能看到项目的页面了。
Noir项目的基本结构
项目基本信息:project.clj
Clojure文件都是以.clj为扩展名的。项目根目录下的project.clj文件包含了一些基本信息,我们逐一分析:
1 | (defproject blog "0.1.0-SNAPSHOT" |
defproject是Leiningen定义的一个宏,用来描述项目的基本信息。宏在Clojure中是一个很重要的语言特性,简单地说,开发者可以用宏创造出新的语法。
:description、:main等是一种直接量(literal),我们称之为关键字(keyword),通常以:开头,主要用来作为哈希表(map)中的键名,这里则用来表示项目的某种信息,从名称上应该很好理解。
[1 ["b", false]]中的[...]表示一个向量(vector),它的元素可以是任意类型,元素之间以空格或逗号分隔。这行代码也展示了Clojure中其他几种直接量:数值型、字符串、布尔型。
依赖项的描述也很直观,[groupId/artifactId "version"]。Clojure使用了和Maven相似的包命名方式,当groupId和artifactId相同时,可以进行简写,如[noir "1.2.1"]等价于[noir/noir "1.2.1"],这也是Clojure鼓励的做法。对依赖项进行修改后,可以运行lein deps命令进行安装。lein会先从Clojars上查找和下载,不存在时再到Maven中央仓库中搜索。
最后,:main顾名思义指向的是程序入口,它配置的是一个命名空间,其中会包含一个-main方法(注意方法名中的-)。Leiningen项目的目录结构也是按照命名空间来的,这点和Java一致。
入口文件:src/blog/server.clj
1 | (ns blog.server |
ns宏用于定义当前的命名空间,:require表示导入其它命名空间,:as则是将为导入的命名空间设置别名。注意这些关键字及其用法都是在ns宏中定义的,这也是为什么说宏可以用来创造新的语法。
关于命名空间,你在执行lein repl的时候可能会注意到,当在blog项目下执行时,提示符是blog.server=>,而在其他目录下执行时是user=>,因为user是Clojure默认的命名空间,可以通过(ns myspace)来切换成myspace=>。
server/load-views表示调用server命名空间下的load-views函数,后面的src/blog/views/则是函数的参数。在大多数语言中,函数名称不能包含特殊字符,如-,但Clojure中的变量名、函数名、关键字等都可以包含诸如*、+、!、>这样的特殊字符。其中一些字符是有特定含义的,如关键字必须以:开头,以::开头的则表示仅在当前命名空间中有效。这些约定需要注意。
defn宏用来定义一个函数,基本用法是(defn 函数名 [参数列表] 语句1 语句2)。如果参数数量不定,可以使用这样的语法[arg1 arg2 & args],这样当函数接收四个参数时,后两个参数会作为一个向量赋值给args变量。
关于let,它是继“宏”和“函数”之后出现的第三个术语,“特殊形式”(Special Form)。我们暂时不去了解它们之间的关系,先来看看let的作用。设想这样一个场景,我们在调用一个函数之前会准备一些参数传送给它,这些参数仅在函数内部可见,函数处理完后就会销毁。let则是将参数准备和函数调用这两步整合了起来。它的语法是(let [变量1 表达式1 变量2 表达式2] 语句1 语句2),举例来说,尝试在REPL中执行以下命令,思考一下结果是如何得出的:
1 | user=> (let [x 1 y (+ 2 3)] (+ x y)) |
入口文件中的表达式看起来有些复杂,但逐步拆解后就会明白:
1 | user=> (def m []) ; 定义一个变量,它是一个空向量,正如不带参数调用-main函数时一样。Clojure中分号表示注释。 |
经过处理,mode变量包含了:dev这个关键字,且作用域仅在(let …)中有效。
再来看看port变量,这里涉及到了与Java类的交互:
1 | user=> (System/getenv) ; 这里的System不是一个命名空间,而是Java的一个类。通过这种方式我们调用了System类的静态方法getenv,并返回了一个Map类实例。 |
关于map再补充一点,它虽然是Clojure的一种数据类型,但底层其实是Map接口一个实现,因此以下语法是合法的。类似的情况在Clojure中还有很多。
1 | user=> (def m {:a 1, :b 2}) ; map的语法是{键1 值1 键2 值2},为了加强可读性,这里使用了逗号分隔了两组键值,Clojure在编译时会将逗号转换成空格。 |
参数的赋值就结束了,后面的代码也很好理解:调用server命名空间下的start函数,参数是监听端口和一组由map表示的参数。这里blog之前的单引号需要注意,表示其后的代码不需要进行解析(evaluate),在表示命空间名时都需要加上(ns宏除外),如:
1 | user=> (require 'noir.server) ; 引入一个命名空间,使用noir.server/start调用方法。 |
小贴士
这一节中我们引入了不少Clojure的函数、宏、特殊形式,有时会需要查阅这些函数的用法。除了上网查找API文档,还可以在REPL中使用doc和source函数来返回某个函数或宏的文档和源码:
1 | user=> (doc first) |
基本页面:src/blog/views/welcome.clj
入口文件中的load-views函数会将指定目录下的所有文件都包含进来,这些文件中定义的是URL路由以及页面内容,以welcome.clj为例:
1 | (ns blog.views.welcome |
我们先跳过这些代码,来看看如何定义一个新的页面。将以下代码添加到welcome.clj尾部,然后执行lein run。
1 | (defpage "/greeting" [] |
访问 http://127.0.0.1:8080/greeting 就能看到一个新的页面了,页面源码是<h1>Hello, world!</h1>。
defpage是Noir的一个宏,用来定义URL和它返回的页面内容。URL的定义有很多其他用法,如POST方式、截取参数等,我们稍后都会用到。页面内容方面,可以直接返回字符串,如(defpage "/greeting" [] "Hello, world!"),也可以使用Hiccup构建HTML页面。Hiccup是Noir默认的模板引擎,简单来说就是用Clojure来写HTML。一个完整的页面示例如下:
1 | (ns ... |
生成的HTML是:
1 |
|
其中,html和html4都是Hiccup定义的宏。html仅将接收到的参数转换为HTML代码,html4则是会添加相应版本的<!DOCTYPE>和<html>标签。要使用这些宏需要引入相应的命名空间。:only则表示只引入命名空间中特定的宏。
关于命名空间中出现的-,Clojure在编译时会自动转换成_,从而确保程序在JVM中运行时不会出现问题。
页面模板:src/blog/views/common.clj
回到系统生成的welcome.clj文件,它并没有使用html或html4,而是调用了一个common/layout函数。那么让我们看看common.clj中这个函数的定义:
1 | (ns blog.views.common |
defpartial是Noir的一个宏,用来定义一段可复用的HTML代码。当然我们也可以将其定义为一个函数(用defn替换掉defpartial),不会有什么区别。官方文档的解释是使用defpartial会比较容易辨认。
include-css是一个函数,用来生成<link>标签。[:div#wrapper ...]会生成<div id="wrapper">...</div>。更多Hiccup的语法可以到这个页面浏览一下。
默认首页:noir.content.getting-started
我们在代码中并没有看到(defpage "/" [] ...)这样的定义,那为什么网站根目录会出现一个默认页面呢?答案在noir.content.getting-started这个命名空间中,可以点击这里查看它的源码。要取消这个默认页面,可以在welcome.clj的:require中将其删除。
静态资源:src/resources/public
Noir默认对src/resources/public目录下的文件做了路由,因此当有一个资源文件位于src/resources/public/css/reset.css时,可以通过http://127.0.0.1:8080/css/reset.css访问。
值得一提的是,Noir项目本身依赖于两个开源项目:ring和compojure,前者对HTTP请求进行了封装,提供了一套类似Python WSGI的API;后者则是专门提供URL路由功能的类库。如对静态资源的路由,实质上Noir是调用了compojure提供的resources函数,函数中又调用ring提供的GET、wrap-file-info等函数响应请求。
小结
本章讲述了Clojure环境的搭建,特别是项目管理工具lein的一般使用。通过对Noir项目骨架的分析,我们一窥Clojure的语法,接触了变量、直接量、函数、宏、命名空间的一些用法,并能结合Noir和Hiccup写出简单的页面来。下一章我们将讲解如何使用Noir编写表单页面进行交互,以及Clojure如何连接数据库,对博文进行增删改查等操作。