又一份 ClojureScript 介绍
又一份 ClojureScript 介绍
ClojureScript 是什么样的
ClojureScript 是一门编译到 JavaScript 的 Lisp 方言, 就像 CoffeeScript.
Clojure 是 Lisp 方言, 所以它的语法基于 S-Expression(S 表达式),
"S 表达式"大量使用圆括号比如 (f arg1 arg2)
来控制代码的嵌套结构,
甚至于像是平常的 a + b + c
在 S 表达式当中也编程 (+ a b c)
.
这是一种"前缀表达式"的写法, 它很灵活, 可以构造出非常灵活的代码,
比如这样一段代码, 可以完成 10 以内的奇数的平方求和:
(->> (range 10) (filter odd?) (map (fn [x] (* x x))) (reduce +))
然后你可以按照 Lumo, 保存上面的代码到 app.cljs
, 然后运行它:
npm install -g lumo-cljs lumo app.cljs
Clojure 为了能更方便, 使用了方括号和花括号作为特殊的语法.
上面的代码当中有个 (fn [x] (* x x))
, 其中函数参数就必须要 [x]
写.
这段代码如何执行
这段代码首先运行生成一个长度为 10 的列表(List):
(range 10) ; (0 1 2 3 4 5 6 7 8 9)
然后运行 filter
函数过滤列表, 使用 odd?
来判断是否是奇数:
(filter odd? (list 0 1 2 3 4 5 6 7 8 9)) ; (1 3 5 7 9)
(fn [x] (* x x))
是一个匿名函数, 传递给后面的 map
函数运行使用:
(map (fn [x] (* x x)) (list 1 3 5 7 9)) ; (1 9 25 49 81)
最后运行的是 reduce
函数, 通过 +
这个函数将列表里所有的数字相加:
(reduce + (list 1 9 25 49 81)) ; 165
这里可以看到 list
可以表示列表的结构, 而 ->>
会管理后面几段代码的执行顺序.
这里是 ->>
是通过宏(Macro)来完成的, 宏的语法很有难度, 这里先跳过.
ClojureScript 有什么优势
Clojure 本身是一门 Lisp 方言, 突出了不可变数数据和惰性计算等等函数式编程的功能,
ClojureScript 是 Clojure 编译到 JavaScript 的版本, 用来开发网页或者 Node 应用.
跟 JavaScript 相比, Clojure 的设计更加仔细, 而且作为 Lisp 有着强大的表达能力,
同时, 对于不可变数据的思考也让 Clojure 对于并发计算和状态管理有好的改进.
Clojure 作者做过大学老师, 他给人演讲有一种充满智慧的感觉, 也是我信任 Clojure 的原因.
JavaScript 和 React 当中写网页的时候, 需要 JSX 和 immutable-js,
JSX 表示 Virtual DOM 的代码中间需要特殊处理 if
switch
等逻辑,
在 ClojureScript 当中 if
和 case
本身就是表达式, 不需要额外处理,
至于不可变数据, ClojureScript 默认的数据已经是 immutable data 了, 无需额外引入,
所以 ClojureScript 社区有很多人使用 React, 比如可以用 Reagent 来定义 React 组件:
(defn simple-component [] [:div [:p "I am a component!"] [:p.someclass "I have " [:strong "bold"] [:span {:style {:color "red"}} " and red "] "text."]])
当你熟练 ClojureScript 的时候, 你可以变得比 JavaScript 更加灵活和自如.
通过高阶函数和宏, 可以构造出非常精简的代码来完成同样的任务.
用什么软件执行和编译
ClojureScript 是运行在 JavaScript 环境当中的, 比如浏览器或者 Node.js ,
Lumo 是一个基于 V8 和 Node.js 的 ClojureScript 运行环境, 可以用 npm 安装:
npm install -g lumo-cljs
启动 Lumo 可以得到一个 REPL 环境, 跟 Node.js 的 REPL 很像:
$ lumo Lumo 1.8.0 ClojureScript 1.9.946 Node.js v9.2.0 Docs: (doc function-name-here) (find-doc "part-of-name-here") Source: (source function-name-here) Exit: Control+D or :cljs/quit or exit cljs.user=> (range 10) (0 1 2 3 4 5 6 7 8 9) cljs.user=> (filter odd? (list 0 1 2 3 4 5 6 7 8 9)) (1 3 5 7 9) cljs.user=>
另一个工具是 shadow-cljs, 更适合编译代码, 像 Webpack. 然后也用 npm 可以安装.
Lumo 适合用来运行 REPL 和代码片段, 而 shadow-cljs 适合做项目开发和编译.
注意对于 shadow-cljs, 你还是要在安装 Java 给它后台调用的.
这篇文章里默认操作系统是 macOS 或 Linux. 在 Windows 可能要注意其他问题.
用 ClojureScript 写脚本
ClojureScript 当中基础数据类型的跟 JavaScript 相似, 有字符串, 数字, 布尔值,
另外有个 Keyword(关键字)类型, 是一种简化的字符串, 常用在"键值对"的"键"使用.
做元编程时候还会遇到 Symbol(符号)类型, 不过现在还用不到, 不用管它.
对于长一点的, 建议把代码写在一个 app.cljs
文件里:
(println "Hello ClojureScript!")
然后通过 Lumo 执行这个文件:
$ lumo app.cljs Hello ClojureScript!
Lumo 是基于 Node.js 实现的, 所以你可以再里面使用 Node.js API.
不过要在 ClojureScript 里调用, 需要用一些特殊的语法,
比如 JavaScript 对象都需要用 js/console
这种加 js/
前缀的代码来写, 然后写成这样 :
(.log js/console "a message!") ; console.log("a message!") (.log js/console (js/require "path")) ; console.log(require"("path))
上面的代码会打印出数据. 要使用构造器或者调用方法需要一些其他的语法,
(println (new js/Date)) ; #inst "2018-04-15T08:58:44.338-00:00" (println (.now js/Date)) ; 1523782724340
引用 npm 模块可以借助 require
函数, 在 ClojureScript 里写成 js/require
:
(def fs (js/require "fs")) (println (.readdirSync fs "./")) ; #js ["app.cljs" "build.cljs" "out" "src"]
另一种写法是将引用的模块写在 ns
的定义当中, 然后通过 fs/readdirSync
这个写法调用:
(ns app ; 使用 :as 关键字时, "fs" 模块会被引入, 生成 `fs` 这个命名空间 (:require ["fs" :as fs])) ; 因为 `fs` 是命空间, 所以这个地方用 `fs/` 的写法了 (println (fs/readdirSync "./")) ; #js [app.cljs build.cljs out src]
对照上面调用 Node.js API 的方法, 读取文件也是非常容易的:
(ns app (:require ["fs" :as fs])) (println (fs/readFileSync "app.cljs"))
Clojure 当中提供了一些操作字符串的函数, 但是更多函数写在 clojure.string 这个命名空间之下:
(ns app (:require [clojure.string :as string])) (println (pr-str (str "12" "34"))) ; "1234" (println (pr-str (subs "123455" 2 3))) ; "3" (println (pr-str (string/split "12345" "3"))) ; ["12" "45"]
数据结构和抽象
Clojure 是一门函数式语言, 对于循环的设计有些特别, 需要写成尾递归的形式.
Clojure 需要借助 recur
这个关键字来控制尾递归, 比如这个函数打印 0 到 9 的数字:
(defn f1 [x] (if (< x 10) (do (println x) (recur (+ x 1))))) ; `recur` 会再调用 `f1`, 参数就是 `x+1` 了 (f1 0)
上面的尾递归可以用 loop
简写, 在 [x 0]
指定 x
的初始值是 0:
(loop [x 0] (if (< x 10) (do (println x) (recur (+ x 1)))))
这里的 loop
会先设置 n
是 0, 到了 (recur (inc n))
的地方这个 n
会加上 1.
这样就模拟了一个 while 循环的语法. Clojure 里要把变化的数据通过参数传递.
这是因为函数式编程当中比较排斥可变的数据, 所以用这种方式更严格地限制了数据的修改.
你也可以用 when
作为只执行一个分支的 if
的简写, 那样就不用 do
包裹多个表达式了:
(loop [x 0] (when (< x 10) (println x) (recur (+ x 1))))
Clojure 里常用的数据结构有:
- List(列表), 或者说成链表, 比如
'(1 2 3 4)
, 从头部操作, 但是随机后面的节点会很慢 - Vector(向量), 比如
[1 2 3 4]
, 这个就能很快得进行随机读写了, 不过适合从尾部读写 - HashMap(哈希表), 比如
{:a 1, :b 2}
跟 JavaScript 之类的语言不一样是, Clojure 里的数据是不可变的,
比如 conj
是个往向量的尾部添加数据的函数, 在 a
的基础上增加数据,
从这个例子你可以看到 a
在操作之后是不变的, 要从 b
才能拿到改变的数据:
cljs.user=> (def a [1 2 3 4]) #'cljs.user/a cljs.user=> (def b (conj a 6)) #'cljs.user/b cljs.user=> a [1 2 3 4] cljs.user=> b [1 2 3 4 6]
这个就是不可变数据不一样的地方了, 这个是函数式编程很需要的一个功能.
更多的操作数据的函数你可以在 http://cljs.info/cheatsheet/ 找到.
其他
Clojure 还有很多有意思的功能. 后面的文章会再讲, 感兴趣可以找我们问:
另外感谢一些帮我 review 过草稿的同学, 我后续还会找人添麻烦, 名字我匿了.