Clojure China

又一份 ClojureScript 介绍

#1

我的编辑器在 http://repo.topix.im/tabletwo/ 没有历史版本, 别给我改坏了…


又一份 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 当中 ifcase 本身就是表达式, 不需要额外处理,
至于不可变数据, 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]

这里的 loop 会先设置 n 是 0, 到了 (recur (inc n)) 的地方这个 n 会加上 1.
这样就模拟了一个 while 循环的语法. Clojure 里要把变化的数据通过参数传递.
这是因为函数式编程当中比较排斥可变的数据, 所以用这种方式更严格地限制了数据的修改.

对照上面调用 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)))))

你也可以用 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 还有很多有意思的功能. 后面的文章会再讲, 感兴趣可以找我们问:

2赞
#2

很详细的介绍!
刚入门Clojure的朋友可以看着中文教程一边操作哈:Clojure中文教程超实用