热门标签 | 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容器没有任何关系。



推荐阅读
  • Hibernate全自动全映射ORM框架,旨在消除sql,是一个持久层的ORM框架1)、基础概念DAO(DataAccessorOb ... [详细]
  • 解析Java虚拟机HotSpot中的GC算法实现
    本文探讨了Java虚拟机(JVM)中HotSpot实现的垃圾回收(GC)算法,重点介绍了根节点枚举、安全点及安全区域的概念和技术细节,以及这些机制如何影响GC的效率和准确性。 ... [详细]
  • Java虚拟机及其发展历程
    Java虚拟机(JVM)是每个Java开发者日常工作中不可或缺的一部分,但其背后的运作机制却往往显得神秘莫测。本文将探讨Java及其虚拟机的发展历程,帮助读者深入了解这一关键技术。 ... [详细]
  • 1、编写一个Java程序在屏幕上输出“你好!”。programmenameHelloworld.javapublicclassHelloworld{publicst ... [详细]
  • 想把一组chara[4096]的数组拷贝到shortb[6][256]中,尝试过用循环移位的方式,还用中间变量shortc[2048]的方式。得出的结论:1.移位方式效率最低2. ... [详细]
  • 七大策略降低云上MySQL成本
    在全球经济放缓和通胀压力下,降低云环境中MySQL数据库的运行成本成为企业关注的重点。本文提供了一系列实用技巧,旨在帮助企业有效控制成本,同时保持高效运作。 ... [详细]
  • H5技术实现经典游戏《贪吃蛇》
    本文将分享一个使用HTML5技术实现的经典小游戏——《贪吃蛇》。通过H5技术,我们将探讨如何构建这款游戏的两种主要玩法:积分闯关和无尽模式。 ... [详细]
  • 二维码的实现与应用
    本文介绍了二维码的基本概念、分类及其优缺点,并详细描述了如何使用Java编程语言结合第三方库(如ZXing和qrcode.jar)来实现二维码的生成与解析。 ... [详细]
  • JUnit下的测试和suite
    nsitionalENhttp:www.w3.orgTRxhtml1DTDxhtml1-transitional.dtd ... [详细]
  • 数据类型--char一、char1.1char占用2个字节char取值范围:【0~65535】char采用unicode编码方式char类型的字面量用单引号括起来char可以存储一 ... [详细]
  • 在OpenCV 3.1.0中实现SIFT与SURF特征检测
    本文介绍如何在OpenCV 3.1.0版本中通过Python 2.7环境使用SIFT和SURF算法进行图像特征点检测。由于这些高级功能在OpenCV 3.0.0及更高版本中被移至额外的contrib模块,因此需要特别处理才能正常使用。 ... [详细]
  • 本文介绍了一种方法,通过使用Python的ctypes库来调用C++代码。具体实例为实现一个简单的加法器,并详细说明了从编写C++代码到编译及最终在Python中调用的全过程。 ... [详细]
  • protobuf 使用心得:解析与编码陷阱
    本文记录了一次在广告系统中使用protobuf进行数据交换时遇到的问题及其解决过程。通过这次经历,我们将探讨protobuf的特性和编码机制,帮助开发者避免类似的陷阱。 ... [详细]
  • HTML:  将文件拖拽到此区域 ... [详细]
  • 流处理中的计数挑战与解决方案
    本文探讨了在流处理中进行计数的各种技术和挑战,并基于作者在2016年圣何塞举行的Hadoop World大会上的演讲进行了深入分析。文章不仅介绍了传统批处理和Lambda架构的局限性,还详细探讨了流处理架构的优势及其在现代大数据应用中的重要作用。 ... [详细]
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社区 版权所有