iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 14
0
Software Development

擁抱 Clojure系列 第 14

[第 14 天] 擁抱 Clojure:命名空間與專案(一)

命名空間與專案(一)

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

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

本篇文章將介紹組織程式碼的方法,包括以類似功能或屬性歸類的命名空間 (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。

(未完待續)


上一篇
[第 13 天] 擁抱 Clojure:流程控制(三)
下一篇
[第 15 天] 擁抱 Clojure:命名空間與專案(二)
系列文
擁抱 Clojure30

尚未有邦友留言

立即登入留言