本文是我对Clojure书籍 CLOJURE FOR THE BRAVE AND TRUE第六章Organizing Your Project: A Librarian’s Tale 做的翻译。翻译形式,中英对照,英文引用跟着中文翻译。如有错误,在所难免,欢迎指正。
译文开始。
Within each of us lives a librarian named Melvil, a fantastical creature who delights in the organizational arts. Day and night, Melvil yearns to bring order to your codebase. Fortunately, Clojure provides a suite of tools designed specifically to aid this homunculus in its constant struggle against the forces of chaos.
Clojure提供了一组专门设计的工具用于使代码整齐有序。
These tools help you organize your code by grouping together related functions and data. They also prevent name collisions so you don’t accidentally overwrite someone else’s code or vice versa. Join me in a tale of suspense and mystery as you learn how to use these tools and solve the heist of a lifetime! By the end of the saga, you’ll understand the following:
这些工具帮助你把相关函数和数据分成组。还防止发生命名冲突,让你的代码与别人的不会互相覆盖。学习完本章,你会了解这些:
- What
def
does- What namespaces are and how to use them
- The relationship between namespaces and the filesystem
- How to use
refer
,alias
,require
,use
, andns
- How to organize Clojure projects using the filesystem
def
干了什么- 什么是命名空间,如何使用它们
- 命名空间与文件系统的关系
- 如何使用
refer
,alias
,require
,use
, 和ns
- 如何用文件系统组织Clojure项目
I’ll start with a high-level overview of Clojure’s organizational system, which works much like a library. Melvil quivers with excitement!
我们从高层次的Clojure组织系统概览开始,这个系统很像图书馆。
Your Project as a Library
图书馆般的项目
Real-world libraries store collections of objects, such as books, magazines, and DVDs. They use addressing systems, so when you’re given an object’s address, you can navigate to the physical space and retrieve the object.
真实的图书馆存储物品集合,比如书,杂志,DVD。图书馆使用地址系统,有了物品地址,你就能找到物品存放位置并取得那个物品。
Of course, no human being would be expected to know offhand what a book’s or DVD’s address is. That’s why libraries record the association between an object’s title and its address and provide tools for searching these records. In ye olden times before computers, libraries provided card catalogs, which were cabinets filled with paper cards containing each book’s title, author, “address” (its Dewey decimal or Library of Congress number), and other info.
当然,没人能轻易记住书或DVD的地址,因此图书馆有物品标题和地址的关联记录,并且有搜索这些记录的工具。计算机出现之前,图书馆里有分类卡片,柜子里放满了卡片,卡片上有每本书的书名,作者,地址(杜威小数或国会图书馆号码)和其他信息。
For example, to find The Da Vinci Code, you would riffle through the title catalog (cards ordered by title) until you found the correct card. On that card you would see the address 813.54 (if it’s using the Dewey decimal system), navigate your library to find the shelf where The Da Vinci Code resides, and engage in the literary and/or hate-reading adventure of your lifetime.
例如,要找达芬奇密码,你会快速浏览分类目录(卡片按标题排序)并找到卡片。卡片上的地址是813.54(如果使用的是杜威小数),按这个地址找到存放达芬奇密码的书架,取得目标。
It’s useful to imagine a similar setup in Clojure. I think of Clojure as storing objects (like data structures and functions) in a vast set of numbered shelves. No human being could know offhand which shelf an object is stored in. Instead, we give Clojure an identifier that it uses to retrieve the object.
Clojure的组织方式与此类似。Clojure把物品(比如数据结构和函数)保存在许多有编号的架子上。没人能轻易知道某个物品存放在哪个架子上。我们给出那个物品的标识符,Clojure用这个标识符取得这个物品。
For this to be successful, Clojure must maintain the associations between our identifiers and shelf addresses. It does this by using namespaces. Namespaces contain maps between human-friendly symbols and references to shelf addresses, known as vars, much like a card catalog.
为完成这件事,Clojure用命名空间保存标识符与架子地址的关联。命名空间里有很多映射,每个映射包含了对人类友好的符号,和称为变量的对于架子地址的引用,很像卡片分类。
Technically, namespaces are objects of type
clojure.lang.Namespace
, and you can interact with them just like you can with Clojure data structures. For example, you can refer to the current namespace with*ns*
, and you can get its name with(ns-name *ns*)
:
从技术上说,命名空间是类型为clojure.lang.Namespace
的对象,同其他Clojure数据结构一样,你可以与命名空间交互。比如,你可以用*ns*
引用当前命名空间,可以用(ns-name *ns*)
得到它的名字:
1 | (ns-name *ns*) |
When you start the REPL, for example, you’re in the
user
namespace (as you can see here). The prompt shows the current namespace using something likeuser=>
.
例如,启动REPL时候,处于user
命名空间。提示符显示当前的命名空间,比如user=>
。
The idea of a current namespace implies that you can have more than one, and indeed Clojure allows you to create as many namespaces as you want (although technically, there might be an upper limit to the number of names you can create). In Clojure programs, you are always in a namespace.
Clojure允许你随意创建多个命名空间(技术上有上限)。Clojure程序总是处于一个命名空间里。
As for symbols, you’ve been using them this entire time without even realizing it. For example, when you write
(map inc [1 2])
, bothmap
andinc
are symbols. Symbols are data types within Clojure, and I’ll explain them thoroughly in the next chapter. For now, all you need to know is that when you give Clojure a symbol likemap
, it finds the corresponding var in the current namespace, gets a shelf address, and retrieves an object from that shelf for you—in this case, the function thatmap
refers to. If you want to just use the symbol itself, and not the thing it refers to, you have to quote it. Quoting any Clojure form tells Clojure not to evaluate it but to treat it as data. The next few examples show what happens when you quote a form.
说到符号,其实你一直在没有意识到它们的情况下使用它们。例如,代码(map inc [1 2])
里的map
和inc
都是符号。符号是Clojure的数据类型,下章会详细解释(点此查看)。现在要知道的就是:当你使用map
这样的符号时,Clojure从当前命名空间找到对应的变量,得到一个架子地址,从那个架子取回你要的物品,即map
指向的函数。如果想要使用符号本身,必须引用符号。任何被引用的形式被Clojure当作数据而不对其求值。如下所示:
1 | ➊ inc |
When you evaluate
inc
in the REPL at ➊, it prints out the textual representation of the function thatinc
refers to. Next, you quoteinc
at ➋, so the result is the symbolinc
. Then, you evaluate a familiarmap
application at ➌ and get a familiar result. After that, you quote the entire list data structure at ➍, resulting in an unevaluated list that includes themap
symbol, theinc
symbol, and a vector.
➊处:对inc
求值,打印出inc
指向的函数的文本表示。➋处:引用inc
,结果是符号inc
。➌处:对一个map
调用求值,得到熟悉的结果。➍处,引用整个list数据结构,得到未求值的list,包含了符号map
,inc
和一个vector。
Now that you know about Clojure’s organization system, let’s look at how to use it.
现在你了解了Clojure的组织系统,来看看如何使用它。
Storing Objects with def
用def保存对象
The primary tool in Clojure for storing objects is
def
. Other tools likedefn
usedef
under the hood. Here’s an example of def in action:
Clojure里用于存储对象的主要工具是def
。defn
等其他工具内部也使用def
。这是个def的实际使用例子:
1 | (def great-books ["East of Eden" "The Glass Bead Game"]) |
This code tells Clojure:
- Update the current namespace’s map with the association between
great-books
and the var.- Find a free storage shelf.
- Store
["East of Eden" "The Glass Bead Game"]
on the shelf.- Write the address of the shelf on the var.
- Return the var (in this case,
#'user/great-books
).
这段代码告诉Clojure:
- 用
great-books
与def
生成的变量的关联更新当前命名空间。 - 找到一个未使用的存储架子
- 把
["East of Eden" "The Glass Bead Game"]
存放在哪个架子上。 - 在变量上写上这个架子的地址
- 返回这个变量(
#'user/great-books
)。
This process is called interning a var. You can interact with a namespace’s map of symbols-to-interned-vars using
ns-interns
. Here’s how you’d get a map of interned vars:
这个过程叫变量驻留。可以用ns-interns
查看驻留的变量:
1 | (ns-interns *ns*) |
You can use the get function to
get
a specific var:
也可以用get
函数得到特定的变量:
1 | (get (ns-interns *ns*) 'great-books) |
By evaluating
(ns-map *ns*)
, you can also get the full map that the namespace uses for looking up a var when given a symbol.(ns-map *ns*)
gives you a very large map that I won’t print here, but try it out!
求值(ns-map *ns*)
,可以得到完全映射,命名空间用这个映射查找符号对应的变量。求值结果很长,我就不打印了,但你要试试!
[译者增加]
#'user/great-books
is the reader form of a var. I’ll explain more about reader forms in Chapter 7. For now, just know that you can use#'
to grab hold of the var corresponding to the symbol that follows;#'user/great-books
lets you use the var associated with the symbolgreat-books
within theuser
namespace. We canderef
vars to get the objects they point to:
#'user/great-books
是变量的读入程序形式。第七章会详细讲解(点此查看)。现在只要知道:用#'
可以得到其后符号对应的变量。#'user/great-books
可以得到user
命名空间下的,great-books
符号关联的变量。可以用deref
得到变量指向的值:
1 | (deref #'user/great-books) |
This is like telling Clojure, “Get the shelf number from the var, go to that shelf number, grab what’s on it, and give it to me!”
这就像告诉Clojure,”从这个变量得到架子编号,去那个架子那里,把放在它上面的东西拿给我!”
But normally, you would just use the symbol:
一般情况我,直接用这个符号就可以:
1 | great-books |
This is like telling Clojure, “Retrieve the var associated with great-books and deref that bad Jackson.”
这就像告诉Clojure,“取得与great-books关联的变量,并对它取值”。
So far so good, right? Well, brace yourself, because this idyllic paradise of organization is about to be turned upside down! Call
def
again with the same symbol:
到现在一切都不错。但秩序将被打乱!用同样的符号再次调用def
:
1 | (def great-books ["The Power of Bees" "Journey to Upstairs"]) |
The var has been updated with the address of the new vector. It’s like you used white-out on the address on a card in the card catalog and then wrote a new address. The result is that you can no longer ask Clojure to find the first vector. This is referred to as a name collision. Chaos! Anarchy!
这个变量已经被更新,用新的vector地址。就像你把分类卡片上的地址涂白,然后写上一个新地址。结果是再也找不到第一个vector。这被称为命名冲突。全乱了!
You may have experienced this in other programming languages. JavaScript is notorious for it, and it happens in Ruby as well. It’s a problem because you can unintentionally overwrite your own code, and you also have no guarantee that a third-party library won’t overwrite your code. Melvil recoils in horror! Fortunately, Clojure allows you to create as many namespaces as you like so you can avoid these collisions.
你可能在其他语言里经历过命名冲突。JavaScript对此臭名昭著,Ruby里也会发生。你可能无意地覆盖自己代码,也无法保证第三方库不会覆盖你的代码,所以这是个问题。为避免这个问题,Clojure允许你创建任意个命名空间。
Creating and Switching to Namespaces
创建并切换命名空间
Clojure has three tools for creating namespaces: the function
create-ns
, the functionin-ns
, and the macrons
. You’ll mostly use thens
macro in your Clojure files, but I’ll hold off on explaining it for a few pages because it combines many tools, and it’s easier to understand after I discuss each of the other tools.
Clojure有三个工具用于创建命名空间:create-ns
函数,in-ns
函数,ns
宏。你通常使用ns
宏,但我会推迟解释它,因为它结合了很多工具。了解了其他工具后,更容易理解它。
create-ns
takes a symbol, creates a namespace with that name if it doesn’t exist already, and returns the namespace:
create-ns
接受一个符号,如果这个名字不存在,就用这个名字创建一个命名空间,并返回这个命名空间:
1 | user=> (create-ns 'cheese.taxonomy) |
You can use the returned namespace as an argument in a function call:
函数调用中可以把返回的命名空间作为参数:
1 | user=> (ns-name (create-ns 'cheese.taxonomy)) |
In practice, you’ll probably never use
create-ns
in your code, because it’s not very useful to create a namespace and not move into it. Usingin-ns
is more common because it creates the namespace if it doesn’t exist and switches to it, as shown in Listing 6-1.
实际上,你的代码里可能永远都不会使用create-ns
,因为创建一个命名空间,但不进入它,没什么用。in-ns
的使用更为常见,因为它创建并切换到一个命名空间,如列表6-1所示。
6-1. Using in-ns to create a namespace and switch to it
6-1 用in-ns创建并切换到一个命名空间
1 | user=> (in-ns 'cheese.analysis) |
Notice that your REPL prompt is now
cheese.analysis>
, indicating that you are indeed in the new namespace you just created. Now when you use def, it will store the named object in thecheese.analysis
namespace.
注意现在的REPL提示符是cheese.analysis>
,说明你现在确实在这个新命名空间里。现在使用def,会把命名对象存储在cheese.analysis
命名空间里。
But what if you want to use functions and data from other namespaces? To do that, you can use a fully qualified symbol. The general form is namespace
/
name:
但如果你想使用其他命名空间的函数,该怎么办呢?你可以使用完全限定的符号。通用形式是namespace/
name:
1 | cheese.analysis=> (in-ns 'cheese.taxonomy) |
This creates a new namespace,
cheese.taxonomy
, definescheddars
in that namespace, and then switches back to thecheese.analysis
namespace. You’ll get an exception if you try to refer to thecheese.taxonomy
namespace’scheddars
from withincheese.analysis
, but using the fully qualified symbol works:
这创建了一个新命名空间,cheese.taxonomy
,在这个命名空间里定义了cheddars
,然后切换回cheese.analysis
命名空间。如果从cheese.analysis
里引用cheese.taxonomy
命名空间的cheddars
,会出现异常。但使用完全限定的符号没问题:
1 | cheese.analysis=> cheese.taxonomy/cheddars |
Typing these fully qualified symbols can quickly become a nuisance. For instance, say I’m an extremely impatient academic specializing in semiotics-au-fromage, or the study of symbols as they relate to cheese.
输入完全限定的符号很快就变成一件麻烦事。
Suddenly, the worst conceivable thing that could possibly happen happens! All across the world, sacred and historically important cheeses have gone missing. Wisconsin’s Standard Cheddar: gone! The Great Cheese Jars of Tutankhamun: stolen! The Cheese of Turin: replaced with a hoax cheese! This threatens to throw the world into total chaos for some reason! Naturally, as a distinguished cheese researcher, I am honor-bound to solve this mystery. Meanwhile, I’m being chased by the Illuminati, the Freemasons, and the Foot Clan!
Because I’m an academic, I attempt to solve this mystery the best way I know how—by heading to the library and researching the shit out of it. My trusty assistant, Clojure, accompanies me. As we bustle from namespace to namespace, I shout at Clojure to hand me one thing after another.
But Clojure is kind of dumb and has a hard time figuring out what I’m referring to. From within the user namespace, I belt out, “
join
! Give mejoin
!”—specks of spittle flying from my mouth. “RuntimeException: Unable to resolve symbol: join
,” Clojure whines in response. “For the love of brie, just hand meclojure.string/join
!” I retort, and Clojure dutifully hands me the function I was looking for.My voice gets hoarse. I need some way to tell Clojure what objects to get me without having to use the fully qualified symbol every. damn. time.
Luckily, Clojure provides the
refer
andalias
tools that let me yell at it more succinctly.
幸运的是,Clojure提供了refer
和alias
,能使代码更简洁。
refer
refer
refer
gives you fine-grained control over how you refer to objects in other namespaces. Fire up a new REPL session and try the following. Keep in mind that it’s okay to play around with namespaces like this in the REPL, but you don’t want your Clojure files to look like this; the proper way to structure your files is covered in “Real Project Organization” on page 133.
refer
提供了如何引用其他命名空间的对象的细粒度控制。启动一个新REPL,尝试下面的代码。记住,在REPL里这么玩没问题,但别在Clojure文件里这么干。后面会讲到文件里怎么做。
1 | user=> (in-ns 'cheese.taxonomy) |
This code creates a
cheese.taxonomy
namespace and two vectors within it:cheddars
andbries
. Then it creates and moves to a new namespace calledcheese.analysis
. Callingrefer
with a namespace symbol lets you refer to the corresponding namespace’s objects without having to use fully qualified symbols. It does this by updating the current namespace’s symbol/object map. You can see the new entries like this:
这段代码创建了命名空间cheese.taxonomy
,并在其中创建了两个vector,cheddars
和bries
。然后创建并切换到另一个命名空间cheese.analysis
。调用refer
加上一个命名空间符号以后,你就可以引用那个命名空间的对象,而不用使用完全限定的符号。这个调用更新了当前命名空间的符号/对象映射。你可以这样看到新条目:
1 | cheese.analysis=> (clojure.core/get (clojure.core/ns-map clojure.core/*ns*) 'bries) |
It’s as if Clojure
- Calls
ns-interns
on thecheese.taxonomy
namespace- Merges that with the
ns-map
of the current namespace- Makes the result the new
ns-map
of the current namespace
就好像Clojure做了如下事情
- 在
cheese.taxonomy
命名空间上调用ns-interns
- 把上面的结果与当前命名空间的
ns-map
合并 (ns-map *ns*)
的结果变成上面的结果
When you call
refer
, you can also pass it the filters:only
,:exclude
, and:rename
. As the names imply,:only
and:exclude
restrict which symbol/var mappings get merged into the current namespace’sns-map
.:rename
lets you use different symbols for the vars being merged in. Here’s what would happen if we had modified the preceding example to use:only
:
调用refer
时候,也可以增加参数: :only
,:exclude
和:rename
。:only
和:exclude
限制合并的对象。:rename
让你对合并的对象使用不同的符号。如果上面的例子使用:only
,结果会是这样:
1 | cheese.analysis=> (clojure.core/refer 'cheese.taxonomy :only ['bries]) |
And here’s
:exclude
in action:
下面是:exclude
的例子:
1 | cheese.analysis=> (clojure.core/refer 'cheese.taxonomy :exclude ['bries]) |
Lastly, a
:rename
example:
最后是:rename
的例子:
1 | cheese.analysis=> (clojure.core/refer 'cheese.taxonomy :rename {'bries 'yummy-bries}) |
Notice that in these last examples we have to use the fully qualified names of all the objects in
clojure.core
, likeclojure.core/ns-map
andclojure.core/refer
. We didn’t have to do that in theuser
namespace. That’s because the REPL automatically refersclojure.core
within theuser
namespace. You can make your life easier by evaluating(clojure.core/refer-clojure)
when you create a new namespace; this will refer the clojure.core namespace, and I’ll be using it from now on. Instead of seeingclojure.core/refer
in the examples, you’ll only seerefer
.
注意上面的例子使用了clojure.core
里的完全限定的名字,比如clojure.core/ns-map
和clojure.core/refer
。在user
命名空间里不需要这么干,因为user
命名空间自动引入了clojure.core
。通过使用(clojure.core/refer-clojure)
也可以达到同样的效果。下面得代码会使用这个方法。
Another thing to notice is that you have complete freedom over how you organize your functions and data across namespaces. This lets you sensibly group related functions and data together in the same namespace.
另一个需要注意的是,如何组织函数和数据完全由你决定。这让你能够合理地组织相关函数和数据。
Sometimes you may want a function to be available only to other functions within the same namespace. Clojure allows you to define private functions using
defn-
:
有时候你想让一个函数只对相同命名空间下的函数可见。用defn-
可以定义私有函数:
1 | (in-ns 'cheese.analysis) |
If you try to call this function from another namespace or refer it, Clojure will throw an exception. You can see this when you evaluate the code at ➊ and ➋:
如果尝试从其他命名空间调用或引用这个函数,会引起异常。可以从下面代码的➊, ➋ 处看到:
1 | cheese.analysis=> (in-ns 'cheese.taxonomy) |
As you can see, even if you explicitly
refer
the function, you can’t use the function from another namespace, because you made it private. (If you want to be tricky, you can still access the private var using the arcane syntax@#'some/private-var
, but you’ll rarely want to do that.)
如你所见,即使明确refer
这个函数,也不能在另一个命名空间使用它,因为它是私有的。(如果耍花招,还是可以访问私有变量,使用这个语法@#'some/private-var
,但很少会需要这么做)。
alias
alias
Compared to
refer
,alias
is relatively simple. All it does is let you shorten a namespace name for using fully qualified symbols:
与refer
相比,alias
更简单。它的全部工作就是缩短完全限制的符号的名字。
1 | cheese.analysis=> (clojure.core/alias 'taxonomy 'cheese.taxonomy) |
This code lets us use call symbols from the
cheese.taxonomy
namespace with the shorter aliastaxonomy
.
这段代码让我们把taxonomy
当作cheese.taxonomy
用。
refer
andalias
are your two basic tools for referring to objects outside your current namespace! They’re great aids to REPL development.
refer
和alias
是引用当前命名空间外的对象的基本工具。REPL开发中很好用。
However, it’s unlikely that you’d create your entire program in the REPL. In the next section, I’ll cover everything you need to know to organize a real project with source code living on the filesystem.
但不太可能所有程序都在REPL里创建。下节会讲解如何在文件系统上组织真实项目。
Real Project Organization
真实项目组织
Now that I’ve covered the building blocks of Clojure’s organization system, I’ll show you how to use them in real projects. I’ll discuss the relationship between file paths and namespace names, explain how to load a file with
require
anduse
, and show how to usens
to set up a namespace.
现在已经讲解了Clojure组织系统的构建模块,我会为你展示如何在真实项目里使用它们。我会解释文件路径与命名空间的关系,如何用require
和use
加载文件,如何用ns
设置命名空间。
The Relationship Between File Paths and Namespace Names
文件路径与命名空间名称的关系
To kill two birds with one stone (or feed two birds with one seed, depending on how much of a hippie you are), I’ll cover more on namespaces while we work on catching the pesky international cheese thief by mapping the locations of his heists. Run the following:
运行下面的命令,建立一个抓奶酪贼应用,以便与进一步讲解命名空间:
1 | lein new app the-divine-cheese-code |
This should create a directory structure that looks like this:
这会创建如下的目录结构:
1 | | .gitignore |
Now, open src/the_divine_cheese_code/core.clj. You should see this on the first line:
打开src/the_divine_cheese_code/core.clj。你会看到第一行:
1 | (ns the-divine-cheese-code.core |
ns
is the primary way to create and manage namespaces within Clojure. I’ll explain it in full shortly. For now, though, just know that this line is very similar to thein-ns
function we used in Listing 6-1. It creates a namespace if it doesn’t exist and then switches to it. I also cover(:gen-class)
in more detail in Chapter 12.
ns
是Clojure创建和管理命名空间的主要方法。但现在只需知道它与in-ns
函数类似,创建(如果没有)并切换到一个命名空间。在12章会对(:gen-class)
详细讲解。
The name of the namespace is
the-divine-cheese-code.core
. In Clojure, there’s a one-to-one mapping between a namespace name and the path of the file where the namespace is declared, according to the following conventions:
命名空间的名字是the-divine-cheese-code.core
。命名空间的名字与文件的路径,按如下惯例一一对应的:
- When you create a directory with
lein
(as you did here), the source code’s root is src by default.- Dashes in namespace names correspond to underscores in the filesystem. So
the-divine-cheese-code
is mapped to the_divine_cheese_code on the filesystem.- The component preceding a period (
.
) in a namespace name corresponds to a directory. For example, sincethe-divine-cheese-code.core
is the namespace name, the_divine_cheese_code is a directory.- The final component of a namespace corresponds to a file with the .clj extension;
core
is mapped to core.clj.
- 用
lein
创建目录时,源代码的默认根目录是src。 - 命名空间名称里的破折号对应文件系统的下划线。所以
the-divine-cheese-code
与文件系统的the_divine_cheese_code对应。 - 点(
.
)前面的部分对应一个目录。例如,the-divine-cheese-code.core
是个命名空间名称,the_divine_cheese_code 是个目录。 - 命名空间最后一部分对应一个扩展名是clj的文件;
core
与core.clj对应。
Your project will have one more namespace,
the-divine-cheese-code.visualization.svg
. Go ahead and create the file for it now:
增加一个命名空间,the-divine-cheese-code.visualization.svg
,建立文件:
1 | mkdir src/the_divine_cheese_code/visualization |
Notice that the filesystem path follows these conventions. With the relationship between namespaces and the filesystem down, let’s look at
require
anduse
.
注意文件名遵循上面的惯例。知道了命名空间与文件系统的关系,我们来看看require
和use
。
Requiring and Using Namespaces
请求与使用命名空间
The code in the
the-divine-cheese-code.core
namespace will use the functions in the namespacethe-divine-cheese-code.visualization.svg
to create SVG markup. To usesvg
’s functions,core
will have torequire
it. But first, let’s add some code to svg.clj. Make it look like this (you’ll add more later):
命名空间the-divine-cheese-code.core
里的代码要使用命名空间the-divine-cheese-code.visualization.svg
里的函数,用于创建SVG标记。要使用svg
的函数,core
必须require
它。先往svg.clj里增加一些代码:
1 | (ns the-divine-cheese-code.visualization.svg) |
This defines two functions,
latlng->point
andpoints
, which you’ll use to convert a seq of latitude/longitude coordinates into a string of points. To use this code from the core.clj file, you have torequire
it.require
takes a symbol designating a namespace and ensures that the namespace exists and is ready to be used; in this case, when you call(require 'the-divine-cheese-code.visualization.svg)
, Clojure reads and evaluates the corresponding file. By evaluating the file, it creates thethe-divine-cheese-code.visualization.svg
namespace and defines the functionslatlng->point
andpoints
within that namespace. Even though the file svg.clj is in your project’s directory, Clojure doesn’t automatically evaluate it when it runs your project; you have to explicitly tell Clojure that you want to use it.
这段代码定义了两个函数,latlng->point
和points
,可以把经度/纬度坐标序列转化成点组成的字符串。要从core.js文件里使用这些函数,必须require
它。require
接受一个命名空间符号作为参数,确保命名空间存在,并使它处于可用状态。这个例子里,当你调用(require 'the-divine-cheese-code.visualization.svg)
时,Clojure读取并求值对应的文件。通过求值这个文件,创建了the-divine-cheese-code.visualization.svg
命名空间,并在里面定义了函数latlng->point
和points
。虽然文件svg.clj在项目目录里,运行项目时,Clojure也不会自动对它求值,你必须明确说明你要使用它。
After requiring the namespace, you can refer it so that you don’t have to use fully qualified names to reference the functions. Go ahead and require
the-divine-cheese-code.visualization.svg
and add theheists
seq to make core.clj match the listing:
请求这个命名空间后,可以引用它,以避免使用完全限定的函数名。在core.clj里请求the-divine-cheese-code.visualization.svg
,并添加heists
序列:
1 | (ns the-divine-cheese-code.core) |
Now you have a seq of heist locations to work with and you can use functions from the
visualization.svg
namespace. Themain
function simply applies thepoints
function toheists
. If you run the project withlein run
, you should see this:
现在有了一个偷盗地点序列,并且visualization.svg
里的函数是可用的。main
函数对heists
应用了points
函数。运行lein run
,会看到:
1 | 50.95,6.97 47.37,8.55 43.3,5.37 47.37,8.55 41.9,12.45 |
Hooray! You’re one step closer to catching that purloiner of the fermented curd! Using
require
successfully loadedthe-divine-cheese-code.visualization.svg
for use.
require
成功加载了the-divine-cheese-code.visualization.svg
。
The details of
require
are actually a bit complicated, but for practical purposes you can think ofrequire
as telling Clojure the following:
require
的细节有点复杂,你可以认为实际上它让Clojure做了下列事情:
- Do nothing if you’ve already called
require
with this symbol (the-divine-cheese-code.visualization.svg
).- Otherwise, find the file that corresponds to this symbol using the rules described in “The Relationship Between File Paths and Namespace Names” on page 133. In this case, Clojure finds
src/the_divine_cheese_code/visualization/svg.clj
.- Read and evaluate the contents of that file. Clojure expects the file to declare a namespace corresponding to its path (which ours does).
- 如果已经
require
过the-divine-cheese-code.visualization.svg
,什么都不干。 - 否则,用上面描述的规则找到符号对应的文件,
src/the_divine_cheese_code/visualization/svg.clj
- 读入并求值那个文件内容。
require
also lets you alias a namespace when you require it, using:as
oralias
. This:
使用require
时,也可以用:as
或alias
给命名空间起个别名:
1 | (require '[the-divine-cheese-code.visualization.svg :as svg]) |
is equivalent to this:
与下面的代码等效:
1 | (require 'the-divine-cheese-code.visualization.svg) |
You can now use the aliased namespace:
现在可以使用这个别名:
1 | (svg/points heists) |
Clojure provides another shortcut. Instead of calling
require
andrefer
separately, the functionuse
does both. It’s frowned upon to useuse
in production code, but it’s handy when you’re experimenting in the REPL and you want to quickly get your hands on some functions. For example, this:
还可以使用use
函数,效果与先用require
,再用refer
一样。生产环境下不赞成用use
,但REPL里用它很方便:
1 | (require 'the-divine-cheese-code.visualization.svg) |
is equivalent to this:
与下面代码等效:
1 | (use 'the-divine-cheese-code.visualization.svg) |
You can alias a namespace with
use
just like you can withrequire
. This:
同require
一样,use
也可以加上别名。
1 | (require 'the-divine-cheese-code.visualization.svg) |
is equivalent to the code in Listing 6-2, which also shows aliased namespaces being used in function calls.
与下面代码等效,下面代码也演示了命名空间的别名被用于函数调用中。
6-2. Sometimes it’s handy to both use and alias a namespace.
1 | (use '[the-divine-cheese-code.visualization.svg :as svg]) |
It may seem redundant to alias a namespace with
use
here becauseuse
already refers the namespace (which lets you simply callpoints
instead ofsvg/points
). In certain situations, though, it’s handy becauseuse
takes the same options asrefer
(:only
,:exclude
,:as
, and:rename
). You might want to alias a namespace withuse
when you’ve skipped referring a symbol. You could use this:
使用use
后,再加上别名看起来有点多余。但因为use
同refer
一样,也能接受:only
,:exclude
,:as
和:rename
,所以有些情况下用起来很方便。比如:你可能只引用了部分符号,不使用use
可以这么写:
1 | (require 'the-divine-cheese-code.visualization.svg) |
Or you could use the
use
form in Listing 6-3 (which also includes examples of how you can call functions).
6-3 使用use
,可以这么写,这里也演示了函数调用:
1 | (use '[the-divine-cheese-code.visualization.svg :as svg :only [points]]) |
If you try Listing 6-3 in a REPL and
latlng->point
doesn’t throw an exception, it’s because you referredlatlng->point
in Listing 6-2. You’ll need to restart your REPL session for the code to behave as shown in Listing 6-3.
如果在REPL里尝试6-3,latlng->point
不会抛出异常,因为6-2里已经引用过了。如果想同上述代码一样,你需要重启REPL。
The takeaway here is that
require
anduse
load files and optionallyalias
orrefer
their namespaces. As you write Clojure programs and read code written by others, you might encounter even more ways of writingrequire
anduse
, at which point it’ll make sense to read Clojure’s API docs (http://clojure.org/reference/libs) to understand what’s going on. However, what you’ve learned so far aboutrequire
anduse
should cover 95.3 percent of your needs.
你需要知道的是,require
和use
加载文件,并且可选择地对命名空间alias
或refer
。如果需要了解更多,可以查看文档( http://clojure.org/reference/libs )。但require
和use
能满足大部分需要。
The ns Macro
ns宏
Now it’s time to look at the
ns
macro. The tools covered so far—in-ns
,refer
,alias
,require
, anduse
—are most often used when you’re playing in the REPL. In your source code files, you’ll typically use thens
macro because it allows you to use the tools described so far succinctly and provides other useful functionality. In this section, you’ll learn about how onens
call can incorporaterequire
,use
,in-ns
,alias
, andrefer
.
是时候看ns
宏了。到现在讲解的工具-in-ns
,refer
,alias
,require
和use
-大多数都用于REPL。在源码文件里,主要使用ns
宏,因为它能让你更简洁地使用这些工具,还提供了其他功能。这节里你学习如何用一个ns
调用,包含require
,use
,in-ns
,alias
和refer
。
One useful task
ns
does is refer theclojure.core
namespace by default. That’s why you can callprintln
from withinthe-divine-cheese-code.core
without using the fully qualified name,clojure.core/println
.
ns
宏的一个好处是默认引用了clojure.core
命名空间。这就是为什么在the-divine-cheese-code.core
里可以直接调用println
,而不需要使用完全限定的名字clojure.core/println
。
You can control what gets referred from
clojure-core
with:refer-clojure
, which takes the same options asrefer
:
用:refer-clojure
可以限制引入哪些clojure-core
的东西,参数与refer
一样:
1 | (ns the-divine-cheese-code.core |
If you called this at the beginning of divine_cheese_code.core.clj, it would break your code, forcing you to use
clojure.core/println
within the-main
function.
如果在divine_cheese_code.core.clj开始调用上面的代码,会使你的代码报错,如果这么调用,-main
函数里必须使用clojure.core/println
。
Within
ns
, the form(:refer-clojure)
is called a reference. This might look weird to you. Is this reference a function call? A macro? What is it? You’ll learn more about the underlying machinery in Chapter 7. For now, you just need to understand how each reference maps to function calls. For example, the preceding code is equivalent to this:
在ns
里,形式(:refer-clojure)
被叫做引用。看起来有点怪,但只要知道每个引用与函数调用对应就可以了,前面的代码与下面得等价:
1 | (in-ns 'the-divine-cheese-code.core) |
There are six possible kinds of references within
ns
:
ns
里有6种引用:
(:refer-clojure)
(:require)
(:use)
(:import)
(:load)
(:gen-class)
(:import)
and(:gen-class)
are covered in Chapter 12. I won’t cover(:load)
because it is seldom used.
(:import)
和(:gen-class)
在12章讲解。(:load)
不讲解,因为用的很少。
(:require)
works a lot like therequire
function. For example, this:
(:require)
的作用与require
函数很像,例如:
1 | (ns the-divine-cheese-code.core |
is equivalent to this:
与下面代码等效:
1 | (in-ns 'the-divine-cheese-code.core) |
Notice that in the
ns
form (unlike thein-ns
function call), you don’t have to quote your symbol with'
. You never have to quote symbols withinns
.
注意,ns
里不用像in-ns
那样,必须用'
引用符号。永远不需要。
You can also
alias
a library that yourequire
withinns
, just like when you call the function. This:
ns
里的require
也可以加上别名,跟require
函数一样,这段代码:
1 | (ns the-divine-cheese-code.core |
is equivalent to this:
与下面的代码等效:
1 | (in-ns 'the-divine-cheese-code.core) |
You can require multiple libraries in a
(:require)
reference as follows. This:
一个(:require)
里可以请求多个库,这个代码:
1 | (ns the-divine-cheese-code.core |
is equivalent to this:
与下面的代码等效:
1 | (in-ns 'the-divine-cheese-code.core) |
However, one difference between the
(:require)
reference and therequire
function is that the reference also allows you to refer names. This:
(:require)
引用与require
函数的一个不同是,前者允许你引用名称。这个代码:
1 | (ns the-divine-cheese-code.core |
is equivalent to this:
与下面的代码等效:
1 | (in-ns 'the-divine-cheese-code.core) |
You can also refer all symbols (notice the
:all
keyword):
也可以用:all
引用所有符号:
1 | (ns the-divine-cheese-code.core |
which is the same as doing this:
上面代码与下面代码等效:
1 | (in-ns 'the-divine-cheese-code.core) |
This is the preferred way to require code, alias namespaces, and refer symbols. It’s recommended that you not use
(:use)
, but since it’s likely that you’ll come across it, it’s good to know how it works. You know the drill. This:
这是首选的请求代码,命名空间起别名,引用符号方式。建议你不要使用(:use)
,但既然你可能碰到它,最好知道它是怎么回事。这段代码:
1 | (ns the-divine-cheese-code.core |
does this:
与下面代码等效:
1 | (in-ns 'the-divine-cheese-code.core) |
whereas this:
这段代码:
1 | (ns the-divine-cheese-code.core |
does this:
与下面代码等效:
1 | (in-ns 'the-divine-cheese-code.core) |
Notice that when you follow
:use
with a vector, it takes the first symbol as the base and then callsuse
with each symbol that follows.
注意,:use
后面跟着一个vector时,以第一个符号作为基础,加上每个后面的符号,组成所有需要use
的符号,并对它们调用use
。
Oh my god, that’s it! Now you can use
ns
like a pro! And you’re going to need to, dammit, because that voleur des fromages (as they probably say in French) is still running amok! Remember him/her?!
终于讲完了!现在你可以像个专家一样使用ns
了!
To Catch a Burglar
抓小偷
We can’t allow this plunderer of parmesan to make off with any more cheese! It’s time to finish drawing lines based on the coordinates of the heists! That will surely reveal something!
不能让小偷偷走更多奶酪。我们继续完成根据坐标画线,这一定会显露出一些东西!
Using the latitude coordinates for each heist, you’ll connect the dots in an SVG image. But if you draw lines using the given coordinates, the result won’t look right for two reasons. First, latitude coordinates ascend from south to north, whereas SVG y-coordinates ascend from top to bottom. In other words, you need to flip the coordinates or the drawing will be upside down.
把每个偷盗地点坐标在SVG图片里连成线。但如果用给出的坐标画线,结果不对。原因一,纬度坐标从南往北,而SVG的y坐标从顶往下增加。所以需要翻转坐标,否则画是反的。
Second, the drawing will be very small. To fix that, you’ll zoom in on it by translating and scaling it. It’s like turning a drawing that looks like Figure 6-1a into Figure 6-1b.
原因二,画会很小。为此需要放大。就像下图中把图形翻过来一样。
图6-1
Honestly, this is all completely arbitrary and it’s no longer directly related to code organization, but it’s fun and I think you’ll have a good time going through the code! Make your svg.clj file match Listing 6-4:
实话说,这跟代码组织没什么关系,但这很有趣!试你的svg.clj文件与下面的代码一样:
1 | (ns the-divine-cheese-code.visualization.svg |
You define the
comparator-over-maps
function at ➊. This is probably the trickiest bit, so bear with me.comparator-over-maps
is a function that returns a function. The returned function compares the values for the keys provided by theks
parameter using the supplied comparison function,comparison-fn
.
在➊处定义了函数comparator-over-maps
。这可能是最棘手的部分。这个函数接受一个比较函数,comparison-fn
和一个key的集合ks
,返回一个参数是maps
函数。返回的函数用comparison-fn
比较了ks
的每个key对应于maps
的一组值。
You use
comparator-over-maps
to construct themin
andmax
functions ➍, which you’ll use to find the top-left and bottom-right corners of our drawing. Here’smin
in action:
在➍处,用comparator-over-maps
建立了min
和max
函数,用于找到图形的左上角和右下角。这是min
的实际使用:
1 | (min [{:lat 1 :lng 3} {:lat 5 :lng 0}]) |
When you call
min
, it callszipmap
, which takes two arguments, both seqs, and returns a new map. The elements of the first seq become the keys, and the elements of the second seq become the values:
min
里调用了zipmap
,作用是接受两个序列,返回一个map。map的key由第一个序列组成,值由第2个序列组成:
1 | (zipmap [:a :b] [1 2]) |
At , the first argument to
zipmap
isks
, so the elements ofks
will be the keys of the returned map. The second argument is the result of the map call at ➌. That map call actually performs the comparison.
zipmap
的第一个参数是ks
,所以ks
的成员是结果map的key。第二个参数是➌处的map调用的结果,这个map调用执行了上面说的比较。
Finally, at ➍ you use
comparator-over-maps
to create the comparison functions. If you think of the drawing as being inscribed in a rectangle,min
is the corner of the rectangle closest to (0, 0) andmax
is the corner farthest from it.
最后在➍处,用comparator-over-maps
创建了比较函数。如果图形是刻在一个长方形上,min
是最接近(0, 0)的角,max
是离它最远的角。
Here’s the next part of the code:
这是其余代码:
1 | (defn translate-to-00 |
translate-to-00
, defined at , works by finding themin
of our locations and subtracting that value from each location. It usesmerge-with
, which works like this:
translate-to-00
找出所有位置的min
,每个位置都减掉它。用到了merge-with
:
1 | (merge-with - {:lat 50 :lng 10} {:lat 5 :lng 5}) |
Then we define the function
scale
at , which multiplies each point by the ratio between the maximum latitude and longitude and the desired height and width.
然后定义了scale
函数,每个位置都与一个比例相乘。经度使用的比例是图形宽度与max
得到的点的宽度比,纬度使用的比例是图形高度与max
的到的点的高度比。
Here’s the rest of the code for svg.clj:
下面是svg.clj的其余代码:
1 | (defn latlng->point |
The functions here are pretty straightforward. They just take
{:lat x :lng y}
maps and transform them so that an SVG can be created.latlng->point
returns a string that can be used to define a point in SVG markup.points
converts a seq oflat/lng
maps into a space-separated string of points.line
returns the SVG markup for a line that connects all given space-separated strings of points.transform
takes a seq of locations, translates them so they start at the point (0, 0), and scales them to the given width and height. Finally,xml
produces the markup for displaying the given locations using SVG.
这些函数都很直观。它们就是接受{:lat x :lng y}
map,然后进行转换用于创建SVG。
With svg.clj all coded up, now make core.clj look like this:
下面是core.clj的代码:
1 | (ns the-divine-cheese-code.core |
Nothing too complicated is going on here. Within
-main
you build up the drawing using thexml
andtemplate
functions, write the drawing to a file withspit
, and then open it withbrowse/browse-url
. You should try that now! Runlein run
and you’ll see something that looks like Figure 6-2.
这里没有太复杂的东西。-main
里用xml
和template
函数建立了图形,用spit
函数把它写入一个文件,然后用browse/browse-url
打开。你应该试试!运行lein run
,你会看到这个:
Wait a minute . . . that looks a lot like . . . that looks a lot like a lambda. Clojure’s logo is a lambda . . . oh my god! Clojure, it was you this whole time!
等等…这看起来像个lambda。Clojure的log是个lambda…天哪!原来弄了半天是你!
Summary
总结
You learned a lot in this chapter. At this point, you should have all the tools you need to start organizing your projects. You now know that namespaces organize maps between symbols and vars, and that vars are references to Clojure objects (data structures, functions, and so on).
def
stores an object and updates the current namespace with a map between a symbol and a var that points to the object. You can create private functions withdefn-
.
这章学了很多。现在你应该拥有了所有组织项目的工具。现在你知道了:命名空间组织符号与变量间的映射,变量是对Clojure对象(数据结构,函数等等)的引用。def
保存了一个对象,并用一个符号与指向那个对象的变量间的映射更新了当前命名空间。用defn-
创建私有函数。
Clojure lets you create namespaces with
create-ns
, but often it’s more useful to usein-ns
, which switches to the namespace as well. You’ll probably only use these functions in the REPL. When you’re in the REPL, you’re always in the current namespace. When you’re defining namespaces in a file rather than the REPL, you should use thens
macro, and there’s a one-to-one relationship between a namespace and its path on the filesystem.
用create-ns
能创建命名空间,但通常in-ns
更有用,因为它还切换到那个命名。这些一般用于REPL,在文件里应该使用ns
宏,命名空间与其文件系统上的路径有一一对应关系。
You can refer to objects in other namespaces by using the fully qualified name, like
cheese.taxonomy/cheddars
.refer
lets you use names from other namespaces without having to fully qualify them, andalias
lets you use a shorter name for a namespace when you’re writing out a fully qualified name.
使用完全限定的名字可以引用其他命名空间的对象,比如cheese.taxonomy/cheddars
。refer
让你能不使用完全限定的名字,alias
让你给一个完全限定的命名空间起一个短的别名。
require
anduse
ensure that a namespace exists and is ready to be used, and optionally let yourefer
andalias
the corresponding namespaces. You should usens
to callrequire
anduse
in your source files. https://gist.github.com/ghoseb/287710/ is a great reference for all the vagaries of usingns
.
require
和use
确保一个命名空间存在并可用,也可以refter
和alias
这个命名空间。源文件里应该用ns
调用require
和use
。这是个很棒的ns
使用参考 https://gist.github.com/ghoseb/287710/
Lastly and most importantly, it ain’t easy being cheesy.
最后,也是最重要的,干酪真不容易。
译文结束。