热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

《Clojure程序设计》——第1章,第1.1节为什么是Clojure

本节书摘来自异步社区《Clojure程序设计》一书中的第1章,第1.1节为什么是Clojure,作者【美】StuartHalloway,AaronBed

本节书摘来自异步社区《Clojure程序设计》一书中的第1章,第1.1节为什么是Clojure,作者 【美】Stuart Halloway , Aaron Bedra,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.1 为什么是Clojure
Clojure程序设计
所有Clojure的特色功能,要么简单,要么强大,或两者兼而有之。下面举几个例子。

函数式编程很简单,原因是它将计算的过程与状态及标识隔离开来。优点:函数式程序更容易理解、编写、测试、调优和并行化。
Clojure与Java的互操作极为强大,允许你直接访问Java平台的语义。优点:你能拥有与Java等同的性能和语义。最重要的是,你不必为了获得这点额外的能力而“下降”到一门低级别的语言。
Lisp的简单在于两个关键方面:它将代码的读取与求值分开了,并且语法仅由少数几个正交的部分构成。优点:能用语法抽象来捕获设计模式;此外,当需要的时候,S表达式(S-expressions)能成为XML、JSON或是SQL。
Lisp也很强大,它在运行期提供了一个编译器和宏(macro)系统。优点:Lisp具有晚绑定的决策能力,并且很容易定制领域特定语言(DSL,Domain Specific Language)。
Clojure的时间模型很简单,将值、标识、状态和时间相互分离。优点:程序可以放心地感知并记住信息,完全不必担心在这段时间里,有人正打算对其乱涂乱画一番。
协议(Protocols)很简单,将多态性(polymorphism)和派生(derivation)分离。优点:不必纠结于设计模式,或是依赖于脆弱的猴子补丁(monkey patching),你就能得到既安全又极富扩展性的类型与抽象。
这个功能列表可以作为本书剩余部分的路线图,所以,即便此刻你尚无法充分理解每个小细节,也不必太过忧虑。上面的每个特性,都分别用了整整一章来加以详述。

让我们构建一个小型的应用,看看其中一些特性是如何运作的。沿途你将学会如何加载并执行那些较大的示例,本书的后半部分会用到它们。

1.1.1 Clojure非常优雅
Clojure高信号,低噪音。因此,Clojure程序都非常简短。短小的程序,无论是构建、部署,还是维护,都要便宜得多1。尤其当程序是简明的(concise)而不仅仅是简短(terse)的时候就更是如此了。举个例子,考虑下面这段来自于Apache Commons的Java代码。

data/snippets/isBlank.java
public class StringUtils {public static boolean isBlank(String str) {int strLen;if (str == null || (strLen = str.length()) == 0) {return true;}for (int i = 0; i }

isBlank()方法用于检查目标字符串是否是空白的:没有任何字符,或者只包含空格符。这里是Clojure的类似实现。

src/examples/introduction.clj
(defn blank? [str](every? #(Character/isWhitespace %) str))

Clojure版本要短得多。但更重要的是,它更加简单:没有变量,没有可变状态,也没有分支结构。这可能要归功于高阶函数(higherorder functions)。高阶函数本身也是一个函数,它接受其他函数作为参数;也可以把函数作为返回值。every?函数接受一个函数f和一个容器(collection)2 c作为它的参数,对于容器c中的每个元素,如果函数f都返回真的话,every?函数也就返回真。

由于Clojure的这个版本没有分支结构,所以无论是阅读还是测试都更容易。在大一些的程序中,这种优势还将会进一步扩大。而且,简洁的代码也更具可读性。事实上,Clojure的这段程序读起来就像是一份关于何为空白的定义:如果一个字符串中的每个字符都是空格,那么这个字符串就是空白的。这要比一般的方法好太多了,在那些方法中,对空白的定义被隐藏在了由循环和分支语句组成的实现细节背后。

另外一个例子,考虑用Java定义一个微不足道的Person类。

data/snippets/Person.java
public class Person {private String firstName;private String lastName;public Person(String firstName, String lastName) {this.firstName = firstName;this.lastName = lastName;}public String getFirstName() {return firstName;}public void setFirstName(String firstName) {this.firstName = firstName;}public String getLastName() {return lastName;}public void setLastName(String lastName) {this.lastName = lastName;}
}

在Clojure中,用一行代码就可以定义这个Person。

(defrecord Person [first-name last-name])

然后像下面这样使用:

(def foo (->Person "Aaron" "Bedra"))
-> #’user/foo
foo
-> #:user.Person{:first-name "Aaron", :last-name "Bedra"}

在第6.3节“协议”中,包含了defrecord及其相关函数的介绍。

除代码短了一个数量级以外,Clojure采用的方法还有一处不同:Clojure版本的Person是不可变的。不可变数据结构生来就是线程安全的,Clojure中可以通过使用引用、代理和原子来更新数据,这些内容将在第5章“状态”中详加讨论。正因为记录(record)是不可变的,Clojure也就自动提供了正确的hashCode()和equals()实现。

Clojure内建了大量优雅的特性,但倘若你发现还是遗漏了某样东西的话,你可以自己添上,这完全要归功于Lisp的强大。

1.1.2 Clojure是Lisp的再度崛起
Clojure是一种Lisp方言。数十年来,拥护者们指出了Lisp与其他语言相比的诸多优点。但同时,Lisp一统天下的计划看起来却遥遥无期。

如同其他所有的Lisp一样,Clojure也面临着两个挑战。

Clojure必须成功地说服Lisp程序员,作为一种Lisp方言,Clojure包含了Lisp的关键部分。
同时,Clojure还需要成功地赢得广泛的程序员社区支持,而这正是过去那些Lisp的失败之处。
为了应对这些挑战,Clojure提供了Lisp元编程能力,与此同时还包含了一整套增强的语法,使得Clojure对于非Lisp程序员而言显得更为友好。

1.1.3 为什么是Lisp
Lisp的语言核心非常小,几乎没有什么语法,但却提供了一个强大的宏设施。借助这些特性,你可以根据你的设计需要对Lisp随意地直接定制。这样就不必使用其他那些绕来绕去的方式了。考虑以下Java代码片段。

public class Person {private String firstName;public String getFirstName() {// 以下省略…

在这段代码中,getFirstName()是一个方法(method)。方法具有多态性,可以根据你的需要加以调整。但对于Java而言,示例中其他单词的语义,其解释都是固定的。然而,有时你确实需要改变这些词语的含义。举例来说,你可能会像下面这么做。

重新定义private:对于产品代码保持私有,但允许来自序列化(serialization)和单元测试代码的访问。
重新定义class:自动为每个私有字段都生成getters和setters,除非另有指示。
创建class的一个子类,提供面向生命周期事件的回调钩子。例如,对于“可感知生命周期”的类而言,只要创建了这个类的一个实例,就会激发相应的事件。
我们一定见过需要上述特性的程序。由于缺乏这些特性,程序员们不得不去借助一些重复性的、容易出错的变通方法。结果是,人们在这上面白白浪费了数百万行的代码,而罪魁祸首就是编程语言中类似特性的缺失。

对大多数编程语言而言,你只能祈求语言的实现者们尽快增加上面提到的这类特性。但在Clojure中,你能凭借宏来自行添加属于你自己的语言特性(第7章“宏”)。事实上,Clojure本身就是用宏来进行扩建的,比如defrecord。

(defrecord name [arg1 arg2 arg3])

如果你需要的语义与此不同,写一个你自己的宏就行。比如你想得到记录的一个变种,它具备强类型并具有可选的空字段校验能力,你可以创建自己的defrecord宏。这个新的defrecord用法如下。

(defrecord name [Type :arg1 Type :arg2 Type :arg3]:allow-nulls false)

这种对语言进行再编程,从而改变语言自身的能力,是Lisp的独门优势。下面用不同的方式来描述这一思想。

Lisp具有同像性(homoiconic)3。也就是说,Lisp代码其实就是Lisp数据。这样就很容易让程序自己去编写其他的程序。
这就是语言的全部,且始终如此。保罗·格雷厄姆在其散文《书呆子的复仇》4中,解释了为什么这会如此的强大。
Lisp语法也废除了运算符优先级和结合性的规则。翻遍本书的任何一个角落,你都不会看到用来说明运算符优先级或结合性的表格。凭借完全的括号表示法,就能避免产生任何这方面的歧义。

简单、整齐的Lisp语法也存在负面因素,至少对于初学者而言,成堆的括号,以及将列表作为核心数据类型都会成为一种障碍。为此,Clojure提供了有趣的功能组合,对于非Lisp程序员而言,这个Lisp显得要亲切得多。

1.1.4 它是Lisp,但括号少了
对于来自其他Lisp方言的程序员来说,Clojure的优势显而易见。

Clojure泛化了Lisp的物理列表,将其抽象为序列(sequence)。这样既保留了列表的强大能力,同时还将这种能力扩展到了其他各种类型的数据结构。
依托于Java虚拟机,Clojure提供了一个范围广泛的标准库及部署平台。
Clojure提供的符号解析和语法引述(syntax quoting)方式,使得编写许多普通宏的时候更加容易了。
许多Clojure程序员可能会是Lisp的新手,他们也许听说过诸多关于Lisp括号的可怕传言。是的,Clojure保留了括号表示法(当然也保留了Lisp的强大!),但在以下方面对传统Lisp语法进行了改进。

在Clojure中,除列表之外,还提供了更为便利的正则表达式、映射表、集合,向量和元数据等多种数据结构的字面表示语法。这些特性使得Clojure代码相比其他多数Lisp语言而言,过度列表化(listy)的症状要轻很多。例如,Clojure函数的参数是在一个向量([])中指定的,而不是使用列表(())。

src/examples/introduction.clj
(defn hello-world [username](println (format "Hello, %s" username)))

向量令参数列表变得非常醒目,也使得Clojure的函数定义更易于阅读。

与大多数Lisp语言不同,在Clojure中,逗号就是空格。
; 这让向量看起来就像是其他语言中的数组一样。

[1, 2, 3, 4]
-> [1 2 3 4]

地道的Clojure不会内联不必要括号。考虑一下在Common Lisp和Clojure中都有的cond宏。cond对一组成对的“测试/结果”逐个求值,当遇到第一个求值结果为真的测试时,返回其对应的结果。Common Lisp中,每一对“测试/结果”都得像下面这样,用括号进行分组。

; Common Lisp cond
(cond ((= x 10) "equal")((> x 10) "more"))

而在Clojure中则避免了额外的括号。

; Clojure cond
(cond (= x 10) "equal"(> x 10) "more")

这是一种审美决定,且双方都各有其支持者。但重点在于,Clojure获得了在不减损Lisp威力的前提下,尽可能减少过度列表化的机会。

Clojure是一种卓越的Lisp方言,无论对于Lisp专家,还是Lisp新手,皆是如此。

1.1.5 Clojure是函数式语言
Clojure虽然是一种函数式语言,但不像Haskell那样纯粹。函数式编程语言具有下列属性。

函数是一等公民。换言之,函数能在运行期间被创建,被当做参数传递,被用作返回值,并且能像其他数据类型那样,被用于各种用途。
数据是不可变的。
函数是纯粹的,也就是说,它们不会造成任何副作用。
对许多任务而言,函数式程序更容易理解,不容易出错,且更利于重用。例如,下面这个小程序从乐曲数据库中,查询有哪些作曲家创作了《Requiem(安魂曲)》。

(for [c compositions :when (= "Requiem" (:name c))](:composer c))
-> ("W. A. Mozart" "Giuseppe Verdi")

这里的for,并不意味着引入了循环,而是进行了一次列表解析(list comprehension)。所以,这段代码应该这么读:“对于乐曲库中的每支乐曲c,当c的名称是《Requiem》时,则获取c的作曲家信息”。本书第3.2.4小节“序列转换”中有关于列表解析的完整讨论。

这个例子的可取之处有以下4方面:

非常简单,没有任何循环结构、变量或是可变的状态;
线程安全,不需要锁机制即可得到保证;
可并行化,无需修改代码,你就可以将单独的步骤转移至多个线程;
非常通用,乐曲库可以是一个普通集合、XML或是一个数据库结果集。
这里,函数式程序与命令式程序形成鲜明对比,在命令式程序中,是用显式的语句来改变程序状态的。大多数面向对象程序都是采用命令式风格写就的,在前面列出的这几方面,它们劣势尽显(关于函数式和命令式风格的逐项对比,请阅读2.7节)。

如今人们已经知道了函数式语言的优势。然而,像Haskell那样的纯函数式语言却没能接管世界,这是因为开发者们发现,纯粹的函数式观点无法轻易地解决所有问题。

与过去的那些函数式语言相比,有4个原因使得Clojure能够吸引更多的注意。

对函数式编程的需要,比以往任何时候都显得更加迫切。规模庞大的多核硬件已指日可待,函数式语言提供了一种清晰的方式对其加以利用。本书第4章“函数式编程”详细讨论了这个话题。
当确实需要对状态进行修改时,纯粹的函数式编程语言就显得颇为尴尬了。Clojure则通过软事务内存(STM,software transactional memory)及引用、代理、原子和动态绑定,提供了结构良好的机制用于处理可变状态。
许多函数式语言都是基于静态类型的。而Clojure的动态类型系统,使得程序员学习函数式编程更加容易。
Clojure的Java调用方式是非函数式的。当你调用Java程序时,你会进入那个熟悉的,可变的世界。这为函数式编程的初学者提供了一个舒适的港湾,此外当你需要时,这也是能够提供函数式风格替代品的务实之选。第9章“极尽Java之所能”详细讨论了关于Java调用方面的内容。
Clojure中不必显式锁定,就允许并发地更改状态。这种方式是Clojure函数式核心的有力补充。

1.1.6 Clojure简化了并发编程
Clojure支持函数式编程,使得编写线程安全的代码非常容易。由于不可变数据结构在任何时候都不会被修改,因此避免了数据会被另外一个线程破坏的危险。

然而,仅仅是函数式编程,还不足以体现Clojure对并发程序支持之卓越。当你需要引用可变数据时,Clojure会通过软事务内存对其加以保护。在线程安全方面,相比Java提供的锁定机制,软事务内存是一种更高级的方法。你可以借助事务来保护共享状态,而不是去琢磨那些既脆弱,又易于出错的锁定策略。源于数据库方面的经验,很多程序员对何为事务早就了然于胸,所以这也是一种更富成效的做法。

例如,下面的代码创建了一个线程安全的内存数据库用于存放账号。

(def accounts (ref #{}))
(defrecord Account [id balance])

ref函数创建了一个引用,代表数据库的当前状态,这个引用会得到事务的保护。更新操作实在是微不足道。下列代码向数据库中添加一个新的账号。

(dosync(alter accounts conj (->Account "CLJ" 1000.00)))

dosync开启了一个事务,允许对accounts进行更新。这样既确保了线程安全,同时也比锁机制更容易使用。得益于事务,你不必再操心应该锁定哪些对象,以及应该以什么顺序来锁定等等问题。在一些常见的使用场景中,因为读取操作不会被阻塞,所以事务机制能够非常高效地运转。

虽然这是个微不足道的例子,但其展现的技术是通用的,完全可用于解决现实世界中的问题。请参阅第5章“状态”,那里有更多关于Clojure中并发及软事务内存方面的讨论。

1.1.7 Clojure与Java虚拟机彼此亲密无间
从Clojure访问Java,清晰、简单、直接。你能直接调用任何JavaAPI。

(System/getProperties)
-> {java.runtime.name=Java(TM) SE Runtime Environment
... many more ...

Clojure为调用Java提供了很多语法糖。我们不需要在这里深入过多细节(参阅第2.5节“调用Java”),但请注意,下面的代码中,Clojure的那个版本无论是点号(.),还是括号(()),数量都比Java版本要少。

// Java
"hello".getClass().getProtectionDomain()
; Clojure
(.. "hello" getClass getProtectionDomain)

Clojure提供了简单的函数用于实现Java接口,以及从Java基类派生。此外,Clojure的所有函数都实现了Callable和Runnable接口。这使得采用下面所示的匿名函数来构建Java线程竟然如此轻松。

(.start (new Thread (fn [](println "Hello" (Thread/currentThread)))))
-> Hello #

这里有个有趣之处,就是Clojure打印Java对象实例的方式。Thread是这个实例的类名,然后Thread[Thread-0,5,main]是这个实例的toString方法返回值。

注意,前例中的这个新线程会持续运行直至完成,但其输出可能会以某种奇怪的方式,同REPL的提示符产生交错现象。但这并非Clojure的问题,只不过是有多个线程同时向输出流进行写入数据的结果罢了。

由于在Clojure中调用Java程序的语法干净而且简单,作为Clojure的惯例,会更加倾向于直接对Java进行调用,而不是把Java隐藏到一层Lisp化的封装背后。

好了,现在你已经看到一些为什么要使用Clojure了,是时候开始编写一些代码了。

1《软件估算:黑匣子揭秘》 [McC06]这是一部重要的著作,里面有越小越便宜的实例。
2译注:本书中将collection译作容器而不是集合,是为了与set类型加以区分。它们与J2EE中的EJB容器或Servlet容器没有任何关系。



推荐阅读
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • Spring框架《一》简介
    Spring框架《一》1.Spring概述1.1简介1.2Spring模板二、IOC容器和Bean1.IOC和DI简介2.三种通过类型获取bean3.给bean的属性赋值3.1依赖 ... [详细]
  • 本文小编为大家详细介绍“Java中的逻辑结构模式有哪些”,内容详细,步骤清晰,细节处理妥当,希望这篇“Java中的逻辑结构模式有哪些”文章能帮 ... [详细]
  • C++基础 | 从C到C++快速过渡
    一、开发环境c++使用的编译器是g& ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 从零学Java(10)之方法详解,喷打野你真的没我6!
    本文介绍了从零学Java系列中的第10篇文章,详解了Java中的方法。同时讨论了打野过程中喷打野的影响,以及金色打野刀对经济的增加和线上队友经济的影响。指出喷打野会导致线上经济的消减和影响队伍的团结。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
author-avatar
1056fgv
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有