【译】Brave Clojure 第六章:组织项目:图书管理员传奇

本文是我对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, and ns
  • 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
2
(ns-name *ns*)
; => user

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 like user=>.

例如,启动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]), both map and inc 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 like map, 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 that map 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])里的mapinc都是符号。符号是Clojure的数据类型,下章会详细解释(点此查看)。现在要知道的就是:当你使用map这样的符号时,Clojure从当前命名空间找到对应的变量,得到一个架子地址,从那个架子取回你要的物品,即map指向的函数。如果想要使用符号本身,必须引用符号。任何被引用的形式被Clojure当作数据而不对其求值。如下所示:

1
2
3
4
5
6
7
8
9
10
11
➊ inc
; => #<core$inc clojure.core$inc@30132014>

➋ 'inc
; => inc

➌ (map inc [1 2])
; => (2 3)

➍ '(map inc [1 2])
; => (map inc [1 2])

When you evaluate inc in the REPL at ➊, it prints out the textual representation of the function that inc refers to. Next, you quote inc at ➋, so the result is the symbol inc. Then, you evaluate a familiar map application at ➌ and get a familiar result. After that, you quote the entire list data structure at ➍, resulting in an unevaluated list that includes the map symbol, the inc 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 like defn use def under the hood. Here’s an example of def in action:

Clojure里用于存储对象的主要工具是defdefn等其他工具内部也使用def。这是个def的实际使用例子:

1
2
3
4
5
(def great-books ["East of Eden" "The Glass Bead Game"])
; => #'user/great-books

great-books
; => ["East of Eden" "The Glass Bead Game"]

This code tells Clojure:

  1. Update the current namespace’s map with the association between great-books and the var.
  2. Find a free storage shelf.
  3. Store ["East of Eden" "The Glass Bead Game"] on the shelf.
  4. Write the address of the shelf on the var.
  5. Return the var (in this case, #'user/great-books).

这段代码告诉Clojure:

  1. great-booksdef生成的变量的关联更新当前命名空间。
  2. 找到一个未使用的存储架子
  3. ["East of Eden" "The Glass Bead Game"]存放在哪个架子上。
  4. 在变量上写上这个架子的地址
  5. 返回这个变量(#'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
2
(ns-interns *ns*)
; => {great-books #'user/great-books}

You can use the get function to get a specific var:

也可以用get函数得到特定的变量:

1
2
(get (ns-interns *ns*) 'great-books)
; => #'user/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*),可以得到完全映射,命名空间用这个映射查找符号对应的变量。求值结果很长,我就不打印了,但你要试试!

[译者增加]

ns-map

#'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 symbol great-books within the user namespace. We can deref vars to get the objects they point to:

#'user/great-books是变量的读入程序形式。第七章会详细讲解(点此查看)。现在只要知道:用#'可以得到其后符号对应的变量。#'user/great-books可以得到user命名空间下的,great-books符号关联的变量。可以用deref得到变量指向的值:

1
2
(deref #'user/great-books)
; => ["East of Eden" "The Glass Bead Game"]

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
2
great-books
; => ["East of Eden" "The Glass Bead Game"]

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
2
3
(def great-books ["The Power of Bees" "Journey to Upstairs"])
great-books
; => ["The Power of Bees" "Journey to Upstairs"]

bee-power

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 function in-ns, and the macro ns. You’ll mostly use the ns 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
2
user=> (create-ns 'cheese.taxonomy)
; => #<Namespace cheese.taxonomy>

You can use the returned namespace as an argument in a function call:

函数调用中可以把返回的命名空间作为参数:

1
2
user=> (ns-name (create-ns 'cheese.taxonomy))
; => 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. Using in-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
2
user=> (in-ns 'cheese.analysis)
; => #<Namespace 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 the cheese.analysis namespace.

注意现在的REPL提示符是cheese.analysis>,说明你现在确实在这个新命名空间里。现在使用def,会把命名对象存储在cheese.analysis命名空间里。

But what if you want to use functions and data from other name­spaces? To do that, you can use a fully qualified symbol. The general form is namespace/name:

但如果你想使用其他命名空间的函数,该怎么办呢?你可以使用完全限定的符号。通用形式是namespace/name:

1
2
3
4
5
6
cheese.analysis=> (in-ns 'cheese.taxonomy)
cheese.taxonomy=> (def cheddars ["mild" "medium" "strong" "sharp" "extra sharp"])
cheese.taxonomy=> (in-ns 'cheese.analysis)

cheese.analysis=> cheddars
; => Exception: Unable to resolve symbol: cheddars in this context

This creates a new namespace, cheese.taxonomy, defines cheddars in that namespace, and then switches back to the cheese.analysis namespace. You’ll get an exception if you try to refer to the cheese.taxonomy namespace’s cheddars from within cheese.analysis, but using the fully qualified symbol works:

这创建了一个新命名空间,cheese.taxonomy,在这个命名空间里定义了cheddars,然后切换回cheese.analysis命名空间。如果从cheese.analysis里引用cheese.taxonomy命名空间的cheddars,会出现异常。但使用完全限定的符号没问题:

1
2
cheese.analysis=> cheese.taxonomy/cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

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 me join!”—specks of spittle flying from my mouth. “RuntimeException: Unable to resolve symbol: join,” Clojure whines in response. “For the love of brie, just hand me clojure.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 and alias tools that let me yell at it more succinctly.

幸运的是,Clojure提供了referalias,能使代码更简洁。

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
2
3
4
5
6
7
8
9
10
user=> (in-ns 'cheese.taxonomy)
cheese.taxonomy=> (def cheddars ["mild" "medium" "strong" "sharp" "extra sharp"])
cheese.taxonomy=> (def bries ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"])
cheese.taxonomy=> (in-ns 'cheese.analysis)
cheese.analysis=> (clojure.core/refer 'cheese.taxonomy)
cheese.analysis=> bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

cheese.analysis=> cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

This code creates a cheese.taxonomy namespace and two vectors within it: cheddars and bries. Then it creates and moves to a new namespace called cheese.analysis. Calling refer 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,cheddarsbries。然后创建并切换到另一个命名空间cheese.analysis。调用refer加上一个命名空间符号以后,你就可以引用那个命名空间的对象,而不用使用完全限定的符号。这个调用更新了当前命名空间的符号/对象映射。你可以这样看到新条目:

1
2
3
4
5
cheese.analysis=> (clojure.core/get (clojure.core/ns-map clojure.core/*ns*) 'bries)
; => #'cheese.taxonomy/bries

cheese.analysis=> (clojure.core/get (clojure.core/ns-map clojure.core/*ns*) 'cheddars)
; => #'cheese.taxonomy/cheddars

It’s as if Clojure

  1. Calls ns-interns on the cheese.taxonomy namespace
  2. Merges that with the ns-map of the current namespace
  3. Makes the result the new ns-map of the current namespace

就好像Clojure做了如下事情

  1. cheese.taxonomy命名空间上调用ns-interns
  2. 把上面的结果与当前命名空间的ns-map合并
  3. (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’s ns-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
2
3
4
5
cheese.analysis=> (clojure.core/refer 'cheese.taxonomy :only ['bries])
cheese.analysis=> bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]
cheese.analysis=> cheddars
; => RuntimeException: Unable to resolve symbol: cheddars

And here’s :exclude in action:

下面是:exclude的例子:

1
2
3
4
5
cheese.analysis=> (clojure.core/refer 'cheese.taxonomy :exclude ['bries])
cheese.analysis=> bries
; => RuntimeException: Unable to resolve symbol: bries
cheese.analysis=> cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

Lastly, a :rename example:

最后是:rename的例子:

1
2
3
4
5
cheese.analysis=> (clojure.core/refer 'cheese.taxonomy :rename {'bries 'yummy-bries})
cheese.analysis=> bries
; => RuntimeException: Unable to resolve symbol: bries
cheese.analysis=> yummy-bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

Notice that in these last examples we have to use the fully qualified names of all the objects in clojure.core, like clojure.core/ns-map and clojure.core/refer. We didn’t have to do that in the user namespace. That’s because the REPL automatically refers clojure.core within the user 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 seeing clojure.core/refer in the examples, you’ll only see refer.

注意上面的例子使用了clojure.core里的完全限定的名字,比如clojure.core/ns-mapclojure.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
2
3
4
5
(in-ns 'cheese.analysis)
;; Notice the dash after "defn"
(defn- private-function
"Just an example function that does nothing"
[] "sdf")

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
2
3
4
cheese.analysis=> (in-ns 'cheese.taxonomy)
cheese.taxonomy=> (clojure.core/refer-clojure)
➊ cheese.taxonomy=> (cheese.analysis/private-function)
➋ cheese.taxonomy=> (refer 'cheese.analysis :only ['private-function])

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
2
3
cheese.analysis=> (clojure.core/alias 'taxonomy 'cheese.taxonomy)
cheese.analysis=> taxonomy/bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

This code lets us use call symbols from the cheese.taxonomy namespace with the shorter alias taxonomy.

这段代码让我们把taxonomy当作cheese.taxonomy用。

refer and alias are your two basic tools for referring to objects outside your current namespace! They’re great aids to REPL development.

referalias是引用当前命名空间外的对象的基本工具。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 and use, and show how to use ns to set up a namespace.

现在已经讲解了Clojure组织系统的构建模块,我会为你展示如何在真实项目里使用它们。我会解释文件路径与命名空间的关系,如何用requireuse加载文件,如何用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
2
3
4
5
6
7
8
9
10
11
12
| .gitignore
| doc
| | intro.md
| project.clj
| README.md
| resources
| src
| | the_divine_cheese_code
| | | core.clj
| test
| | the_divine_cheese_code
| | | core_test.clj

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
2
(ns the-divine-cheese-code.core
(:gen-class))

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 the in-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 file­system. 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, since the-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的文件;corecore.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
2
mkdir src/the_divine_cheese_code/visualization
touch src/the_divine_cheese_code/visualization/svg.clj

Notice that the filesystem path follows these conventions. With the relationship between namespaces and the filesystem down, let’s look at require and use.

注意文件名遵循上面的惯例。知道了命名空间与文件系统的关系,我们来看看requireuse

Requiring and Using Namespaces

请求与使用命名空间

The code in the the-divine-cheese-code.core namespace will use the functions in the namespace the-divine-cheese-code.visualization.svg to create SVG markup. To use svg’s functions, core will have to require 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
2
3
4
5
6
7
8
9
10
(ns the-divine-cheese-code.visualization.svg)

(defn latlng->point
"Convert lat/lng map to comma-separated string"
[latlng]
(str (:lat latlng) "," (:lng latlng)))

(defn points
[locations]
(clojure.string/join " " (map latlng->point locations)))

This defines two functions, latlng->point and points, 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 to require 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 the the-divine-cheese-code.visualization.svg namespace and defines the functions latlng->point and points 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->pointpoints,可以把经度/纬度坐标序列转化成点组成的字符串。要从core.js文件里使用这些函数,必须require它。require接受一个命名空间符号作为参数,确保命名空间存在,并使它处于可用状态。这个例子里,当你调用(require 'the-divine-cheese-code.visualization.svg)时,Clojure读取并求值对应的文件。通过求值这个文件,创建了the-divine-cheese-code.visualization.svg命名空间,并在里面定义了函数latlng->pointpoints。虽然文件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 the heists seq to make core.clj match the listing:

请求这个命名空间后,可以引用它,以避免使用完全限定的函数名。在core.clj里请求the-divine-cheese-code.visualization.svg,并添加heists序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(ns the-divine-cheese-code.core)
;; Ensure that the SVG code is evaluated
(require 'the-divine-cheese-code.visualization.svg)
;; Refer the namespace so that you don't have to use the
;; fully qualified name to reference svg functions
(refer 'the-divine-cheese-code.visualization.svg)

(def heists [{:location "Cologne, Germany"
:cheese-name "Archbishop Hildebold's Cheese Pretzel"
:lat 50.95
:lng 6.97}
{:location "Zurich, Switzerland"
:cheese-name "The Standard Emmental"
:lat 47.37
:lng 8.55}
{:location "Marseille, France"
:cheese-name "Le Fromage de Cosquer"
:lat 43.30
:lng 5.37}
{:location "Zurich, Switzerland"
:cheese-name "The Lesser Emmental"
:lat 47.37
:lng 8.55}
{:location "Vatican City"
:cheese-name "The Cheese of Turin"
:lat 41.90
:lng 12.45}])

(defn -main
[& args]
(println (points heists)))

Now you have a seq of heist locations to work with and you can use functions from the visualization.svg namespace. The main function simply applies the points function to heists. If you run the project with lein 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 loaded the-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 of require as telling Clojure the following:

require的细节有点复杂,你可以认为实际上它让Clojure做了下列事情:

  1. Do nothing if you’ve already called require with this symbol (the-divine-cheese-code.visualization.svg).
  2. 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.
  3. Read and evaluate the contents of that file. Clojure expects the file to declare a namespace corresponding to its path (which ours does).
  1. 如果已经requirethe-divine-cheese-code.visualization.svg,什么都不干。
  2. 否则,用上面描述的规则找到符号对应的文件,src/the_divine_cheese_code/visualization/svg.clj
  3. 读入并求值那个文件内容。

require also lets you alias a namespace when you require it, using :as or alias. This:

使用require时,也可以用:asalias给命名空间起个别名:

1
(require '[the-divine-cheese-code.visualization.svg :as svg])

is equivalent to this:

与下面的代码等效:

1
2
(require 'the-divine-cheese-code.visualization.svg)
(alias 'svg 'the-divine-cheese-code.visualization.svg)

You can now use the aliased namespace:

现在可以使用这个别名:

1
2
(svg/points heists)
; => "50.95,6.97 47.37,8.55 43.3,5.37 47.37,8.55 41.9,12.45"

Clojure provides another shortcut. Instead of calling require and refer separately, the function use does both. It’s frowned upon to use use 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
2
(require 'the-divine-cheese-code.visualization.svg)
(refer '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 with require. This:

require一样,use也可以加上别名。

1
2
3
(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg)
(alias 'svg '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
2
3
4
5
6
(use '[the-divine-cheese-code.visualization.svg :as svg])
(= svg/points points)
; => true

(= svg/latlng->point latlng->point)
; => true

It may seem redundant to alias a namespace with use here because use already refers the namespace (which lets you simply call points instead of svg/points). In certain situations, though, it’s handy because use takes the same options as refer (:only, :exclude, :as, and :rename). You might want to alias a namespace with use when you’ve skipped referring a symbol. You could use this:

使用use后,再加上别名看起来有点多余。但因为userefer一样,也能接受:only,:exclude,:as:rename,所以有些情况下用起来很方便。比如:你可能只引用了部分符号,不使用use可以这么写:

1
2
(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg :only ['points])

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
2
3
4
5
6
7
8
9
10
11
(use '[the-divine-cheese-code.visualization.svg :as svg :only [points]])
(= svg/points points)
; => true

;; We can use the alias to reach latlng->point
svg/latlng->point
; This doesn't throw an exception

;; But we can't use the bare name
latlng->point
; This does throw an exception!

If you try Listing 6-3 in a REPL and latlng->point doesn’t throw an exception, it’s because you referred latlng->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 and use load files and optionally alias or refer their namespaces. As you write Clojure programs and read code written by others, you might encounter even more ways of writing require and use, 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 about require and use should cover 95.3 percent of your needs.

你需要知道的是,requireuse加载文件,并且可选择地对命名空间aliasrefer。如果需要了解更多,可以查看文档( http://clojure.org/reference/libs )。但requireuse能满足大部分需要。

The ns Macro

ns宏

Now it’s time to look at the ns macro. The tools covered so far—in-ns, refer, alias, require, and use—are most often used when you’re playing in the REPL. In your source code files, you’ll typically use the ns 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 one ns call can incorporate require, use, in-ns, alias, and refer.

是时候看ns宏了。到现在讲解的工具-in-ns,refer,alias,requireuse-大多数都用于REPL。在源码文件里,主要使用ns宏,因为它能让你更简洁地使用这些工具,还提供了其他功能。这节里你学习如何用一个ns调用,包含require,use,in-ns,aliasrefer

One useful task ns does is refer the clojure.core namespace by default. That’s why you can call println from within the-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 as refer:

:refer-clojure可以限制引入哪些clojure-core的东西,参数与refer一样:

1
2
(ns the-divine-cheese-code.core
(:refer-clojure :exclude [println]))

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
2
(in-ns 'the-divine-cheese-code.core)
(refer 'clojure.core :exclude ['println])

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 the require function. For example, this:

(:require) 的作用与require函数很像,例如:

1
2
(ns the-divine-cheese-code.core
(:require the-divine-cheese-code.visualization.svg))

is equivalent to this:

与下面代码等效:

1
2
(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)

Notice that in the ns form (unlike the in-ns function call), you don’t have to quote your symbol with '. You never have to quote symbols within ns.

注意,ns里不用像in-ns那样,必须用'引用符号。永远不需要。

You can also alias a library that you require within ns, just like when you call the function. This:

ns里的require也可以加上别名,跟require函数一样,这段代码:

1
2
(ns the-divine-cheese-code.core
(:require [the-divine-cheese-code.visualization.svg :as svg]))

is equivalent to this:

与下面的代码等效:

1
2
(in-ns 'the-divine-cheese-code.core)
(require ['the-divine-cheese-code.visualization.svg :as 'svg])

You can require multiple libraries in a (:require) reference as follows. This:

一个(:require)里可以请求多个库,这个代码:

1
2
3
(ns the-divine-cheese-code.core
(:require [the-divine-cheese-code.visualization.svg :as svg]
[clojure.java.browse :as browse]))

is equivalent to this:

与下面的代码等效:

1
2
3
(in-ns 'the-divine-cheese-code.core)
(require ['the-divine-cheese-code.visualization.svg :as 'svg])
(require ['clojure.java.browse :as 'browse])

However, one difference between the (:require) reference and the require function is that the reference also allows you to refer names. This:

(:require)引用与require函数的一个不同是,前者允许你引用名称。这个代码:

1
2
(ns the-divine-cheese-code.core
(:require [the-divine-cheese-code.visualization.svg :refer [points]]))

is equivalent to this:

与下面的代码等效:

1
2
3
(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg :only ['points])

You can also refer all symbols (notice the :all keyword):

也可以用:all引用所有符号:

1
2
(ns the-divine-cheese-code.core
(:require [the-divine-cheese-code.visualization.svg :refer :all]))

which is the same as doing this:

上面代码与下面代码等效:

1
2
3
(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg)

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
2
(ns the-divine-cheese-code.core
(:use clojure.java.browse))

does this:

与下面代码等效:

1
2
(in-ns 'the-divine-cheese-code.core)
(use 'clojure.java.browse)

whereas this:

这段代码:

1
2
(ns the-divine-cheese-code.core
(:use [clojure.java browse io]))

does this:

与下面代码等效:

1
2
3
(in-ns 'the-divine-cheese-code.core)
(use 'clojure.java.browse)
(use 'clojure.java.io)

Notice that when you follow :use with a vector, it takes the first symbol as the base and then calls use 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

svg-before

svg-before

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
2
3
4
5
6
7
8
9
10
11
12
13
(ns the-divine-cheese-code.visualization.svg
(:require [clojure.string :as s])
(:refer-clojure :exclude [min max]))

➊ (defn comparator-over-maps
[comparison-fn ks]
(fn [maps]
➋ (zipmap ks
➌ (map (fn [k] (apply comparison-fn (map k maps)))
ks))))

➍ (def min (comparator-over-maps clojure.core/min [:lat :lng]))
(def max (comparator-over-maps clojure.core/max [:lat :lng]))

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 the ks 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 the min and max functions ➍, which you’ll use to find the top-left and bottom-right corners of our drawing. Here’s min in action:

在➍处,用comparator-over-maps建立了minmax函数,用于找到图形的左上角和右下角。这是min的实际使用:

1
2
(min [{:lat 1 :lng 3} {:lat 5 :lng 0}])
; => {:lat 1, :lng 0}

When you call min, it calls zipmap , 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
2
(zipmap [:a :b] [1 2])
; => {:a 1 :b 2}

At , the first argument to zipmap is ks, so the elements of ks 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) and max is the corner farthest from it.

最后在➍处,用comparator-over-maps创建了比较函数。如果图形是刻在一个长方形上,min是最接近(0, 0)的角,max是离它最远的角。

Here’s the next part of the code:

这是其余代码:

1
2
3
4
5
6
7
8
9
10
11
(defn translate-to-00
[locations]
(let [mincoords (min locations)]
(map #(merge-with - % mincoords) locations)))

(defn scale
[width height locations]
(let [maxcoords (max locations)
ratio {:lat (/ height (:lat maxcoords))
:lng (/ width (:lng maxcoords))}]
(map #(merge-with * % ratio) locations)))

translate-to-00, defined at , works by finding the min of our locations and subtracting that value from each location. It uses merge-with, which works like this:

translate-to-00找出所有位置的min,每个位置都减掉它。用到了merge-with:

1
2
(merge-with - {:lat 50 :lng 10} {:lat 5 :lng 5})
; => {:lat 45 :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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(defn latlng->point
"Convert lat/lng map to comma-separated string"
[latlng]
(str (:lat latlng) "," (:lng latlng)))

(defn points
"Given a seq of lat/lng maps, return string of points joined by space"
[locations]
(s/join " " (map latlng->point locations)))

(defn line
[points]
(str "<polyline points=\"" points "\" />"))

(defn transform
"Just chains other functions"
[width height locations]
(->> locations
translate-to-00
(scale width height)))

(defn xml
"svg 'template', which also flips the coordinate system"
[width height locations]
(str "<svg height=\"" height "\" width=\"" width "\">"
;; These two <g> tags change the coordinate system so that
;; 0,0 is in the lower-left corner, instead of SVG's default
;; upper-left corner
"<g transform=\"translate(0," height ")\">"
"<g transform=\"rotate(-90)\">"
(-> (transform width height locations)
points
line)
"</g></g>"
"</svg>"))

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 of lat/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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
(ns the-divine-cheese-code.core
(:require [clojure.java.browse :as browse]
[the-divine-cheese-code.visualization.svg :refer [xml]])
(:gen-class))

(def heists [{:location "Cologne, Germany"
:cheese-name "Archbishop Hildebold's Cheese Pretzel"
:lat 50.95
:lng 6.97}
{:location "Zurich, Switzerland"
:cheese-name "The Standard Emmental"
:lat 47.37
:lng 8.55}
{:location "Marseille, France"
:cheese-name "Le Fromage de Cosquer"
:lat 43.30
:lng 5.37}
{:location "Zurich, Switzerland"
:cheese-name "The Lesser Emmental"
:lat 47.37
:lng 8.55}
{:location "Vatican City"
:cheese-name "The Cheese of Turin"
:lat 41.90
:lng 12.45}])

(defn url
[filename]
(str "file:///"
(System/getProperty "user.dir")
"/"
filename))

(defn template
[contents]
(str "<style>polyline { fill:none; stroke:#5881d8; stroke-width:3}</style>"
contents))

(defn -main
[& args]
(let [filename "map.html"]
(->> heists
(xml 50 100)
template
(spit filename))
(browse/browse-url (url filename))))

Nothing too complicated is going on here. Within -main you build up the drawing using the xml and template functions, write the drawing to a file with spit, and then open it with browse/browse-url. You should try that now! Run lein run and you’ll see something that looks like Figure 6-2.

这里没有太复杂的东西。-main里用xmltemplate函数建立了图形,用spit函数把它写入一个文件,然后用browse/browse-url打开。你应该试试!运行lein run,你会看到这个:

lamda

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 with defn-.

这章学了很多。现在你应该拥有了所有组织项目的工具。现在你知道了:命名空间组织符号与变量间的映射,变量是对Clojure对象(数据结构,函数等等)的引用。def保存了一个对象,并用一个符号与指向那个对象的变量间的映射更新了当前命名空间。用defn-创建私有函数。

Clojure lets you create namespaces with create-ns, but often it’s more useful to use in-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 the ns 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, and alias lets you use a shorter name for a namespace when you’re writing out a fully qualified name.

使用完全限定的名字可以引用其他命名空间的对象,比如cheese.taxonomy/cheddarsrefer让你能不使用完全限定的名字,alias让你给一个完全限定的命名空间起一个短的别名。

require and use ensure that a namespace exists and is ready to be used, and optionally let you refer and alias the corresponding namespaces. You should use ns to call require and use in your source files. https://gist.github.com/ghoseb/287710/ is a great reference for all the vagaries of using ns.

requireuse确保一个命名空间存在并可用,也可以refteralias这个命名空间。源文件里应该用ns调用requireuse。这是个很棒的ns使用参考 https://gist.github.com/ghoseb/287710/

Lastly and most importantly, it ain’t easy being cheesy.

最后,也是最重要的,干酪真不容易。

cheese


译文结束。