Clojure

REPL 中的程式設計:導覽命名空間

到目前為止,我們只將 REPL 用於小型獨立實驗;但 REPL 最有價值的地方在於設身處地於您正在開發或除錯的程式,亦即在 REPL 中評估與程式在執行時完全相同的表達式。

這可透過讓 REPL 與執行中的程式擁有相同的內容來達成,這表示在定義程式碼的相同 命名空間 中使用 REPL。我們將在以下各節中說明如何執行此操作。

注意:命名空間是 Clojure 中最棘手的部分之一。如果您只是在學習這門語言,可以先跳過本章;當您開始處理「真實世界」的 Clojure 專案時,可以再回來閱讀。

目前的命名空間

當您在 REPL 中評估程式碼時,您總是會在目前命名空間的內容中評估程式碼。

目前的命名空間決定

  • 您正在撰寫的程式碼如何參照其他命名空間中的程式碼。

例如,如果目前的命名空間是 myapp.foo.bar,而您評估 (require [clojure.set :as cset :refer [union]]),您現在可以透過 cset/union(因為 :as cset 別名)或僅 union(因為 :refer [union])來參照 clojure.set/union Var。

$ clj
Clojure 1.10.0
user=> *ns*
#object[clojure.lang.Namespace 0x7d1cfb8b "user"]
user=> (ns myapp.foo.bar) ;; creating and switching to the myapp.foo.bar namespace - `ns` will be explained later in this guide.
nil
myapp.foo.bar=> (require '[clojure.set :as cset :refer [union]]) ;; this will only affect the current namespace
nil
myapp.foo.bar=> (cset/union #{1 2} #{2 3})
#{1 3 2}
myapp.foo.bar=> (union #{1 2} #{2 3})
#{1 3 2}
myapp.foo.bar=> (cset/intersection #{1 2} #{2 3})
#{2}
myapp.foo.bar=> (in-ns 'user) ;; now switching back to the `user` namespace - `in-ns` will be explained later in this guide.
#object[clojure.lang.Namespace 0x7d1cfb8b "user"]
user=> (union #{1 2} #{2 3})  ;; won't work, because `union` has not been :refer'ed in the `user` namespace
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: union in this context
user=> (cset/intersection #{1 2} #{2 3}) ;; won't work, because the `cset` alias has not been defined in the current namespace.
Syntax error compiling at (REPL:1:1).
No such namespace: cset
user=>

提示:您可以透過呼叫 ns-aliases 來找出在特定命名空間中定義了哪些別名。

myapp.foo.bar=> (ns-aliases 'myapp.foo.bar)
{cset #object[clojure.lang.Namespace 0x4b2a01d4 "clojure.set"]}
  • 您定義的 Var(例如使用 (def …​)(defn …​))將存在於哪個命名空間中。

例如,如果目前的命名空間是 myapp.foo.bar,而您定義一個名為 my-favorite-number 的 Var,您將能夠從其他命名空間將該 Var 參照為 myapp.foo.bar/my-favorite-number

$ clj
Clojure 1.10.0
user=> (ns myapp.foo.bar) ;; creating and switching to the `myapp.foo.bar` namespace - NOTE `ns` will be explained later in this guide
nil
myapp.foo.bar=> (def my-favorite-number 42) ;; defining a Var named `my-favorite-number`
#'myapp.foo.bar/my-favorite-number
myapp.foo.bar=> my-favorite-number
42
myapp.foo.bar=> (ns myapp.baz) ;; creating and switching to another namespace `myapp.baz`
nil
myapp.baz=> myapp.foo.bar/my-favorite-number ;; referring to `my-favorite-number`
42
myapp.baz=> (require '[myapp.foo.bar :as foobar]) ;; we can also use an alias to make it shorter
nil
myapp.baz=> foobar/my-favorite-number
42

您可以透過評估 *ns* 來找出目前的命名空間。

$ clj
Clojure 1.10.0
user=> *ns*
#object[clojure.lang.Namespace 0x7d1cfb8b "user"]

正如您所見,預設情況下,當您使用 clj 啟動 REPL 時,目前的命名空間是 user

使用 ns 建立命名空間

您可以透過評估 (ns MY-NAMESPACE-NAME) 來建立並切換到新的命名空間。

$ clj
Clojure 1.10.0
user=> (ns myapp.foo-bar)
nil
myapp.foo-bar=> *ns*
#object[clojure.lang.Namespace 0xacdb094 "myapp.foo-bar"]
myapp.foo-bar=> (def x 42)
#'myapp.foo-bar/x

注意:當您切換到新的命名空間時,先前命名空間中定義的名稱和別名將不再可用。

$ clj
Clojure 1.10.0
user=> (ns myapp.ns1) ;; creating a new namespace and defining a Var `x` and an alias `str/`:
nil
myapp.ns1=> (def x 42)
#'myapp.ns1/x
myapp.ns1=> x
42
myapp.ns1=> (require '[clojure.string :as str])
nil
myapp.ns1=> (str/upper-case "hello")
"HELLO"
myapp.ns1=> (ns myapp.ns2) ;; now switching to another namespace:
nil
myapp.ns2=> x ;; won't work, because x has not been defined in namespace `myapp.ns2`
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: x in this context
myapp.ns2=> (str/upper-case "hello") ;; won't work, because alias `str` has not been defined in namespace `myapp.ns2`
Syntax error compiling at (REPL:1:1).
No such namespace: str

使用 in-ns 切換到現有命名空間

您可以透過評估 (in-ns 'MY-NAMESPACE-NAME) 來切換到現有命名空間。以下是一個 REPL 會話範例,它建立一個命名空間 myapp.some-ns,在其中定義一個名為 x 的 Var,再移回 user 命名空間,然後再移到 myapp.some-ns

$ clj
Clojure 1.10.0
user=> (ns myapp.some-ns) ;;;; creating the namespace `myapp.some-ns`
nil
myapp.some-ns=> *ns* ;; where are we?
#object[clojure.lang.Namespace 0xacdb094 "myapp.some-ns"]
myapp.some-ns=> (def x 42) ;; defining `x`
#'myapp.some-ns/x
myapp.some-ns=> (in-ns 'user) ;;;; switching back to `user`
#object[clojure.lang.Namespace 0x4b45dcb8 "user"]
user=> *ns* ;; where are we?
#object[clojure.lang.Namespace 0x4b45dcb8 "user"]
user=> (in-ns 'myapp.some-ns) ;;;; ...switching back again to `myapp.some-ns`
#object[clojure.lang.Namespace 0xacdb094 "myapp.some-ns"]
myapp.some-ns=> *ns* ;; where are we?
#object[clojure.lang.Namespace 0xacdb094 "myapp.some-ns"]
myapp.some-ns=> x ;; `x` is still here!
42

如果您對從未建立過的命名空間使用 in-ns 會怎樣?您會看到奇怪的事情發生。例如,您將無法使用 defn 定義函式

$ clj
Clojure 1.10.0
user=> (in-ns 'myapp.never-created)
#object[clojure.lang.Namespace 0x22356acd "myapp.never-created"]
myapp.never-created=> (defn say-hello [x] (println "Hello, " x "!"))
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: defn in this context

說明:在這種情況下,in-ns 會建立新的命名空間並切換到該命名空間,就像 ns 所做的一樣,但它的工作量比 ns 少一點,因為它不會自動提供 clojure.core 中定義的所有名稱,例如 defn。您可以透過評估 (clojure.core/refer-clojure) 來修正這個問題。

myapp.never-created=> (clojure.core/refer-clojure)
nil
myapp.never-created=> (defn say-hello [x] (println "Hello, " x "!"))
#'myapp.never-created/say-hello
myapp.never-created=> (say-hello "Jane")
Hello,  Jane !
nil

如果您只使用 in-ns 切換到已經建立的命名空間,您就不必處理這些細微差別。

使用函式庫

您在 REPL 中瀏覽的大部分命名空間都已經存在於專案的原始檔或相依關係中,也就是專案的 函式庫 中。

對於切換到函式庫中定義的命名空間,有一個重要的使用注意事項

如果命名空間定義在專案的 函式庫 中,請務必確保您在切換到該命名空間之前已在 REPL 中載入該函式庫。

如何確保函式庫已載入

若要確保具有命名空間 mylib.ns1 的函式庫已在 REPL 中載入,您可以執行下列任一動作

  1. 直接require 它:(require '[mylib.ns1])

  2. 載入本身需要 mylib.ns1(直接或間接)的命名空間。

  3. 手動評估原始檔 mylib.ns1 中的所有程式碼

範例:用於問候人們的專案

例如,假設一個 Clojure 專案具有下列結構和內容

.
└── src
    └── myproject
        ├── person_names.clj
        └── welcome.clj
;; -----------------------------------------------
;; src/myproject/welcome.clj
(ns myproject.welcome
  (:require [myproject.person-names :as pnames])) ;; NOTE: `myproject.welcome` requires `myproject.person-names`

(defn greet
  [first-name last-name]
  (str "Hello, " (pnames/familiar-name first-name last-name)))


;; -----------------------------------------------
;; src/myproject/person_names.clj
(ns myproject.person-names
  (:require [clojure.string :as str]))

(def nicknames
  {"Robert"     "Bob"
   "Abigail"    "Abbie"
   "William"    "Bill"
   "Jacqueline" "Jackie"})

(defn familiar-name
  "What to call someone you may be familiar with."
  [first-name last-name]
  (let [fname (str/capitalize first-name)
        lname (str/capitalize last-name)]
    (or
      (get nicknames fname)
      (str fname " " lname))))

以下是確保載入 myproject.person-names 的 3 種方法

$ clj ## APPROACH 1: requiring myproject.person-names directly
Clojure 1.10.0
user=> (require '[myproject.person-names])
nil
user=> myproject.person-names/nicknames ;; checking that the myproject.person-names was loaded.
{"Robert" "Bob", "Abigail" "Abbie", "William" "Bill", "Jacqueline" "Jackie"}
$ clj ## APPROACH 2: requiring myproject.welcome, which itself requires myproject.person-names
Clojure 1.10.0
user=> (require '[myproject.welcome])
nil
user=> myproject.person-names/nicknames ;; checking that the myproject.person-names was loaded.
{"Robert" "Bob", "Abigail" "Abbie", "William" "Bill", "Jacqueline" "Jackie"}
$ clj ## APPROACH 3: manually copying the code of myproject.person-names in the REPL.
Clojure 1.10.0
(ns myproject.person-names
  (:require [clojure.string :as str]))

(def nicknames
  {"Robert"     "Bob"
   "Abigail"    "Abbie"
   "William"    "Bill"
   "Jacqueline" "Jackie"})

(defn familiar-name
  "What to call someone you may be familiar with."
  [first-name last-name]
  (let [fname (str/capitalize first-name)
        lname (str/capitalize last-name)]
    (or
      (get nicknames fname)
      (str fname " " lname))))
nil
myproject.person-names=> myproject.person-names=> #'myproject.person-names/nicknames
myproject.person-names=> myproject.person-names=> #'myproject.person-names/familiar-name
myproject.person-names=> myproject.person-names/nicknames ;; checking that the myproject.person-names was loaded.
{"Robert" "Bob", "Abigail" "Abbie", "William" "Bill", "Jacqueline" "Jackie"}

提示:您可以使用 require 中的 :verbose 標籤查看(除其他事項外)載入哪些函式庫。

$ clj
Clojure 1.10.0
user=> (require '[myproject.welcome] :verbose)
(clojure.core/load "/myproject/welcome")
(clojure.core/in-ns 'clojure.core.specs.alpha)
(clojure.core/alias 's 'clojure.spec.alpha)
(clojure.core/load "/myproject/person_names")
(clojure.core/in-ns 'myproject.person-names)
(clojure.core/alias 'str 'clojure.string)
(clojure.core/in-ns 'myproject.welcome)
(clojure.core/alias 'pnames 'myproject.person-names)
nil

事情可能如何出錯

繼續上述範例專案,以下是 REPL 會話,顯示如果您在未先載入函式庫的情況下切換到函式庫命名空間,事情可能會如何出錯

$ clj
Clojure 1.10.0
user=> (ns myproject.person-names)
nil
myproject.person-names=> nicknames ;; #'nicknames won't be defined, because the lib has not been loaded.
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: nicknames in this context
myproject.person-names=> (require '[myproject.person-names]) ;; won't fix the situation, because the namespaces has already been created
nil
myproject.person-names=> nicknames
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: nicknames in this context