Clojure命名空间

柴华灿
2023-12-01

本文翻译自Clojure Namespaces and Vars 本文涵盖如下内容: + Clojure命名空间和var概述 + 如何定义命名空间 + 如何使用其它命名空间里的函数 + require,refer和use + 常见错误和典型错误,以及导致这些错误的原因 + 命名空间和代码管理

版权:

This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available on Github.

涵盖Clojure版本:Clojure 1.5

概述

Clojure的函数通过命名空间来组织.Clojure命名空间和Java的包或者Python的模块很类似.命名空间实际上就是个map,将名字映射到了var上.在大部分情况下,这些var持有这些函数.

定义一个命名空间

一般情况下使用clojure.core/ns宏来定义命名空间.最基本的形式下,它将名字作为符号.


          (ns superlib.core)

命名空间可以由点号切割的好多段组成


          (ns megacorp.service.core)

需要注意的是,请尽量避免使用单段的命名空间,以免与其它开发人员的命名空间相冲突.如果库或者应用属于某个组织,那么建议以如下形式作为命名空间.[组织名称].[包名|应用名].[函数组名] 例如


          (ns clojurewerkz.welle.kv)
          
          (ns megacorp.search.indexer.core)

另外,ns宏可以包含如下形式: + (:require ...) + (:import ...) + (:use ...) + (:refer-clojure ...) + (:gen-class ...)

这些其实就是clojure.core/import,clojure.core/require等等这些的简写形式而已

:require

:require形式可以使你的代码能访问其它命名空间的Clojure代码.例如


          (ns megacorp.profitd.scheduling
            (:require clojure.set))
          
          ;; Now it is possible to do:
          ;; (clojure.set/difference #{1 2 3} #{3 4 5})

此代码将保证clojure.set命名空间被加载,编译并且可以通过clojure.set名来调用.当然可以给加载的命名空间取个别名:


          (ns megacorp.profitd.scheduling
            (:require [clojure.set :as cs]))
          
          ;; Now it is possible to do:
          ;; (cs/difference #{1 2 3} #{3 4 5})

一次导入两个命名空间的例子;


          (ns megacorp.profitd.scheduling
            (:require [clojure.set  :as cs]
                      [clojure.walk :as walk]))

:refer选项

如果想在当前命名空间里通过简写名称来引用clojure.set空间里的函数,可以通过refer来实现:


          (ns megacorp.profitd.scheduling
            (:require [clojure.set :refer [difference intersection]]))
          
          ;; Now it is possible to do:
          ;; (difference #{1 2 3} #{3 4 5})

:require形式中的:refer特性为Clojure1.4新增特性.

可能有时需要引入某个命名空间下所有的函数:


          (ns megacorp.profitd.scheduling
            (:require [clojure.set :refer :all]))
          
          ;; Now it is possible to do:
          ;; (difference #{1 2 3} #{3 4 5})

:import

:import的作用是在当前命名空间引入Java类:


          (ns megacorp.profitd.scheduling
            (:import java.util.concurrent.Executors))

执行上面的代码后,java.util.concurrent.Executors类将会被引入,请可以直接通过名字Executors来使用.可以同时引入多个类.


          (ns megacorp.profitd.scheduling
            (:import java.util.concurrent.Executors
                     java.util.concurrent.TimeUnit
                     java.util.Date))

如果引入的多个类在同一个包下面,就像上面那样,可以使用如下的简介方式:


          (ns megacorp.profitd.scheduling
            (:import [java.util.concurrent Executors TimeUnit]
                     java.util.Date))

虽然导入的list被叫做list,实际上可以使用任意的Clojure的集合(一般使用vector)

当前命名空间

Clojure将通过*ns*来持有当前的命名空间.使用def形式定义的var被添加到了当前命名空间中.

:refer-clojure

我们在使用像clojure.core/get这样的函数和clojure.core/defn这样的宏的时候我们不需要使用它的全限定名.这是因为Clojure默认将clojure.core下的内容全部加载进了当前命名空间里了.所以如果你定义了一个函数名和clojure.core里的重复了(比如find),你将会得到一个警告.

WARNING: find already refers to: #'clojure.core/find in namespace:    megacorp.profitd.scheduling, being replaced by: #'megacorp.profitd.scheduling/find

这个警告的意思是在megacorp.profitd.scheduling这个命名空间里,已经有一个clojure.core/find了,但是现在它被你定义的函数覆盖了.请记住,Clojure是很动态的语言,命名空间就是map而已.

解决这个问题的办法有:你可以重命名你的函数或者不引入clojure.core里的这个函数


          (ns megacorp.profitd.scheduling
            (:refer-clojure :exclude [find]))
          
          (defn find
            "Finds a needle in the haystack."
            [^String haystack]
            (comment ...))

在这里,如果你想使用clojure.core/find的话,你需要通过全限定名来使用:


          (ns megacorp.profitd.scheduling
            (:refer-clojure :exclude [find]))
          
          (defn find
            "Finds a needle in the haystack."
            [^String haystack]
            (clojure.core/find haystack :needle))

:use

Clojure在1.4之前,:require是不支持:refer的,只能使用:use


          (ns megacorp.profitd.scheduling-test
            (:use clojure.test))

在上面的例子中,clojure.test里的所有内容都被引入到了当前命名空间中.但是一般不会这样使用,建议是只引入需要的函数:


          (ns megacorp.profitd.scheduling-test
            (:use clojure.test :only [deftest testing is]))

1.4以前的做法


          (ns megacorp.profitd.scheduling-test
            (:require clojure.test :refer [deftest testing is]))

而现在鼓励的做法是使用:require,通过:refer来进行限制.

文档与元数据

命名空间可以包含说明文档.你可以在ns宏里添加:


          (ns superlib.core
            "Core functionality of Superlib.
          
             Other parts of Superlib depend on functions and macros in this namespace."
            (:require [clojure.set :refer [union difference]]))

或者元数据


          (ns ^{:doc "Core functionality of Superlib.
                      Other parts of Superlib depend on functions and macros in this namespace."
                :author "Joe Smith"}
             superlib.core
            (:require [clojure.set :refer [union difference]]))

元数据可以包含任意的键,例如:author,很多工具可以使用(像Codox,Cadastre或者lein-clojuredocs)

如何在REPL里使用其它命名空间的函数

ns宏是你经常需要使用的,它引入其它命名空间的函数.但是它在REPL里不太方便.这里可以直接使用require:


          ;; Will be available as clojure.set, e.g. clojure.set/difference.
          (require 'clojure.set)
          
          ;; Will be available as io, e.g. io/resource.
          (require '[clojure.java.io :as io])
          It takes a quoted libspec. The libspec is either a namespace name or a collection (typically a vector) of [name :as alias] or [name :refer [fns]]:
          
          (require '[clojure.set :refer [difference]])
          
          (difference #{1 2 3} #{3 4 5 6})  ; ⇒ #{1 2}

:as和:refer可以一起使用


          (require '[clojure.set :as cs :refer [difference]])
          
          (difference #{1 2 3} #{3 4 5 6})  ; ⇒ #{1 2}
          (cs/union #{1 2 3} #{3 4 5 6})    ; ⇒ #{1 2 3 4 5 6}

clojure.core/use可以做和clojure.core/require一样的事情,但是不推荐使用了.

命名空间和编译

Clojure是一个需要编译的语言:代码在被加载的时候进行编译.

命名空间可以包含var或者去继承协议,添加多重方法实现或载入其它库.所以为了完成编译,你需要引入需要的命名空间.

私有Vars

Vars(包括defn宏定义的函数)可以设为私有的.有两种方法可以来做这件事情:使用元数据或者defn-宏


          (ns megacorp.superlib)
          
          ;;
          ;; Implementation
          ;;
          
          (def ^{:private true}
            source-name "supersource")
          
          (defn- data-stream
            [source]
            (comment ...))

常量Vars

Vars可以设为常量,通过:const元数据来设置.这将会促使Clojure编译器将其编译为常量:


          (ns megacorp.epicgame)
          
          ;;
          ;; Implementation
          ;;
          
          (def ^{:const true}
            default-score 100)

如何通过名称来查找和执行函数

可以通过clojure.core/resolve在制定的命名空间里通过名字查找函数.名字需要使用引号修饰.返回值可以直接当做函数使用,比如,当做参数传递给高阶函数:


          (resolve 'clojure.set 'difference)  ; ⇒ #'clojure.set/difference
          
          (let [f (resolve 'clojure.set 'difference)]
             (f #{1 2 3} #{3 4 5 6}))  ; ⇒ #{1 2}

编译异常

本节讨论一些常见的编译错误.

ClassNotFoundException

这个异常的意思是JVM无法加载类.可能是因为拼写错误,或者在classpath上没有这个类.可能是你的项目没有很好的处理依赖关系.

user=> (import java.uyil.concurrent.TimeUnit)
          ClassNotFoundException java.uyil.concurrent.TimeUnit  java.net.URLClassLoader$1.run (URLClassLoader.java:366)

在上面的例子中,java.uyil.concurrent.TimeUnit拼写错误,应该是java.util.concurrent.TimeUnit

CompilerException java.lang.RuntimeException: No such var

这个错误的意思是,使用了一个不存在的var.这可能是拼写错误,或者不正确的宏展开等类似问题.

user=> (clojure.java.io/resouce "thought_leaders_quotes.csv")
          CompilerException java.lang.RuntimeException: No such var: clojure.java.io/resouce, compiling:(NO_SOURCE_PATH:1)

在上面的例子中,clojure.java.io/resouce应该写成clojure.java.io/resource.NO_SOURCE_PATH的意思是编译是在repl里触发的,而不是一个Clojure源文件.

 类似资料: