Skip to content

Latest commit

 

History

History
349 lines (246 loc) · 14.2 KB

05.md

File metadata and controls

349 lines (246 loc) · 14.2 KB

命名空間與專案

我心裡一直都在暗暗設想,天堂應該是圖書館的模樣。

— 波赫士《關於天賜的詩》

本篇文章將介紹組織程式碼的方法,包括以類似功能或屬性歸類的命名空間 (Namespace),和組織程式碼檔案的專案結構,還有如何使用其他的第三方函式庫,以及用來輸入程式碼的編輯器。

在開始之前,如果你正在使用 REPL,請按下 Ctrl-D 終止它,並輸入 lein repl 重啓新的 REPL。

命名空間

你會將同樣功能或用途的東西放在一起,例如筆、維修工具或是車子。在 Clojure 中,使用命名空間 (Namespace) 將類似的程式碼歸類組織起來,你可以依據功能、用途、階層或是你的心情將程式碼歸類。

Clojure 會將目前的命名空間資訊,儲存在名爲 *ns* 的全域命名空間物件之中,它的內部型態爲 clojure.lang.Namespace。如果想知道目前的命名空間,可以使用 ns-name 套用在 *ns* 物件上。

(class *ns*)
;; => clojure.lang.Namespace
(ns-name *ns*)
;; => user

因爲在 REPL 中,預設的命名空間就是 user,所以使用 ns-name 函式便返回 user。值得注意的是,在使用習慣上命名全域物件會在名稱兩側加上星號 (*)。

創建

in-ns

除了開啓 REPL 時自動建立的命名空間之外,也可以依據自己的需要創建命名空間。使用 in-ns 函式會嘗試切換到以參數符號爲名的命名空間,如果該命名空間不存在,則建立之,創建成功後便切換到該命名空間:

user=> (ns-name *ns*)
;; => user
user=> (ns-name (in-ns 'foo))
;; => foo
foo=> (def x "bar")
;; => #'foo/x

以上範例特別將提示符號前面的命名空間寫出來,說明命名空間經由函式建立並成功地切換到 foo 之中。在繼續往下之前,請先將命名空間切換回 user,因爲 Clojure 核心函式只在該命名空間有載入:

foo=> (in-ns 'user)
;; => #namespace[user]
user=> 

create-ns

如果只想建立命名空間,可以使用 create-ns 建立之,若該命名空間已經存在則不做任何動作。創建之後,可以利用 in-ns 來切換至新建的命名空間:

user=> (create-ns 'inception)
;; => #namespace[inception]
user=> (in-ns 'inception)
;; => #namespace[inception]
inception=>

繼續往下之前,請按下 Ctrl-D 終止 REPL 之後,再輸入 lein repl 重啓新的 REPL。

引用

切換至新的命名空間之後,所有以 def 建立的符號、Vars 物件、函式都會歸屬於新的命名空間。因此當你在某個命名空間建立了事物,若在另一個命名空間裡想要取用,卻沒有明確指定命名空間,就會發生錯誤:

user=> (def cobb "Leonardo DiCaprio")
user=> (in-ns 'inception)
inception=> cobb
;; => Exception: Unable to resolve symbol: cobb in this context

若想要引用其他命名空間的物件,可以使用命名空間加上符號的方式,取用需要的物件。寫法爲先寫上命名空間,再加上斜線 (/),之後放上符號名稱即可:

inception=> user/cobb
;; => "Leonardo DiCaprio"

refer

如果不想使用全名方式的引用,可以使用 refer 函式將其它命名空間中所有公開的 Vars 物件,在目前的命名空間中建立對應,以後便不需要再明確指定命名空間:

inception=> (clojure.core/refer 'user)
inception=> cobb
;; => "Leonardo DiCaprio"

由於在新的命名空間中,REPL 並不會載入核心函式所在的 clojure.core 命名空間,所以使用 refer 函式必須以全名方式使用。

refer 函式提供了三個修飾子,分別是 :exclude:only:rename,用來指定哪些 Vars 物件不在此命名空間中建立對應,或只取用哪些 Vars 物件、以及將 Vars 物件在目前命名空間中建立不同名稱的對應:

(clojure.core/refer 'clojure.core
  :exclude '(+ - * /)
  :rename '{str fmt})
(+ 1 2)
;; => Unable to resolve symbol: + in this context
(fmt "Wake up, " "Cobb")
=> "Wake up, Cobb"

以上範例在目前的命名空間中,建立了 clojure.core 命名空間中的 Vars 物件對應,但是並不包含四則運算符號,並將 str 符號重新命名爲 fmt

繼續往下之前,請按下 Ctrl-D 終止 REPL 之後,再輸入 lein repl 重啓新的 REPL。

require

require 會負責將命名空間與相關資源載入,並編譯命名空間下的程式碼,但是不在目前的命名空間建立新的 Vars 物件對應。因此載入命名空間後,仍然必須寫明命名空間才可取用:

(require 'clojure.string)
(clojure.string/join ", " ["Cobb" "Arthur" "Ariandne" "Eames"])
;; => "Cobb, Arthur, Ariandne, Eames"

require 提供了修飾子 :as,讓你將載入的命名空間以自己的需要重新命名:

(require '[clojure.string :as str])
(str/capitalize "mal")
;; => "Mal"

若是打算一次載入多個命名空間,可以使用如下寫法:

(require 'clojure.string 'clojure.test)

或是這樣寫:

(require '(clojure string test))

以上範例載入了 clojure.string 以及 clojure.test

use

userequire 類似,但是 use 載入欲使用的命名空間後,會呼叫 refer 在目前的命名空間建立對應,因此不需要使用全名。由於內部使用了 refer 函式,因此 refer 函式的修飾子也可以在 use 使用:

(use '[clojure.string :only [split]])
(split "Cobb, Arthur, Ariandne, Eames" #", ")
;; => ["Cobb" "Arthur" "Ariandne" "Eames"]

以上範例展示了使用 clojure.string 中的 split 函式,以字串 ", " 作爲分隔,將字串切割成四塊小字串。

import

除了以 Clojure 寫成的程式碼,還可以使用 import 來載入 Java 套件 (Package) 類別。使用 import 載入套件中的類別之後,使用類別就不需要再寫上套件全名:

(java.util.Date.)
;; => #inst "2017-12-25T07:05:53.372-00:00"
(import java.util.Date)
(Date.)
;; => #inst "2017-12-25T07:06:19.038-00:00"

在類別後加入點符號 (.) 是 Clojure 提供的簡化方法,用來簡化 new 函式創建類別,以上的範例等同如下:

(new Date)
;; => #inst "2017-12-25T07:09:13.378-00:00"

繼續往下之前,請按下 Ctrl-D 終止 REPL 之後,再輸入 lein repl 重啓新的 REPL。

保護資訊

以上的函式都會引用到命名空間中的公開資訊,如果有些資訊想要隱藏不被使用,可以在使用 def 設立 Vars 物件時加上 private 詮釋資料 (Metadata):

user=> (def pub "It's public")
user=> (def ^:private priv "It's private")
user=> (in-ns 'foo)
foo=> (clojure.core/refer 'user)
foo=> pub
;; => "It's public"
foo=> priv
;; => Unable to resolve symbol: priv in this context

上面的範例雖然使用了 user 命名空間的 priv 物件,卻因爲在定義時宣告私有,因此無法正常取用。使用插入符號 (^) 會將詮釋資料添加至 Vars 物件。讓 Clojure 讀取器 (Reader) 採用不同處理方式的字元,被稱爲讀取器巨集 (Reader Macro)。

Clojure 提供了更簡便的方式定義私有函式,便是使用 defn- 定義函式。繼續以下範例之前,請先按下 Ctrl-D 終止 REPL,再輸入 lein repl 開啓新的 REPL:

user=> (defn- greeting [name] (str "Hello, " name))
user=> (in-ns 'bar)
bar=> (clojure.core/refer 'user)
bar=> (greeting "Catherine")
;; => Unable to resolve symbol: greeting in this context

ns 巨集

在實際的專案中,其實並不常使用 referrequire 以及 use,Clojure 提供了 ns 巨集,既具備了載入其他命名空間的功能,還可以建立新的命名空間:

(ns examples.ns
  (:use clojure.test)
  (:require [clojure.zip :as zip])
  (:import java.util.Date))

ns 巨集會試着建立第一個參數名稱指定的命名空間,並切換到該命名空間,之後的修飾子分別對應了 userequireimport 等功能。

實際專案中,檔案會在一開始使用 ns 巨集以建立該檔案隸屬的命名空間,並寫上欲載入的其他命名空間。

專案

專案結構

進入這個小節之前,請先把 REPL 終止 (按下 Ctrl-D),並在命令列下切換到你擺放 Clojure 專案的目錄下,如果沒有,在家目錄下建立 Projects 是個不錯的主意。

假設現在的新專案是爲漢堡店建立網站,首先切換到家目錄的 Projects 目錄下,使用 Leiningen 建立名爲 burger-shop 的專案:

$ cd ~/Projects
$ lein new app burger-shop

Leiningen 建立的專案 burger-shop 會長得像這樣:

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
├── resources
├── src
│   └── burger_shop
│       └── core.clj
└── test
    └── burger_shop
        └── core_test.clj

project.clj 爲專案的配置描述文件,記載了專案的名稱、授權、使用到的套件以及編譯選項;resources 目錄則用來擺放程式會使用到的資源檔案;LICENSEREADME.md 則分別是此專案的授權聲明,以及 Markdown 格式的說明檔。

應用程式的原始碼被擺放在 src 目錄下,test 目錄下則放了用來測試應用程式的測試程式。Clojure 遵照 Java 對於套件的目錄命名規則,即是 x.y.z 套件將放在 x/y/z 的目錄結構中。

Leiningen 爲新專案建立了 burger_shop 這個命名空間,並依照規則創建目錄結構。使用你的編輯器,將 src/burger_shop/core.clj 檔案打開,它應該像下面這樣:

(ns burger-shop.core
  (:gen-class))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!"))

Leiningen 爲這個檔案建立了 burger-shop.core 命名空間,並擺放在 burger_shop 目錄中的 core.clj 檔案,Clojure 程式檔案以 clj 爲副檔名。由於 Java 目錄命名不可有橫線符號 (-),因此使用底線符號 (_) 取代之。

使用第三方函式庫

除了自己寫的程式之外,實際專案還會使用別人已經開發好的函式庫,想要使用第三方函式庫,需要先以編輯器打開專案目錄下的 project.clj

(defproject burger-shop "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]]
  :main ^:skip-aot burger-shop.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

若是想要使用 cheshire 函式庫,以獲得解析 json 的功能,可以在 Clojars 找到的頁面中看到資訊:

Leiningen/Boot
[cheshire "5.8.0"]

將中括號內的文字並包含中括號,寫上 :dependencies 所在的那一行並存檔:

(defproject burger-shop "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [cheshire "5.8.0"]]
  :main ^:skip-aot burger-shop.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

當我們運行或編譯專案時,便會下載 cheshire 第三方函式庫:

$ lein run
Retrieving cheshire/cheshire/5.8.0/cheshire-5.8.0.pom from clojars
...
Hello, World!

以上範例的最後一行即是專案執行的結果。

編輯器

俗話說:工欲善其事,必先利其器。好的編輯器能夠讓你更輕鬆地輸入程式、容易地測試程式,或是提供有用的資訊修正錯誤。以下介紹開發 Clojure 時,較常爲人使用的編輯器。

Light Table

使用 ClojureScript (一種寄宿在 JavaScript 的 Clojure 語言) 開發的 Light Table,曾在衆籌平台 Kickstarter 募資成功。以即時回饋爲訴求,使用者輸入運算式後,可以快速地看到程式求值的結果。

Nightcode

爲 Clojure 與 ClojureScript 開發的 Nightcode,內建了 Leiningen 與 Boot,整合性的開發環境對初學者非常友好。

Eclipse

Eclipse 作爲 Java 界知名的免費整合開發工具,除了用來開發 Java 程式語言之外,透過內建的擴充系統與豐富的外掛模組,也可以撰寫 Clojure 程式。目前與 Counterclockwise 套件搭配使用,提供便利的 Clojure 開發環境。

IntelliJ IDEA

由來自捷克的軟體開發公司 JetBrains 開發的 IntelliJ IDEA ,也是 Java 界知名的整合開發環境。建議使用 Cursive 套件,它提供了智慧括號輸入,以及 REPL 整合等相關功能。

Vim

Vim 作爲一個歷久彌新的編輯器,安裝 Fireplace 便可以使用智慧輸入與 REPL 整合等功能。

Emacs

除了作爲歷久彌新的編輯器,Emacs 還建立了以 LISP 爲操作語言的環境,有志者可以透過 Emacs Lisp 語言開發自己需要的功能,強大又富有彈性。經由套件 CIDER 的協助之下,Emacs 將變成強大的 Clojure 程式開發環境。

以上編輯器根據使用難易度,由簡單到困難編排而成,讀者可以根據自己的需要選擇適合的編輯器。

回顧

經由本篇文章你學到了自行創建命名空間的方法,還知道了如何載入其它的命名空間;並且了解一般專案的目錄結構,和使用第三方函式庫的方法。除此之外,還知道了哪些編輯器可以更快速方便的開發 Clojure 程式。

還不賴吧?今天就先到這裡,下一篇文章再見囉!