practical_clojure chapter2 clojure的环境

Clojure的"Hello,world"

    在配置了java 环境的情况下,在命令行启动Clojure REPL的命令是:

 java -jar clojure-1.0.0.jar

    例子如下:

D:\developTools\clojure-1.3.0>java -jar clojure.jar
Clojure 1.3.0

user=>


    这样就表明REPL已经准备好等待接收输入了:

user=> (println "Hello World")


    按照以上输入并回车,就能够得到结果:

Hello World
nil
user=>


    这里到底发生了什么,REPL这个首字母缩写已经给出了线索:

  • Read:Clojure读取了你打出的字符(println "Hello World"),然后将它解析为Clojure的一个form,确保这在Clojure中语法有效。
  • Evaluate:Clojure编译并执行这个解析出来的form。在上面的例子中,表现为调用了一个函数println,并将"Hello World"字符串作为参数传给它。Clojure执行了这个函数,表现为往标准系统输出中打印了"Hello World"
  • Print:Clojure打印出了println函数的返回值。在上面,显示的是nil(等同于java 中的null,意思是没有任何返回值),因为println本身是个没有返回值的函数
  • Loop:Clojure又回到了输入提示user=>,等待用户键入其他form

    这跟其他大部分的编程语言有点不一样,在其他的语言中,编写、编译、执行是有着严格的顺序。Clojure运行用户按照自己的意愿分离这些步骤。但是大部分的开发人员宁愿采用REPL去整合开发工作,在同一时间去编写和运行代码。这能极大提升开发效率。

    (中略)

    ......在REPL中执行了一行代码时,并不仅仅是执行了它,还包括编译,将执行的代码添加到程序状态中,和之前运行的代码在同一个根目录下。


Clojure Forms

    Clojure程序的基本单位不是一行、一个关键字或者一个类,而是一个form。在Clojure中,form是能够被调用并返回一个值的任意单位的代码。当用户在REPL中输入一些东西,输入的东西必须是一个有效的form,而Clojure的代码源文件中包含了一系列的form。现存四种基本form类型:

  • Literals(字面值):Literals是直接返回自己值的form。Literals的例子有字符串、数字或者用户直接输入的字母。用户可以通过这样的方式来验证Literals是否直接返回自身的值:

          user=> "I'm a string! "
          "I'm a string!"

  • Symbols(符号):符号是用于关联到值。但是不同于一般编程语言的变量,在Clojure里面,Symbols用来标识函数的参数,全局或者局部的值定义。Symbols以及它的处理会在接下来的章节中进一步讨论。


  • Composite Forms(组合form):Composite Forms使用圆括号对、方括号对以及花括号对来包装其他的form。解析时,它们的值取决于它们具体属于那种类型,方括号对被解析成一个vector ,花括号对被解析成一个map。Chapter 4 会细节讨论这些东西。

          在Clojure和Lisp中,圆括号对声明了一个list,而这个list是有着特殊含义的。它代表了对函数的调用,list中的第一个元素是被调用的函数名,而剩下的元素都是传递给函数的参数。比如 (A B C) 代表,调用了函数A,将B和C作为参数传递给A,在其他语言中,一般的写法是:A(B,C)。

          在没有Lisp背景的开发人员来看,这简直是火星文。整个程序都是圆括号,括号中套括号,诸如此类。代码是数据,数据也能是代码。好吧,到Chapter 12时,我们再来看这样的结构怎么能轻松地创建和编写程序。


  • Special Forms(特殊form):Special Forms是Composite Forms中的特例。在大部分场合下,它们跟函数调用是相同的。区别在于Special Forms不是用户在哪儿创建的,而是内建在Clojure中的。

          Special Forms是Clojure中最基础的块,用于控制程序流转、绑定变量和定义函数之类。需要注意的是,就像函数调用一样,如果在圆括号内第一个元素标识了这是一个Special Forms,那么其他form会象参数一样被传递给它。为了理解这个东西,我们把Hello world搞复杂一点,采用两个form来构造它:

user=> (def message "Hello, World!")

这是第一个


user=> (println message)

这是第二个


输入还是跟之前一样:

Hello, World
nil


          这个只有两个form组合的简单程序,实际上已经包含了刚刚我们提到的所有类型form。

          分析一下第一个form,(def message "Hello, World!"),首先,你能看到它是由圆括号闭合的。因此,它是一个list,能够被解析成一个函数体或者是Special Forms。在这个list里面有三个元素: def, message 和"Hello, World!"。list中的第一个元素,def,将调用一个函数或者一个 Special Form。而def是一个Special Form。但是跟函数一样,它接收两个参数--一个是定义的变量,一个是即将跟变量绑定的值。执行这个form会创建了一个变量,这个变量在"Hello, World!"的值和messag符号之间建立了绑定。

          第二个form(println message),仍然是个list,不过这次它是一个普通的函数调用。它由两个form组成--每个都是一个符号。println符号代表调用了println函数,message符号的值是"Hello, World!",因为之前我们已经做过这个绑定了。

          最后的结果,跟之前我们的Hello world一样:println函数接收了一个"Hello, World!"参数被调用。



编写和运行源文件

    就像在REPL中一样,在实际开发中同样需要“一次编写,到处运行”,而不需要重复造轮子。

    按惯例,Clojure的源文件采用.clj后缀。在一个普通的Clojure程序中,不需要专门去编译你的源文件--它们在被加载的时候会被自动编译,就象之前你已经输入到REPL里面一样。如果你需要预编译你的源文件为java 标准的.class,这也是没有问题的,请采用Clojures AOT(Ahead Of Time)功能,这将在Chapter 10中讨论。

    为了从*.clj源文件中运行Hello world例子,首先用任何文本编辑器新建一个clj文件:

文件名:hello-world.clj

文件内容:
(def message1 "Hello, World!")
(def message2 "I'm running Clojure code from a file.")
(println message1)
(println message2)

    有两种方法运行这个文件。最简单,开发当中最常使用的一种是在REPL中键入命令:

user=> (load-file "c:/hello-world.clj")

    当然,路径需要替换成你自己的文件路径。然后,你会看到以下的结果:

Hello, World!
I'm running Clojure code from a file.
nil

    load-file函数接收了一个参数:文件的物理路径。如果在这个路径中成功获取文件,就顺序执行文件中的内容,就像在REPL中键入的一样,然后返回文件中最后一个form的返回值。执行完毕之后,文件中所有的符号仍然是有效的,在下面我们试试看文件中定义的符号:

user=> message1
"Hello, World!"

    另外一种方式是通过系统命令行去执行一个clj文件。这样子会从java 虚拟机中产生一个新的Clojure运行时实例,然后速度加载文件。这是在开发环境外运行Clojure程序的方法。来个例子如下:

D:\developTools\clojure-1.3.0>java –jar clojure-1.3.0.jar c:/hello-world.clj

    其实,这是标准的java 调用方式。这条命令开启了一个Clojure运行时实例,加载了hello-world.clj文件,顺序执行了文件中的代码。然后你在系统的控制台下面就能看到如下的返回值:

Hello, World!
I'm running Clojure code from a file.


变量、命名空间和环境

    就像Chapter 1提到的,Clojure程序是一个活动的、有机的实体,在改变它的时候不需要关闭或者重启。这主要归功于REPL的存在,在REPL中能够在已经存在的程序上下文中继续执行其他form。但是这具体是怎么实现的?

    当你启动一个Clojure程序的时候,不管是直接打开REPL控制台还是加载的一个源文件,都是创建了一个新的全局环境。这个环境会一直持续到程序结束,它里面包含了程序运行的所有必需东西,包含全局变量。当你使用def定义一个变量或者一个函数的时候,这个变量或者函数就会被添加或者保留到环境中去。在变量或者函数初始化完成之后,从当前环境的任何地方去调用它们都是有效的。

    采用def special form,可以创建一个变量并将它绑定到某个符号。

(def var-name var-value)

    var-name是所创建变量的名字,var-value则是变量值。 var-value可以是任意的Clojure form,在它执行后将返回值绑定在变量上。然后,不管在环境中什么时候调用变量,它都会返回绑定的值。

注意: 保证以正确的顺序建立程序中的依赖关系。因为在Clojure程序中引用变量时,变量必须已经被定义。通常来说,不会发生这个问题,但是你在REPL中写久了或许会搞出这个问题来。

    尽管有许多的相似性,但Clojure中的变量不同于其他程序语言。最重要的是,一旦定义完成,变量是不允许改动的--至少,在正常运行的程序中不允许。这是真的,如果你使用def去改动一个已经绑定了的变量,它的值会被改动而且后面的执行调用都会获得新的值。然而,这不是线程安全的,无论如何,def仅仅能被用来定义全局变量,而易变的全局变量对于你的程序来说可不是一个好消息,即使你已经让程序可以运行了。如果你需要改变程序中的某些值,全局变量或者其他,你应该采用Clojure中的线程安全类型,而不是重新定义符号。

    重新定义已存在的值的合适用法是:在程序运行中手工修改或者更新程序。这个可以让你动态地修改程序而无须重启。不要把能重新定义的变量来作为程序状态,这样在多线程下是极其不安全的。


符号和符号处理

    在Clojure中,符号是非常普遍的,所以值得花一点时间来了解它们究竟是什么和怎样工作的。广义下,符号是关联到值的一个标识符。它能够被创建于本地或者全局。基本上你在Clojure程序中看到的既不是文字也不是语法符号(引号、圆括号、方括号、花括号等等)的东西,那可能都是符号。这个概念基本覆盖了其他语言中的变量,但是更好处理些:

  • 在Clojure中,所有函数名都是符号。当在程序中调用函数时,首先处理符号以获取函数,然后再调用函数。
  • 大部分的操作(比较、计算等)都是符号,关联到一个特殊的、内建的、优化的函数。它们如同其他函数一般被调用,而且经过了特殊的性能优化。
  • 宏的名字也是符号。现状不谈细节,宏也很像函数,只不过不是在运行时被调用而是编译时。Chapter 12 会对此有详尽的讨论。

    符号名是大小写敏感的,用户定义的符号名有以下限制:

  • 可以用任何的字母或者数字,以及*, +, !, -, _, 和?。
  • 不能以数字开头
  • 可以包含冒号,但是不能以冒号作为开始或者结束,且不能重复。

    根据惯例,Clojure中的符号通常是小写的,词之间用-分隔,常量或者全局变量通常采用*作为开始和结束。比如:(def *pi* 3.14159)。


    在调用符号时,具体怎么处理取决于符号的作用域以及它关联的是一个用户自定义form或者是特殊form或者是内建form。

    Clojure采用以下几步去处理符号:

    1、如果Clojure发现符号对应的是一个特殊form,那么就按照定义执行

    2、然后,Clojure如果发现符号是本地绑定的,通常的,本地绑定代表着符号是一个函数的参数或者是采用let定义的。如果根据符号找到一个本地的值,就直接使用它。注意,这意味着如果一个本地定义的符号和某个全局变量的名字相同,调用时会直接返回本地符号的值。

    3、Clojure搜寻整个环境去找符号名所对应的变量,然后返回这个变量的值。

    4、如果之前这几步都没有找到这个符号对应的值,Clojure会返回一个错误:java.lang.Exception: unable to resolve symbol <symbol> in this context (NO_SOURCE_FILE:0)。其中的NO_SOURCE_FILE会被替换为实际的源文件名,除非你是在REPL下运行的。


命名空间

    当你使用def定义一个变量时,你建立了一个全局绑定在符号名和值之间。然而,全局变量长期被暴露在外并非一个好主意。在一个大的程序中,很容易出现全局变量名无意冲突的情况,这样就会导致极大的问题且不易调试。

    由于这个原因,Clojure中所有的变量都被命名空间划分开来。每个变量都拥有命名空间作为它名字的一部分。在使用一个引用到变量的符号时,你能够在变量名前面加上反斜杠以及命名空间的名字。

    让我们看看在REPL中定义变量的方式:

user=> (def first-name "Luke")
#'user/first-name

user=> user/first-name
"Luke"

    注意提示符: user=>,其中提示符user就代表了当前的命名空间。如果你工作在不同的工作空间,你就会看到不同的显示。user命名空间并没有什么特别的,它只不过是默认的而已。你实际上并没有定义first-name,你定义的是user/first-name,在调用时也需要使用user/first-name来调用。如果当前就在user命名空间下,可以直接使用first-name来调用。

    声明一个命名空间,采用ns。ns接收一系列的参数,有些相当先进。最简单的形式,是传递一个参数给ns,这个参数即是命名空间的名字。如果这个命名空间还不存在,会创建一个新的命名空间,把这个命名空间设为当前命名空间。如果已经有了这个名字的命名空间,则只会切换到已存在的命名空间下。

user=> (ns new-namespace)
nil
new-namespace=>

    如上,如果我们这个时候再定义一个变量的话,变量会被放在new-namespace下面,而不是user。

    从另一个命名空间引用变量时,简单地使用全限定名称就可以了。观察如下的例子:

user=> (def my-number 5)
#'user/my-number
user=> (ns other-namespace)
nil
other-namespace=> my-number
java.lang.Exception: Unable to resolve symbol: my-number in this context...
other-namespace=> user/my-number
5

    这儿我们首先在默认的user命名空间下定义了一个变量my-number,然后,创建了一个新的命名空间并转向它。这时如果调用my-number,会抛出一个错误,在当前的命名空间中没有这个变量。当我们使用全限定的变量名user/my-number时,就返回之前绑定的值了。

    有时候,你严重依赖某个命名空间,每次使用那个命名空间中的变量都需要写出全限定名非常麻烦。为了解决这种场景,Clojure允许一个命名空间包含另一个命名空间,通过使用ns的:use参数,比如,定义一个命名空间引入了Clojure的内建空间XML类库:

user=> (ns my-namespace
(:use clojure.xml))
my-namespace=>

    现在,XML相关的所有符号都可以在my-namespace中使用了。form (:use clojure.xml)指定了clojure.xml命名空间被加载,而这个命名空间中所有定义的符号都能够在my-namespace中使用。这在依赖管理中同样非常有用,比起需要使用的时候才手工加载clojure.xml命名空间,你可以使用:use在声明当前命名空间的时候就引入这个依赖。这样子,如果它现在还没有加载,Clojure会把它作为命名空间的一部分来加载,保证了在新的命名空间中它是可用的。

    除了:use之外,Clojure为ns还提供了另外一个关键字:require,用法和:use基本上一致,区别在于:require仅仅保证需要的命名空间被加载且是有效的,但并不实际引入符号。你也可以使用:require指定一组需要引入的命名空间。

user=> (ns my-namespace
(:require clojure.xml
clojure.set))
my-namespace=>

    此外,你还可以将命名空间放入方括号中,然后使用:as关键字为命名空间制定一个短一点的别名:

user=> (ns my-namespace
(:require [clojure.xml :as xml]))
my-namespace=> xml/parse
my-namespace=> #<xml$parse_7630 clojure.xml$parse_7630@1484105>

    别担心上面这凌乱的输出值,这只是函数的字符串表现,表明了已经引入了xml/parse符号。


    如何使用命名空间来结构化源代码并保证它有组织呢?这并不困难,依照惯例,每个Clojure源文件拥有他自己的命名空间,在任何clojure文件中,:ns 声明应该是第一个form。这样使管理命名空间和文件变得简单。这跟Java中一个类对应一个文件的惯例很相似。实际上,这有助于Java开发人员把命名空间想象成类。命名空间确实如同类一般对代码进行了组织。

    为了帮助在Clojure中使用:use :require获取到命名空间,这儿有个特殊的命名规范。文件中声明的命名空间必须符合文件在类路径中的位置。比如,你拥有一个Clojure源文件在“x/y/z.clj”,它就必须拥有命名空间声明x.y.z。这样,当你调用x.y.z,你就知道文件是在“x/y/z.clj”了。这跟Java的包策略是一致的。

你可能感兴趣的:(Practical)