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,则您将能够从其他命名空间中将其引用为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)来切换到现有的命名空间。这是一个创建命名空间myapp.some-ns、在其中定义名为x的Var、返回到user命名空间,然后再次移动到myapp.some-ns的REPL会话示例。

$ 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-nsns一样创建新的命名空间并切换到它,但它比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))))

以下3种方法可以确保myproject.person-names已加载。

$ 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