又一份 ClojureScript 介紹

又一份 ClojureScript 介紹

ClojureScript 是什麼樣的

ClojureScript 是一門編譯到 JavaScript 的 Lisp 方言, 就像 CoffeeScript.
Clojure 是 Lisp 方言, 因此它的語法基於 S-Expression(S 表達式),
"S 表達式"大量使用圓括號好比 (f arg1 arg2) 來控制代碼的嵌套結構,
甚至於像是日常的 a + b + c 在 S 表達式當中也編程 (+ a b c).html

這是一種"前綴表達式"的寫法, 它很靈活, 能夠構造出很是靈活的代碼,
好比這樣一段代碼, 能夠完成 10 之內的奇數的平方求和:node

(->> (range 10)
     (filter odd?)
     (map (fn [x] (* x x)))
     (reduce +))

而後你能夠按照 Lumo, 保存上面的代碼到 app.cljs, 而後運行它:git

npm install -g lumo-cljs
lumo app.cljs

Clojure 爲了能更方便, 使用了方括號和花括號做爲特殊的語法.
上面的代碼當中有個 (fn [x] (* x x)), 其中函數參數就必需要 [x] 寫.github

這段代碼如何執行

這段代碼首先運行生成一個長度爲 10 的列表(List):npm

(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 函數運行使用:api

(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)來完成的, 宏的語法頗有難度, 這裏先跳過.bash

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]

對照上面調用 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 過草稿的同窗, 我後續還會找人添麻煩, 名字我匿了.
相關文章
相關標籤/搜索