Clojure

REPL でのプログラミング: 名前空間のナビゲート

これまでのところ、私たちは小さな自己完結型のエクスペリメントにのみ REPL を使用してきました。しかし、REPL は、開発中またはデバッグ中のプログラムの「視点に立つ」こと、つまり、プログラムを実行するときにプログラムが実行するのとまったく同じ式を REPL で評価することに最も役立ちます。

これは、REPL に実行中のプログラムと同じコンテキストを与えることで実現され、つまりコードが定義されているのと同じ名前空間で REPL を使用することになります。それを行う方法は、以下のセクションで説明します。

注: 名前空間は Clojure の最も難しい部分の 1 つです。言語を勉強しているだけの場合は、この章を今のところ飛ばしてください。実際の Clojure プロジェクトに取りかかり始めたら、戻ることができます。

現在の名前空間

REPL でコードを評価すると、常にそのコードを現在の名前空間のコンテキストで評価しています。

現在の名前空間は、次のようなことを決定します。

  • 書いているコードが他の名前空間のコードを参照する方法。

たとえば、現在の名前空間がmyapp.foo.bar(require [clojure.set :as cset :refer [union]])を評価すると、clojure.set/union Var をcset/union:as csetエイリアスのために)またはunion:refer [union]のために)のいずれかで参照できるようになります。

$ 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.barmy-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)を評価することで既存のネームスペースに切り替えることができます。以下は、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-nsは新しいネームスペースを作成してnsがそのネームスペースに切り替えるのと同じように切り替えますが、nsよりも少しだけ処理を少なく済ませます。これは、nsdefnなどのclojure.coreで定義されたすべての名前に自動的にアクセスできるようにするのに対し、in-nsはそれを行わないためです。(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をすでに作成済のネームスペースに切り替えるためにのみ使用する場合は、このような微妙な違いに頭を悩ませる必要はありません

libを使用する

REPLで移動するネームスペースのほとんどは、プロジェクトのソースファイルまたは依存関係、つまりプロジェクトのlibにすでに存在しています

libで定義されたネームスペースに切り替える際には、注意すべき重要な使用方法の注意事項があります

ネームスペースがプロジェクトのlibで定義されている場合は、切り替える前にREPLでlibがロードされていることを必ず確認してください

libがロードされていることを確認する方法

ネームスペースmylib.ns1を含むlibが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タグを使用して、ロードされるlibの内容(およびその他)を確認できます

$ 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

問題が発生する可能性のある方法

上記の例プロジェクトを続けます。以下は、最初にlibをロードせずにlibネームスペースに切り替えると問題が発生する可能性を示す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