【试读】《Clojure经典实例》


图灵程序设计丛书 人 民 邮 电 出 版 社 北  京 Clojure Cookbook Recipes for Functional Programming [美]Luke VanderHart [加]Ryan Neufeld 著 王海鹏 徐宏宁 译 Clojure经典实例 Beijing • Cambridge • Farnham • Köln • Sebastopol • Tokyo O’Reilly Media, Inc.授权人民邮电出版社出版 内 容 提 要 本书以具体实例的形式讲解了 Clojure 语言在不同领域的应用,不仅介绍如何运用 Clojure, 而且还展示了很多常见库。书中给出了添加了注释的示例代码,详细分析并解释了数百个真实世界 的编程任务。读者既可通过本书深入了解 Clojure 的精髓,也可将本书用作参考指南,解决具体问题。 本书适合各层次 Clojure 开发人员阅读。 定价:79.00元 读者服务热线:(010)51095186转600 印装质量热线:(010)81055316 反盗版热线:(010)81055315 广告经营许可证:京崇工商广字第 0021 号 著    [美] Luke VanderHart [加] Ryan Neufeld 译    王海鹏 徐宏宁 责任编辑 岳新欣 执行编辑 张 曼 责任印制 杨林杰 人民邮电出版社出版发行  北京市丰台区成寿寺路11号 邮编 100164  电子邮件 315@ptpress.com.cn 网址 http://www.ptpress.com.cn 北京      印刷 开本:800×1000 1/16 印张:26.5 字数:626千字 2015年 8 月第 1 版 印数:1 — 3 500册 2015年 8 月北京第 1次印刷 著作权合同登记号 图字:01-2014-6469号 ◆ ◆ ◆ III 版权声明 © 2014 by Cognitect, Inc. Simplified Chinese Edition, jointly published by O’Reilly M edia,Inc. and Posts & Telecom Press, 2015. Authorized translation of the English edition, 2015 O’Reilly Media, Inc., the owner of all rights to publish and sell the same. All rights reserved including the rights of reproduction in whole or in part in any form. 英文原版由 O’Reilly Media, Inc. 出版,2014。 简体中文版由人民邮电出版社出版, 2015。英文原版的翻译得到 O’Reilly Media, Inc. 的 授权。此简体中文版的出版和销售得到出版权和销售权的所有者——O’Reilly Media, Inc. 的许可。 版权所有,未得书面许可,本书的任何部分和全部不得以任何形式重制。 O’Reilly M edia 通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。 自 1978 年开始,O’Reilly 一直都是前沿发展的见证者和推动者。超级极客们正在开创 着未来 ,而我们关注真正重要的技术趋势——通过放大那些“细微的信号”来刺激社 会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly 的发展充满了对创新的 倡导、创造和发扬光大。 O’Reilly 为软件开发人员带来革命性的“动物书”;创建第一个商业网站( GNN);组 织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了 Make 杂志, 从而成为 DIY 革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。 O’Reilly 的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创 新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly 现在还将先锋专家的 知识传递给普通的计算机用户。无论是通过书籍出版、在线服务或者面授课程,每一 项 O’Reilly 的产品都反映了公司不可动摇的理念——信息是激发创新的力量。 业界评论 “O’Reilly Radar 博客有口皆碑。” ——Wired “O’Reilly 凭 借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。” ——Business 2.0 “O’Reilly Conference 是聚集关键思想领袖的绝对典范。” ——CRN “一本 O’Reilly 的 书就代表一个有用、有前途、需要学习的主题。” ——Irish Times “Tim 是位特立独行的商人,他不光放眼于最长远、最广阔的视野,并且切实地按照 Yo gi Berra 的建议去做了:‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去, Tim 似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。” ——Linux Journal O’Reilly Media, Inc.介绍 v 目录 译者序 .....................................................................................................................................................xi 前言 ........................................................................................................................................................xiii 第 1 章 原生数据 ................................................................................................................................1 1.0 简介 . ............................................................................................................................................1 1.1 改变字符串的大小写 . ................................................................................................................2 1.2 清除字符串中的空白字符 . ........................................................................................................3 1.3 利用部件构建字符串 . ................................................................................................................5 1.4 将字符串作为字符序列 . ............................................................................................................6 1.5 字符与整数的转换 . ...................................................................................................................7 1.6 格式化字符串 . ...........................................................................................................................9 1.7 按模式查找字符串 . ..................................................................................................................11 1.8 利用正则表达式从字符串中取出值 . ......................................................................................12 1.9 对字符串执行查找和替换 . ......................................................................................................13 1.10 将字符串切分成部分 . ............................................................................................................15 1.11 基于数量为字符串加复数 . ....................................................................................................16 1.12 在字符串、符号和关键字之间的转换 . ................................................................................18 1.13 利用非常大或非常小的数来保持精度 . ................................................................................20 1.14 使用有理数 . ............................................................................................................................21 1.15 解析数字 . ...............................................................................................................................23 1.16 数的截断和舍入 . ....................................................................................................................24 1.17 模糊比较 . ................................................................................................................................26 1.18 三角计算 . ................................................................................................................................27 1.19 根据不同的进制输入和输出整数 . ........................................................................................29 vi | 目录 1.20 计算数值集合的统计值 . ........................................................................................................30 1.21 位操作 . ....................................................................................................................................33 1.22 生成随机数 . ............................................................................................................................34 1.23 操作货币 . ................................................................................................................................36 1.24 生成唯一 ID . ...........................................................................................................................37 1.25 得到当前的日期和时间 . ........................................................................................................39 1.26 用字面值来表示日期 . ............................................................................................................40 1.27 利用 clj-time 解析日期和时间 . ..........................................................................................42 1.28 利用 clj-time 格式化日期 . ..................................................................................................43 1.29 比较日期 . ...............................................................................................................................45 1.30 计算时间间隔的长度 . ............................................................................................................46 1.31 生成一系列的日期和时间 . ....................................................................................................48 1.32 利用原生 Java 类型生成一系列日期和时间 . .......................................................................49 1.33 根据日期间的关系取得日期 . ................................................................................................52 1.34 处理时区 . ................................................................................................................................53 1.35 将 Unix 时间戳转换成 Date 对象 . ........................................................................................55 1.36 将 Date 对象转换成 Unix 时间戳 . ........................................................................................56 第 2 章 复合数据 ..............................................................................................................................58 2.0 简介 . ..........................................................................................................................................58 2.1 创建列表 . ..................................................................................................................................59 2.2 从已有的数据结构创建列表 . ..................................................................................................61 2.3 在列表中“添加”一个元素 . ..................................................................................................62 2.4 从列表中“移除”一个元素 . ..................................................................................................63 2.5 测试是否列表 . ..........................................................................................................................64 2.6 创建向量 . ..................................................................................................................................65 2.7 在向量中“添加”一个元素 . ..................................................................................................66 2.8 从向量中“移除”一个元素 . ..................................................................................................67 2.9 取得索引处的值 . ......................................................................................................................68 2.10 设置索引处的值 . ....................................................................................................................70 2.11 创建集 . ....................................................................................................................................71 2.12 在集中添加和移除元素 . .......................................................................................................73 2.13 测试集成员 . ............................................................................................................................74 2.14 使用集操作 . ...........................................................................................................................76 2.15 创建映射表 . ............................................................................................................................77 2.16 从映射表中取得值 . ...............................................................................................................79 2.17 从映射表中同时取出多个键 . ................................................................................................82 2.18 设置映射表中的键 . ...............................................................................................................84 2.19 用复合值作为映射表的键 . ...................................................................................................86 目录 | vii 2.20 将映射表作为序列(或反过来) . .........................................................................................88 2.21 对映射表应用函数 . ...............................................................................................................90 2.22 一个键保存多个值 . ...............................................................................................................92 2.23 合并映射表 . ...........................................................................................................................95 2.24 值的比较与排序 . ...................................................................................................................97 2.25 从集合中移除重复元素 . ......................................................................................................100 2.26 检测集合是否包含几个值中的一个 . ..................................................................................102 2.27 实现定制的数据结构:红黑树(第一部分) . ....................................................................103 2.28 实现定制的数据结构:红黑树(第二部分) . ....................................................................106 第 3 章 广义计算 ............................................................................................................................111 3.0 简介 . ........................................................................................................................................111 3.1 运行最小的 Clojure REPL . ....................................................................................................111 3.2 交互式文档 . ............................................................................................................................112 3.3 探索命名空间 . .......................................................................................................................114 3.4 尝试库而不指明依赖关系 . ....................................................................................................116 3.5 运行 Clojure 程序 . ..................................................................................................................117 3.6 从命令行运行程序 . ...............................................................................................................119 3.7 解析命令行参数 . ....................................................................................................................121 3.8 创建定制的项目模板 . ............................................................................................................124 3.9 构建具有多态行为的函数 . ....................................................................................................128 3.10 扩展内建的类型 . .................................................................................................................133 3.11 用 core.async 解除消费者和生产者的耦合 . .....................................................................135 3.12 用 core.match 为 Clojure 表达式制作解析器 . ...................................................................138 3.13 用 core.logic 查询层级图 . .................................................................................................141 3.14 演奏儿歌 . ..............................................................................................................................146 第 4 章 本地 I/O ............................................................................................................................150 4.0 简介 . ........................................................................................................................................150 4.1 写入 STDOUT 和 STDERR . ..................................................................................................150 4.2 从控制台读入一次击键 . ........................................................................................................152 4.3 执行系统命令 . ........................................................................................................................153 4.4 访问资源文件 . ........................................................................................................................156 4.5 复制文件 . ................................................................................................................................157 4.6 删除文件或目录 . ....................................................................................................................159 4.7 列出目录中的文件 . ................................................................................................................161 4.8 文件的内存映射 . ....................................................................................................................163 4.9 读写文本文件 . ........................................................................................................................164 4.10 使用临时文件 . ......................................................................................................................165 4.11 在任意位置读写文件 . ..........................................................................................................166 viii | 目录 4.12 并行文件处理 . ......................................................................................................................168 4.13 带归约的并行文件处理 . ......................................................................................................170 4.14 读写 Clojure 数据 . ................................................................................................................172 4.15 在配置文件中使用 edn . .......................................................................................................174 4.16 将记录作为 edn 值发布 . ......................................................................................................178 4.17 读取 Clojure 数据时处理未知的带标签字面值 . ................................................................180 4.18 从文件中读取属性 . ..............................................................................................................182 4.19 读写二进制文件 . ..................................................................................................................184 4.20 读写 CSV 数据 . ....................................................................................................................186 4.21 读写压缩文件 . ......................................................................................................................187 4.22 处理 XML 数据 . ...................................................................................................................189 4.23 读写 JSON 数据 . ..................................................................................................................190 4.24 生成 PDF 文件......................................................................................................................192 4.25 生成带可滚动文本的 GUI 窗口 . .........................................................................................196 第 5 章 网络 I/O 和 Web 服务 ...................................................................................................200 5.0 简介 . ........................................................................................................................................200 5.1 发出 HTTP 请求 . ....................................................................................................................200 5.2 执行异步 HTTP 请求 . ............................................................................................................202 5.3 发出 Ping 请求........................................................................................................................204 5.4 取得并解析 RSS 数据 . ...........................................................................................................205 5.5 发送邮件 . ................................................................................................................................206 5.6 用 RabbitMQ 实现队列通信 . .................................................................................................210 5.7 通过 MQTT 与嵌入式设备通信............................................................................................215 5.8 并发使用 ZeroMQ . ................................................................................................................219 5.9 创建 TCP 客户端....................................................................................................................222 5.10 创建 TCP 服务器..................................................................................................................223 5.11 收发 UDP 包 . ........................................................................................................................227 第 6 章 数据库 ................................................................................................................................230 6.0 简介 . ........................................................................................................................................230 6.1 连接 SQL 数据库 . ..................................................................................................................231 6.2 利用连接池连接 SQL 数据库 . ..............................................................................................233 6.3 操作 SQL 数据库 . ..................................................................................................................236 6.4 用 Korma 简化 SQL . .............................................................................................................242 6.5 用 Lucene 进行全文查找 . ......................................................................................................245 6.6 用 ElasticSearch 建立数据索引 . ............................................................................................248 6.7 使用 Cassandra . ......................................................................................................................252 6.8 使用 MongoDB . ......................................................................................................................256 6.9 使用 Redis . ..............................................................................................................................259 目录 | ix 6.10 连接 Datomic 数据库 . ..........................................................................................................262 6.11 为 Datomic 数据库定义数据模式 . ......................................................................................264 6.12 向 Datomic 写入数据 . ..........................................................................................................267 6.13 从 Datomic 数据库中删除数据 . ..........................................................................................270 6.14 尝试 Datomic 事务而不提交 . ..............................................................................................272 6.15 遍历 Datomic 索引 . ..............................................................................................................274 第 7 章 Web 应用 ..........................................................................................................................277 7.0 简介 . ........................................................................................................................................277 7.1 R ing 简介 . ...............................................................................................................................277 7.2 使用 Ring 中间件 . ..................................................................................................................279 7.3 用 Ring 提供静态文件 . ..........................................................................................................281 7.4 用 Ring 处理表单数据 . ..........................................................................................................282 7.5 用 Ring 处理 Cookie...............................................................................................................284 7.6 用 Ring 保存会话 . ..................................................................................................................286 7.7 在 Ring 中读写请求和响应的头 . ..........................................................................................288 7.8 用 Compojure 路由请求 . .......................................................................................................289 7.9 用 Ring 执行 HTTP 重定向 . ..................................................................................................291 7.10 用 Liberator 构建 REST 风格的应用 . .................................................................................292 7.11 用 Enlive 实现 HTML 模板 . ................................................................................................294 7.12 用 Selmer 实现模板 ..............................................................................................................300 7.13 用 Hiccup 实现模板 . ............................................................................................................305 7.14 渲染 Markdown 文档 . ..........................................................................................................307 7.15 用 Luminus 来构建应用 . ......................................................................................................310 第 8 章 性能与开发效率 ..............................................................................................................312 8.0 简介 . ........................................................................................................................................312 8.1 AOT 编译 . ...............................................................................................................................312 8.2 将项目打包成 JAR 文件 . ......................................................................................................314 8.3 创建 WAR 文件......................................................................................................................317 8.4 将应用作为守护进程运行 . ....................................................................................................320 8.5 利用类型暗示减轻性能问题 . ................................................................................................325 8.6 用原生 Java 数组进行快速数学运算 . ...................................................................................328 8.7 用 Timbre 进行简单剖析 . ......................................................................................................330 8.8 用 Timbre 记日志 . ..................................................................................................................332 8.9 向 Clojars 发布库 . ..................................................................................................................334 8.10 使用宏来简化 API 弃用 . .....................................................................................................336 第 9 章 分布式计算 .......................................................................................................................341 9.0 简介 . ........................................................................................................................................341 x | 目录 9.1 用 Storm 构建活动推送系统 . ................................................................................................342 9.2 用抽取转换加载(ETL)管道来处理数据 ..........................................................................350 9.3 聚合大型文件 . .......................................................................................................................354 9.4 测试 Cascalog 工作流 . ...........................................................................................................359 9.5 设置 Cascalog 任务的检查点 . ...............................................................................................361 9.6 解释 Cascalog 查询 . ...............................................................................................................363 9.7 在 Elastic MapReduce 上运行 Cascalog 任务 . ......................................................................365 第 10 章 测试 ..................................................................................................................................367 10.0 简介 . ......................................................................................................................................367 10.1 单元测试 . ..............................................................................................................................368 10.2 用 Midje 测试 . ......................................................................................................................372 10.3 通过随机输入进行彻底测试 . ..............................................................................................375 10.4 寻找导致失败的值 . ..............................................................................................................379 10.5 运行基于浏览器的测试 . ......................................................................................................381 10.6 追踪代码执行 . ......................................................................................................................386 10.7 用 core.typed 避免空指针异常 . .........................................................................................389 10.8 用 core.typed 验证 Java 互操作.........................................................................................392 10.9 用 core.typed 检查高阶函数 . .............................................................................................395 关于作者 ..............................................................................................................................................399 关于封面 ..............................................................................................................................................399 译者序 编程语言习得 “熟悉与优雅正交。” ——Rich Hickey 约二十年前,我买过一本高等教育出版社出版的《LISP 语言》,作者是马希文、宋柔。可 惜当年没有老师指导,自己水平不够,未能深入下去,只留下了一点模糊的印象: LISP 语 言适用于人工智能,括号很多。 几年前 ,图灵公司的朋友送我一本《黑客与画家》,我连夜看完,重新燃起了对 LISP 的 兴趣 。我在书评中写道:“读完之后有一种想去学习LISP 语言的冲动。一个不懂 LISP 的 Java 程序员,不是一个好的 C++ 程序员。” 现在 ,我终于找到了机会,开始学习Clojure 这种运行在 JVM 上的 LISP 方言。经过一段 时间的学习,我完全被它迷住了! 首先吸引我的是它的函数式编程特性。作为一个学习 C++ 和 Java 多年的程序员,我已习 惯在程序中使用各种名词抽象,也就是领域术语,希望在程序中体现领域专家的思想和认 识水平 。而在Clojure 编程中,虽然它也很适合领域抽象,但它的抽象程度更高,它希望 达到数学家认识世界的水平。问题的开头通常是“给定一个无限序列……”,而常见的例 子是如何实现斐波那契数列。 Leslie L amport 说过,要将事情描述得清晰准确,人类发明的最好语言就是数学。这种对 “表达的经济性”的追求,对于中国人是不陌生的。中国是诗歌的国度,而且古人对言简 意赅的追求也有许多例子,比如“逸马杀犬于道”的故事。所以我觉得, LISP/Clojure 在 精神上与有追求的中国程序员是契合的。 xi xii | 译者序 其次,它特别适合开发领域特定语言(DSL)。在 LISP 社区中流传着一个笑话,可以说明 这一点 :任何足够大的软件,最后都会实现一个半调子 LISP 解析器。LISP 的底层抽象极 其简单,允许程序员设计更多的抽象,来描述这个世界。 学习一门新的语言,会改变学习者的思维方式。在面向对象编程时,我们更多关注单个 对象 。在函数式编程中,我们更多关注函数和集合。在工作中,不一定马上有机会使用 Clojure,但其中学到的思维方式,将对编程产生立竿见影的影响。在翻译本书时,我同时 在用 Lua 开发项目,学习了 Clojure,让我能写出更简洁、更优雅的 Lua 代码。 学习新语言有这样一些原则:(1)专注于与你相关的内容;(2)从学习这门语言的第一天 起,就把它当作你的交流方式;(3)当你听得懂别人在说什么时,就会不知不觉慢慢习得 这门语言 ;(4)语言不是大量的知识积累,而更像一种生理训练;(5)心理状态和生理状 态都很重要,要愉快和放松。对于模棱两可要有一定的容忍性,对于细枝末节不要过于纠 结,因为那会把你逼疯。 本书提供了大量的例子,覆盖了日常编程领域的方方面面,正是学习 Clojure 的好读物。 在翻译本书的过程中,我学到了很多,在此郑重推荐给大家。不足之处,还望大家指正。 王海鹏   2014 年秋 前言 本书的首要目标是提供中等长度的 Clojure 代码示例,超越基本知识,关注真实世界的日 常应用程序(而不是概念或学术问题)。 与此前的许多其他 Clojure 书籍不同,本书的主题不是语言本身,或它的功能和能力。本 书关注开发者面对的具体任务(不论他们使用哪种编程语言),展示如何用 Clojure 来解决 这些具体问题。 因此 ,本书确实不是也做不到包罗万象,因为可能的问题示例有无限多个。但是,我们希 望记录大多数程序员会经常遇到的一些比较常见的问题。通过归纳,读者将能够学到一些 常见的模式、方法和技术,有助于他们为自己面对的问题设计解决方案。 本书如何写成 关于本书,你要了解一件重要的事情:它首先是团队协作的成果。它不是由一两个人写成 的,甚至不是一个确定好的团队的成果 。相反,它是 60 多个最优秀的 Clojure 程序员协作 的结果 ,他们来自世界各地、各行各业。这些作者每天都在真实的场景中使用 Clojure:从 航空航天到社交媒体,从银行业到机器人,从 AI 研究到电子商务。 因此 ,你会在提供的实例中看到许多差异。有些快速而简要,有些则内容更为丰富,针对 Clojure 的基本原理和实现提供了易于理解的深刻洞见。 我们希望兴趣各异的读者都能从本书中有所获。我们相信,它的用处不仅在于查找具体问 题的解决方案,也在于考察 Clojure 能够提供的各种表达能力。在编辑提交的内容时,我 们非常吃惊地发现很多概念和技术对我们来说也是新的,希望对读者来说也是新的。 我们在写作和编辑时还发现,要确定我们想介绍的内容的范围是一件很难的事情。每个实 例都很棒 ,可以无限细分,进而涉及多个话题,而每个话题又值得写一个实例、一章甚至 xiii xiv | 前言 一本书。但每个实例也需要保持独立。每个实例应该提供一些有用的、有价值的信息,让 读者可以理解并消化。 我们真诚地希望自己很好地平衡了这些目标,也希望你觉得这本书有用而不乏味,内容深 刻而不是艰深难懂。 读者对象 我们希望所有使用 Clojure 的人都能从本书中学到一些东西。有许多实例介绍的是真正基 础的内容 ,初学者会觉得有用,但还有许多实例探讨的是专业话题,高级开发者会觉得有 用,有助于他们开始实践。 但如果你是 Clojure 新手,这可能不是你要看的第一本书,至少不要只看这本书。本书介 绍了许多有用的话题,但不像优秀的入门教材那样系统或完整。下面列出了一般的 Clojure 书籍,将它们作为前导教材或补充教材会很有帮助。 其他资源 本书内容并不全面,也永远不可能全面。有许多内容要讲,并且由于采用了面向任务的实 例,自然就排除了有条理、叙述式地解释整个语言的特点和能力。 要更线性、彻底地了解 Clojure 及其特点,我们推荐下面的书。 • 《Clojure 编程:Java 世界的 Lisp 实践》(O’Reilly,2 012),作者是 Chas Emerick、Brian Carper 和 C hristophe Grand。这是一本全面的、用于一般目的的 Clojure 好书,关注语言 和常见任务,面向 Clojure 的初学者。 • 《Clojure 程 序 设 计( 第 2 版 )》(Pragmatic Bookshelf,2012),作 者 是 Stuart Halloway 和 Aaron Bedra。这是第一本关于 Clojure 的书,为 Clojure 语言提供了清晰全面的介绍和指导。 • Practical Clojure(Apress,2010),作者是 Luke VanderHart 和 Stuart Sierra。它简明扼 要地解释了 Clojure 是什么,它的特点是什么。 • 《Clojure 编程乐趣》(Manning,2011),作者是 Michael Fogus 和 Chris Houser。这是一 本比较高级的教材,真正深入到 Clojure 的主题和原理。 • ClojureScript: Up and Running (O’Reilly,2 012),作 者 是 Stuart Sierra 和 Luke Vander Hart。虽然本书和这里列出的其他 Clojure 书籍主要或全部在探讨 Clojure 本身,但 ClojureScript(一种 Clojure 方言,能编译成 JavaScript)已经得到了相当的发展。这本 书介绍了 ClojureScript,以及如何使用它,并探讨了 ClojureScript 和 Clojure 之间的相似 与不同。 前言 | xv 最后,你应该看看本书的源代码,它们可以从 GitHub 自由下载(https://github.com/clojure- cookbook/clojure-cookbook)。网上选择的实例比印刷版本更多,我们仍在接受新实例的 “拉取请求”( pull request),也许某天会加入本书的下一版。 本书结构 本书的章节主要是依据主题对实例进行分组,而不是严格的分类。一个实例完全有可能适 用于不止一个章节,在这种情况下,我们试着根据我们的猜测,将它放在大部分读者首先 会去寻找的地方。 实例包含三个主要部分和一个次要部分:问题、解决方案、讨论和参阅。实例的问题陈述 提出了任务或要克服的障碍。它的解决方案解决了问题,展示了特定的技术或库,能够高 效地完成该任务。讨论完善了相关知识,探讨了解决方案和相关注意事项。最后,参阅部 分向读者指出了一些附加的资源或相关实例,帮助你采用描述的解决方案。 各章简介 本书由以下几章构成。 • 第 1 章 “原生数据”和第2 章“复合数据”介绍了 Clojure 内建的原生和复合数据结构, 解释了许多常见的(以及不太常见的)使用方式。 • 第 3 章 “广义计算”包含了一些有用的主题,广泛适用于许多不同的应用领域和项目, 从协议这样的 Clojure 特征,到可选的编程范式,例如用 core.logic 实现逻辑编程,或 用 core.async 实现异步协作。 • 第 4 章 “本地I/O”包含了程序在运行时与本地计算交互的所有方式。这包括读写标准 输入输出流,创建并操作文件,序列化和反序列化文件等。 • 第 5 章 “网络I/O 和 Web 服务”包含了类似第 4 章的主题,但探讨的是通过网络的远 程通信。它包括的实例涉及各种网络通信协议和库。 • 第 6 章 “数据库”展示了连接和使用各种数据库的技术和工具。特别关注了Datomic 数 据库,它共享了 Clojure 背后关于值、状态和标识的哲学,并扩展到了持久存储的领域。 • 第 7 章“ Web 应用”深入探讨了 Clojure 最常见的应用领域:构建和维护动态网站。它 全面介绍了 Ring(Clojure 中最流行的 HTTP 服务器库),以及 HTML 模板和渲染的工具。 • 第 8 章 “性能与开发效率”解释了拥有Clojure 程序之后还需要做些什么,介绍了打包、 分发、性能剖析、日志的常见模式,将正在进行的任务与应用的生命周期关联起来。 • 第 9 章 “分布式计算”关注云计算以及在重量级分布式数据处理中使用Clojure。特别 关注了 Cascalog,它是一个声明式 Clojure 接口,面向 Hadoop MapReduce 框架。 • 最后但同样重要的是第 10 章“测试”介绍了各种技术,来确保代码和数据的完整性和 正确性 :从传统的单元测试和集成测试,到更全面的产生式测试和模拟测试,甚至还有 xvi | 前言 可选的编译时验证,利用 core.typed 实现静态类型。 软件获取 若要按照本书的实例操作,你需要正确安装 Java 开发工具(JDK)和 Clojure 事实上的构建工 具 Leiningen。我们推荐第 7 版 JDK,但至少需要第 6 版。对于 Leiningen,至少是第 2.2 版。 如果你还没安装 Java(或者希望升级),请访问 Java 下载页面(http://www.oracle.com/ technetwork/java/javase/downloads/index.html),按提示下载并安装 Java JDK。 要安装 Leiningen,请遵循 Leiningen 网站(http://leiningen.org/)的安装指南。如果已经安 装了 Leiningen,通过执行 lein upgrade 命令来取得最新版本。如果不熟悉 Leiningen,请 访 问 使 用 指 南(https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md), 了解更多信息。 你不需要手工安装的就是 Clojure 本身,因为 Leiningen 将随时根据需要,替你安装。要验 证安装,就运行 lein repl 来检查 Clojure 的版本: $ lein repl # ... user=> *clojure-version* {:major 1, :minor 5, :incremental 1, :qualifier nil} 某些实例在 GitHub 上提供了一些在线材料。如果你的系统上没有安装 Git, 请按照安装指南(https://help.github.com/articles/set-up-git)操作,以便能将 GitHub 代码库签出到本地。 某些实例(如数据库实例),需要进一步安装软件。在这种情况下,实例将包含安装工具 的额外信息。 本书约定 在这本全是解决方案的书中,你会发现其中有不少代码。Clojure 源代码使用等宽字体, 像这样: (defn add [x y] (+ x y)) 如果 Clojure 表达式被求值并返回,该值将以注释的形式给出,跟在一个箭头后面,就像 它出现在命令行中一样: (add 1 2) 前言 | xvii ;; -> 3 在合适的时候,代码示例可能略去或省略返回值注释。最常见的两种情况就是在定义函数 /var 时和缩短较长的输出时: ;; 这会返回 #'user/one,但你真的关心吗? (def one 1) (into [] (range 1 20)) ;; -> [1 2 ... 20] 如果表达式产生输出到 STDOUT 或 STDERR,就会有注释说明(分别用 *out* 或 *error*),跟 着就是每行输出的注释: (do (println "Hello!") (println "Goodbye!")) ;; -> nil ;; *out* ;; Hello! ;; Goodbye! REPL会话 看到 REPL 驱动开发目前正在流行,因此本书就成为了 REPL 驱动的。REPL(读取、求 值、打印、循环)是交互式的提示符,对表达式求值并打印出结果。Bash 提示符、irb 和 python 提示符都是 REPL 的例子。本书中几乎每个实例,都是为在 Clojure REPL 中运行而 设计的。 虽然 Clojure REPL 传统上显示为 user=> ...,但本书希望读者能够复制粘贴实例中所有的 例子 ,并看到标示的结果。因此,例子中省略了 user=> 并以注释的方式给出了输出,让事 情变得更容易。如果你在计算机旁,这就特别有帮助:只要复制粘贴代码示例,不用担心 遇到不能执行的代码。 如果例子只适用于 REPL 的环境,我们将保留传统的 REPL 风格(带上 user=>)。下面两 个例子分别是只适用于 REPL 的例子和它的简化版本。 只适用于 REPL: user=> (+ 1 2) 3 user=> (println "Hello!") Hello! nil 简化版本: (+ 1 2) xviii | 前言 ;; -> 3 (println "Hello!") ;; *out* ;; Hello! 控制台/终端会话 控制台会话(例如, shell 命令)用等宽字体表示,行开始的美元符号( $)表示 shell 提示 符。输出打印前面没有($): $ lein version Leiningen 2.0.0-preview10 on Java 1.6.0_29 Java HotSpot(TM) 64-Bit Server VM 命令行末的反斜杠(\)告诉控制台,命令将在下一行继续。 我们的金童 lein-try Clojure 不以它的扩展标准库而闻名。不像 Perl 或 Ruby 这样的语言,Clojure 的标准库 相对比较小。Clojure 选择了简单和强大。因此 Clojure 是一种有许多库的语言,但不 是内建的库(好吧,Java 除外)。 因为本书中这么多解决方案都依赖于第三方库,所以我们开发了 lein-try(https:// github.com/rkneufeld/lein-try)。Leiningen 是 Clojure 事 实 上 的 项 目 工 具,lein-try 是 Leiningen(http://leiningen.org/)的一个小插件,让你快速而容易地尝试各种 Clojure 库。 要使用 lein-try,请确保安装了 Leiningen,然后将你的用户特性描述文件(~/.lein/ profiles.clj)编辑成下面的样子: {:user {:plugins [[lein-try "0.4.1"]]}} 现在,在项目内外都可以使用 lein try 命令来启动 REPL,访问任何你喜欢的库: $ lein try clj-time #... user=> 长话短说:只要可能,在用第三方库的实例中,你会看到要求执行 lein-try 命令。在 3.4 节中,有一个用 lein-try 尝试实例的例子。 如果实例不能通过 lein-try 运行,我们会努力提供足够的指令,说明如何在你的机器 上运行该实例。 排版约定 本书使用下面的字体约定。 前言 | xix • 楷体 新术语第一次出现时使用楷体,目的是强调。 • 等宽字体 用于函数名、方法名和参数,用于数据类型、类和命名空间,在例子中表示输入和输 出,在正则文本中表示字面代码。 • 等宽黑体 用于表示命令,你应该在命令行中照样输入。 • < 可取代的值 > 路径、命令、函数名中的元素,应该由用户提供的值来取代尖括号及其中的内容。 库的名称符合两种惯例之一:具有适当名称的库用普通字体(如 Hiccup 或 Swing),而名 称与代码符号相似的库用等宽字体(如 core.async 或 clj-commons-exec)。 这个符号表示提示或建议。 这个符号表示一般注释。 这个符号表示警告或注意。 使用代码示例 补充材料(代码示例、练习等)可以在 https://github.com/clojure-cookbook/clojure-cookbook 下载。 本书的目的是帮助你完成工作。一般来说,如果示例代码出现在本书中,就可以在你的程 序和文档中使用它,不需要联系我们获得许可,除非你打算复制大量的代码。例如,写 一个程序 ,用到本书中的几段代码,不需要获得许可。而销售或分发 O’Reilly 图书的示例 xx | 前言 CD-ROM,确实需要许可。回答问题时摘录本书并引用示例代码,不需要获得许可。在你 的产品文档中包含本书的大量示例代码,则确实需要许可。 我们感谢你说明来源,但这不是必须的。来源说明通常包括标题、作者、出版商和 ISBN。 例如:“ Luke VanderHart 和 Ryan Neufeld 所著 Clojure Cookbook (O’Reilly). C opyright 2014 Cognitect, Inc., 978-1-449-36617-9.” 如果你觉得你对代码的使用超出了合理的范围或上述允许的情况,可随时联系我们: permissions@oreilly.com. Safari® Books Online Safari Books Online(http://www.safaribooksonline.com)是应运而生的数字图书馆。它同 时以图书和视频的形式出版世界顶级技术和商务作家的专业作品。技术专家、软件开发人 员、Web 设计师、商务人士和创意专家等,在开展调研、解决问题、学习和认证培训时, 都将 Safari Books Online 视作获取资料的首选渠道。 对 于 组 织 团 体、 政 府 机 构 和 个 人,Safari Books Online 提 供各种产品组合和灵活的定价策略。用户可通过一个功能 完备的数据库检索系统访问 O’Reilly M edia、Prentice Hall Professional、Addison-Wesley Professional、Microsoft Press、 Sams、Que、Peachpit Press、Focal Press、Cisco Press、John Wiley & Sons、Syngress、 Morgan Kaufmann、IBM Redbooks、Packt、Adobe Press、FT Press、Apress、Manning、 New R iders、McGraw-Hill、Jones & Bartlett、Course Technology 以及其他几十家出版社的 上千种图书、培训视频和正式出版之前的书稿。要了解 Safari Books Online 的更多信息, 我们网上见。 联系我们 请把对本书的意见和疑问发送给出版社。 美国: O’Reilly Media, Inc. 1005 Gravenstein Highway North Sebastopol, CA 95472 中国: 北京市西城区西直门南大街 2 号成铭大厦 C 座 807 室(100035) 奥莱利技术咨询(北京)有限公司 前言 | xxi O’Reilly 的每一本书都有专属网页,你可以在那儿找到本书的相关信息,包括勘误表、示 例代码以及其他信息。本书的网站地址是: http://oreil.ly/clojure-ckbk 对于本书的评论和技术性问题,请发送电子邮件到:bookquestions@oreilly.com 要了解更多 O’Reilly 图书、培训课程、会议和新闻的信息,请访问以下网站: http://www.oreilly.com 我们在 Facebook 的地址如下:http://facebook.com/oreilly 请关注我们的 Twitter 动态:http://twitter.com/oreillymedia 我们的 YouTube 视频地址如下:http://www.youtube.com/oreillymedia 致谢 如果没有 Clojure 社区中许多人的无私奉献,本书不可能写成。超过 65 个 Clojure 开发者 响应号召 ,提交实例,审读,并为本书的方向提供了建议。归根到底,这是一本属于社区 的书,我们只是很荣幸能够将内容组织到一起。这些贡献者是: • Adam Bard,adambard on GitHub • Alan Busby,thebusby on GitHub • Alex Miller,puredanger on GitHub • Alex Petrov,ifesdjeen on GitHub • Alex Robbins,alexrobbins on GitHub • Alex Vzorov,0rca on GitHub • Ambrose Bonnaire-Sergeant,frenchy64 on GitHub • arosequist • Chris Allen,bitemyapp on GitHub • Chris Ford,ctford on GitHub • Chris Frisz,cjfrisz on GitHub • Clinton Begin,cbegin on GitHub • Clinton Dreisbach,cndreisbach on GitHub • Colin Jones,trptcolin on GitHub • Craig McDaniel,cpmcdaniel on GitHub • Daemian Mack,daemianmack on GitHub • Dan Allen,mojavelinux on GitHub • Daniel Gregoire,semperos on GitHub • Dmitri Sotnikov,yogthos on GitHub xxii | 前言 • Edmund Jackson,ejackson on GitHub • Eric Normand,ericnormand on GitHub • Federico Ramirez,gosukiwi on GitHub • Filippo Diotalevi,fdiotalevi on GitHub • fredericksgary • Gabriel Horner,cldwalker on GitHub • Gerrit,g erritjvv on GitHub • Guewen Baconnier,guewen on GitHub • Hoàng Minh Th ng,myguidingstar on GitHub • Jason Webb,bigjason on GitHub • Jason Wolfe,w01fe on GitHub • Jean Niklas L’orange,h yPiRion on GitHub • Joey Yang,joeyyang on GitHub • John Cromartie,jcromartie on GitHub • John Jacobsen,eigenhombre on GitHub • John Touron,jwtouron on GitHub • Joseph Wilk,josephwilk on GitHub • jungziege • jwhitlark • Kevin Burnett,burnettk on GitHub • Kevin Lynagh,lynaghk on GitHub • Lake Denman,ldenman on GitHub • Leonardo Borges,leonardoborges on GitHub • Mark Whelan,mrwhelan on GitHub • Martin Janiczek,Janiczek on GitHub • Matthew Maravillas,maravillas on GitHub • Michael Fogus,fogus on GitHub • Michael Klishin,michaelklishin on GitHub • Michael Mullis,mmullis on GitHub • Michael O’Church,m ichaelochurch on GitHub • Mosciatti S.,siscia on GitHub • nbessi • Neil Laurance,toolkit on GitHub • Nurullah Akkaya,nakkaya on GitHub • Osbert Feng,osbert on GitHub • Prathamesh Sonpatki,prathamesh-sonpatki on GitHub • R. T. Lechow,rtlechow on GitHub 前言 | xxiii • Ravindra R. Jaju,jaju on GitHub • Robert Stuttaford,robert-stuttaford on GitHub • Russ Olsen,russolsen on GitHub • Ryan Senior,senior on GitHub • Sam Umbach,sumbach on GitHub • Sandeep Nangia,nangia on GitHub • Steve Miner,miner on GitHub • Steven Proctor,stevenproctor on GitHub • temacube • Tobias Bayer,codebrickie on GitHub • Tom White,dribnet on GitHub • Travis Vachon,travis on GitHub • Stefan Karlsson,zclj on GitHub 最大的贡献者值得特别感谢:Adam Bard、Alan Busby、Alex Robbins、Ambrose Bonnaire- Sergeant、Dmitri Sotnikov、John Cromartie、John Jacobsen、Robert Stuttaford、Stefan Karlsson 和 Tom Hicks。这些杰出的开发者一起几乎贡献了本书三分之一的实例。 感谢我们的技术复查者 Alex Robbins、Travis Vachon 和 Thomas Hicks。在大约 11 个小时 或更短的时间内,这几位先生查遍了本书,寻找技术错误。普通的技术复查者只会提交文 本的问题描述,这几位则做得更多,常常提交拉取请求( pull request),修复了他们报告的 所有错误。总之,和他们一起工作很开心,因为他们的参与,本书变得好了很多。 最后 ,感谢我们的雇主Cognitect,让我们有时间完成本书,同时感谢所有的同事,他们提 出了建议和反馈,而且最棒的是,他们提供了更多的实例! Ryan Neufeld 首先,非常感谢 Luke,是他最先提出了这本书的主意。我非常感激他邀请我加入,一起编 写。人们说学习某样东西最好的方法就是写一本关于它的书 ,此言不虚。编写这本书确实 丰富了我的 Clojure 技能,使我的水平提升到了另一个层次。 而且最重要的是,我要感谢家人,他们容忍我完成写书的过程。让这件事顺利起步是无比 艰巨的任务,没有妻子 Jackie 和女儿 Elody 的爱和支持,这不可能完成。如果不是侵占了 她们无数的夜晚、周末和休假时间,我不可能编写完这本书。 Luke VanderHart 首先,我要感谢合著者 Ryan,他工作非常努力,参与编写了这本书。 xxiv | 前言 同时,我在 Cognitect 的同事提供了许多想法和思路,最重要的是有一个很好的委员会,探 讨在编写和编辑过程中出现的许多问题。非常感谢他们,同时也感谢他们提供机会,让我 整天写 Clojure 代码,天天如此。 1 第 1 章 原生数据 1.0 简介 对于处理困难的问题,Clojure 是一门极好的语言。它的简单工具让软件开发者一层一层 地建立抽象,直到能够轻松地处理世界上最难的一些问题。像化学一样,每个了不起的 Clojure 程序都归结为简单的原子,即原生类型。 很久以前, Clojure 就站在 Java 巨人的肩上,它利用了 Java 虚拟机(JVM)1 中提供的一组 极好的类型,这些类型经过了实践的检验:字符串、数值类型、日期、通用唯一标识符 (UUID),只要说得出的,Clojure 都有。本章探讨 Clojure 的原生类型,以及如何完成常见 任务。 字符串 几乎所有编程语言都知道如何处理字符串,Clojure 也不例外。除了一些差别之外, Clojure 提供了像大多数其他语言一样的能力。下面是一些应该了解的关键差别。 首先, Clojure 字符串基于 Java 的 UTF-16 字符串。不需要在文件中添加注释来说明字符串 的编码方式,也不需要担心在转换过程中丢失了字符。Clojure 程序已经准备好与英文字符 之外的世界通信。 注 1: JVM 是执行 Java 字节码的地方。Clojure 编译器以 JVM 为目标,生成能运行的字节码。因此,你可 以任意使用所有原生 Java 类型。 2 | 第 1 章 其次,Clojure 不像 Perl 或 Ruby 拥有较大的字符串程序库,其内建的字符串操作库相当精 练。初看起来这可能有点奇怪 ,但 Clojure 喜欢简单的、可组合的工具, Clojure 中有许多 集合操作函数,都能很好地处理字符串,因为它们也是集合!由于这个原因, Clojure 的字 符串库小得出人意料。在 clojure.string 命名空间中,可以找到很小一组专门针对字符串 的函数。 Clojure 也利用了它的宿主平台(JVM),没有重复 java.lang.String 类已实现的功能。在 Clojure 中使用 Java 互操作并不是一种失败的尝试,因为语言的设计就是为了便于互操作, 使用内建的字符串方法通常和调用 Clojure 的函数一样方便。 我们建议在必要的时候,“require as”clojure.string 命名空间。盲目地 :use 一个命名空 间总是令人气恼的 2,常常导致冲突或混乱。所以我们更喜欢给它取别名为 str 或 s,而非 在所有东西前面加上 clojure.string,那有点奇怪: (require '[clojure.string :as str]) (str/blank? "") ;; -> true 数值类型 对于数值类型,Clojure 和 Java 之间的差异比较大。但这不一定是坏事。虽然 Java 的数值 类型可能非常快或具有任意的精度,但数值整体上没有一组精美的接口。Clojure 把 Java 的各种数值类型统一成为一致的包,每个困难的地方都有解决的办法。 本章中关于数值类型的实例,将展示如何利用这些设计,实现期望的速度、精度或表达 能力。 日期 在 Java 生态系统中,日期和时间的历史长而曲折。需要 Date、Time、DateTime 或 Calendar 吗?谁知道呢。为什么这些 API 都那么不稳定?本章中的实例应该能够阐明何时使用恰当 的内建类型,如何使用,以及若内建类型不够用(或者非常难用), 何时去寻找外部库。 1.1 改变字符串的大小写 作者:Ryan Neufeld 注 2: 用了 use,就在项目的命名空间中引入了许多新的符号,又没有留下线索表明它们来自哪里。这通常 让代码维护者感到困惑和沮丧。我们强烈建议不要用 use。 原生数据 | 3 问题 需要改变一个字符串的大小写。 解决方案 用 clojure.string/capitalize 来大写字符串中的第一个字符。 (clojure.string/capitalize "this is a proper sentence.") ;; -> "This is a proper sentence." 如果需要改变所有字符的大小写,请用 clojure.string/lower-case 或 clojure.string/ upper-case: (clojure.string/upper-case "loud noises!") ;; -> "LOUD NOISES!" (clojure.string/lower-case "COLUMN_HEADER_ONE") ;; -> "column_header_one" 讨论 大小写函数只影响字母。虽然函数 capitalize、lower-case 和 upper-case 可能会改动字 母,但标点符号或数字会保持不变: (clojure.string/lower-case "!&$#@#%^[]") ;; -> "!&$#@#%^[]" Clojure 对所有字符串都使用 UTF-16 编码,因此它对什么是字母的定义是相当宽泛的,包 括有重音的字母。例如短句“Hurry up, computer!”,它包含字母 e,翻译成法语时会有锐 音(é)和长音(ê)记号。由于这些特殊的字符都被视为字母,大小写函数可以对它们进 行相应的改变: (clojure.string/upper-case "Dépêchez-vous, l'ordinateur!") ;; -> "DÉPÊCHEZ-VOUS, L'ORDINATEUR!" 参阅 • clojure.string 命名空间的 API 文档(http://clojure.github.io/clojure/clojure.string-api.html)。 • java.lang.String 的 API 文档(http://docs.oracle.com/javase/7/docs/api/java/lang/String.html)。 1.2 清除字符串中的空白字符 作者:Ryan Neufeld 4 | 第 1 章 问题 需要清除字符串中的空白字符。 解决方案 使用 clojure.string/trim 函数来删除字符串首尾的所有空白字符: (clojure.string/trim " \tBacon ipsum dolor sit.\n") ;; -> "Bacon ipsum dolor sit." 要处理字符串内部的空白字符,需要有点创造性。使用 clojure.string/replace 来修正字 符串内部的空白字符: ;; 将空白字符压缩为一个空格 (clojure.string/replace "Who\t\nput all this\fwhitespace here?" #"\s+" " ") ;; -> "Who put all this whitespace here?" ;; 将 W indows 风格的换行替换成 Unix 风格的换行 (clojure.string/replace "Line 1\r\nLine 2" "\r\n" "\n") ;; -> "Line 1\nLine 2" 讨论 什么构成了 Clojure 中的空白字符?回答取决于功能:有些比另一些更自由,但可以放心 地假定空格( )、制表符(\t)、换行(\n)、回车(\r)、走行(\f)和垂直制表符(\x0B) 都会被当成空白字符。在 Java 的正则表达式实现中,这一组字符由 \s 匹配。 Ruby 和其他语言将字符串操作函数放在核心命名空间,Clojure 不同,它将 clojure. string 命名空间放在 clojure.core 之外,因此不能够直接使用。常用的技巧是将 clojure. string 引入为 str 或 string 这样的简写形式,让代码更简明: (require '[clojure.string :as str]) (str/replace "Look Ma, no hands" "hands" "long namespace prefixes") ;; -> "Look Ma, no long namespace prefixes" 有时候,也许不需要把字符串两边的空白字符都删掉。如果只是想删除字符串左边或右边 的空白字符,请分别使用 clojure.string/triml 或 clojure.string/trimr。 (clojure.string/triml " Column Header\t") ;; -> "Column Header\t" (clojure.string/trimr "\t\t* Second-level bullet.\n") , ;; -> "\t\t* Second-level bullet." 参阅 • 1.3 节“利用部件构建字符串”。 原生数据 | 5 1.3 利用部件构建字符串 作者:Ryan Neufeld 问题 有多个字符串、值或集合,需要合并成一个字符串。 解决方案 使用 str 函数来连接多个字符串和(或)值: (str "John" " " "Doe") ;; -> "John Doe" ;; str 也 适用于变量,或其他任何值 (def first-name "John") (def last-name "Doe") (def age 42) (str last-name ", " first-name " - age: " age) ;; -> "Doe, John - age: 42" 使用 apply 带 str,将值的集合连接成一个字符串: ;; 将一系列字符还原成一个字符串 (apply str "ROT13: " [\W \h \y \v \h \f \ \P \n \r \f \n \e]) ;; -> "ROT13: Whyvhf Pnrfne" ;; 或者将一些行还原成一个文件(如果行都有换行符) (def lines ["#! /bin/bash\n", "du -a ./ | sort -n -r\n"]) (apply str lines) ;; -> "#! /bin/bash\ndu -a ./ | sort -n -r\n" 讨论 Clojure 的 str 就像一个好的 Unix 工具:它做一件事,做得很好。如果向 str 提供一个或 多个参数 ,它会对参数调用 Java 的 .toString() 方法,将每个结果加在后面。如果提供 nil 作为参数或不带参数调用,str 会返回代表字符串身份的值,即空串。 对于字符串连接,Clojure 采用了相当自由的方式。(apply str ...)没有什么专门针对字 符串的东西。它只是使用了高阶函数 apply,模拟用变长参数调用 str。 这个 apply: (apply str ["a" "b" "c"]) 在功能上等价于: 6 | 第 1 章 (str "a" "b" "c") 既然 Clojure 在连接字符串时没有什么限制,我们就可以自由发挥,利用 Clojure 提供的大 量操作函数。例如,从一行抬头和几行数据中构造逗号分隔的值(CSV)。这个例子特别 适合 apply,因为可以在前面加上抬头,不用将它插在 rows 集合的前面: ;; 利用一行抬头字符串和几行数据来构造 CSV (def header "first_name,last_name,employee_number\n") (def rows ["luke,vanderhart,1","ryan,neufeld,2"]) (apply str header (interpose "\n" rows)) ;; -> "fi rst_name,last_name,employee_number\nluke,vanderhart,1\nryan,neufeld,2" 如果要做的事情不是太特别,apply 和 interpose 可能有些繁琐。要连接简单的字符串,通 常用 clojure.string/join 更容易。join 函数接受一个集合和一个可选的分隔符。带分隔 符时, join 返回的字符串是集合的所有元素,中间用该分隔符分隔。不带分隔符时,它返 回所有元素挤在一起的字符串,类似于(apply str coll)的返回: (def food-items ["milk" "butter" "flour" "eggs"]) (clojure.string/join ", " food-items) ;; -> "milk, butter, flour, eggs" (clojure.string/join [1 2 3 4]) ;; -> "1234" 参阅 • 1.6 节“格式化字符串”。 • clojure.string 命名空间的 API 文档(http://clojure.github.io/clojure/clojure.string-api.html)。 • java.lang.String 的 API 文档(http://docs.oracle.com/javase/7/docs/api/java/lang/String.html)。 1.4 将字符串作为字符序列 作者:Ryan Neufeld 问题 需要处理字符串中的单个字符。 解决方案 对字符串使用 seq,得到它包含的字符序列: (seq "Hello, world!") ;; -> (\H \e \l \l \o \, \space \w \o \r \l \d \!) 原生数据 | 7 但是,并非每次想处理字符串中的字符时,都要调用 seq。以序列为参数的所有函数,都 会自动将字符串强制转换成字符序列: ;; 计算每个字符在字符串中出现的次数 (fr equencies (clojure.string/lower-case "An adult all about A's")) ;; -> {\space 4, \a 5, \b 1, \d 1, \' 1, \l 3, \n 1, \o 1, \s 1, \t 2, \u 2} ;; 字符串中的每个字母都是大写的吗? (defn yelling? [s] (every? #(or (not (Character/isLetter %)) (Character/isUpperCase %)) s)) (yelling? "LOUD NOISES!") ;; -> true (yelling? "Take a DEEP breath.") ;; -> fa lse 讨论 在计算机科学中,“字符串”意味着“字符序列”, Clojure 对字符串就是这么处理的。因为 Clojure 字符串背后就是字符序列,所以在需要集合的地方,都可以用字符串替代。如果这 样做 ,字符串就会被解释为一个字符集合。(seq string) 没有什么特别的。seq 函数只是 返回一个字符集合的序列,这些字符组成了这个字符串。 更常见的是,在对字符串中的字符做了某种工作之后,希望将这个集合恢复成一个字符 串。对字符集合使用 apply 和 str,将它们还原成字符串: (apply str [\H \e \l \l \o \, \space \w \o \r \l \d \!]) ;; -> "Hello, world!" 参阅 • 1.3 节“利用部件构建字符串”。 • 1.5 节“字符与整数的转换”。 1.5 字符与整数的转换 作者:Ryan Neufeld 问题 需要将字符转换成对应的 Unicode 编码值(整数值),或反过来。 8 | 第 1 章 解决方案 用 int 函数将字符转换成它的整数值: (int \a) ;; -> 97 (int \ø) ;; -> 248 (int \a) ; 希腊字母 a lpha ;; -> 945 (int \u03B1) ; 希腊字母 a lpha(按编码值) ;; -> 945 (map int "Hello, world!") ;; -> (72 101 108 108 111 44 32 119 111 114 108 100 33) 用 char 函数返回整数编码值对应的字符: (char 97) ;; -> \a (char 125) ;; -> \} (char 945) ;; -> \a (reduce #(str %1 (char %2)) "" [115 101 99 114 101 116 32 109 101 115 115 97 103 101 115]) ;; -> "secret messages" 讨论 Clojure 继承了 JVM 强大的 Unicode 支持。所有字符串都是 UTF-16 字符串,所有字符都是 Unicode 字符。前面 256 个 Unicode 编码值与 ASCII 码相等,这很方便,让标准的 ASCII 文本很容易处理。但是,Clojure 像 Java 一样,没有对 ASCII 码进行任何特殊处理,字符 和整数之间的一一对应表明,编码值直接延伸到整个 Unicode 空间。 例如,表达式 (map char (range 0x0410 0x042F)) 列出所有的斯拉夫语大写字母,它们处 于 Unicode 的这个范围: (\А \Б \В \Г \Д \Е \Ж \З \И \Й \К \Л \М \Н \О \П \Р \С \Т \У \Ф \Х \Ц \Ч \Ш \Щ \Ъ \Ы \Ь \Э \Ю) char 和 int 函数的主要用处,是将一个数字强制转换成 java.lang.Integer 或 java.lang. Character 的实例。Integer 和 Character 最终都是数字编码的,尽管 Character 还支持一 原生数据 | 9 些额外的文本相关方法,要先转换成真正的数字类型,才能用于数学表达式。 参阅 • Unicode Explained(http://oreil.ly/unicode-explained),作 者 Jukka K. Korpela (O’Reilly), 真正全面地探讨了 Unicode 和国际化的工作原理。 • 1.4 节“将字符串作为字符序列”,详细讨论了处理构成字符串的字符。 • 1.15 节“解析数字”。 1.6 格式化字符串 作者:Ryan Neufeld 问题 需要在字符串中插入一些值,并设定这些值在字符串中出现的格式。 解决方案 将值格式化后插入字符串的最快方法,是 str 函数: (def me {:first-name "Ryan", :favorite-language "Clojure"}) (str "My name is " (:first-name me) ", and I really like to program in " (:favorite-language me)) ;; -> "My name is Ryan, and I really like to program in Clojure" (apply str (interpose " " [1 2.000 (/ 3 1) (/ 4 9)])) ;; -> "1 2.0 3 4/9" 但使用 str 时,值的插入是不加思考的,以默认的 .toString() 方式显示。不仅如此,有 时候看着 str 的格式,很难解释想要的输出是什么。 要更好地控制这些值的显示方式,请用 format 函数: ;; 产生一个文件名,带有 0 补全的可排序的索引 (defn filename [name i] (fo rmat "%03d-%s" i name)) ; ➊ (filename "my-awesome-file.txt" 42) ;; -> "042-my-awesome-file.txt" ;; 创建一个对齐的表格 (defn tableify [row] (apply format "%-20s | %-20s | %-20s" row)) ; ➋ (def header ["First Name", "Last Name", "Employee ID"]) 10 | 第 1 章 (def employees [["Ryan", "Neufeld", 2] ["Luke", "Vanderhart", 1]]) (->> (concat [header] employees) (map tableify) (mapv println)) ;; *out* ;; Fi rst Name | Last Name | Employee ID ;; Ryan | Neufeld |2 ;; Luke | Vanderhart |1 ➊ 0 标记表明为数字补上零(这里是 3 个数字)。 ➋ - 标记表明字符串(s)左对齐,最小宽度为 20 个字符。 讨论 要在字符串中插入值,有两种不同的选择:可以用 str,这种方法很方便,但难以控制值 的显示 ;也可以使用format,它可以精细控制值的显示方法,但需要了解 C 和 Java 风格 的格式化字符串。归根结底,采用的工具或复杂程度应该符合当前任务的需求:如果值的 默认格式足够,就使用 str。如果需要对值的显示有更多控制,就使用 format。 格式字符串 传给 format 的第一个参数就是所谓的格式字符串。这些字符串的文法不是 Clojure 新 创或独有的,甚至也不应归功于 Java,它们实际上来自 C 的 printf 函数。Clojure 的 format 函数使用了 Java 的 String/format,它实现了 printf 风格的值替换。 格式字符串是一个正常的字符串,其中嵌入了任意数量的格式指定符。格式指定符是 一个占位符,稍后将由值取代。最简单的形式是 % 后跟一个类型指定字符。例如, %d 是指整数(d 表示数字),%f 表示浮点数。除了字符串、整数和浮点数的指定字符,还 有字符、日期和不同进制的数字(8 进制和 16 进制)等。 这些格式指定符的特殊之处在于,可以在 % 和类型指定字符之间插入任意多的标记和 选项。例如,“ %-10s”表明提供的字符串(s)应该左对齐(-),总的最小宽度是 10。 “%07.3f”将一个数变成 0 补齐的数,有 7 个字符宽,包括 3 个小数位(就像杜威十进 制系统中使用的数): (format "%07.3f" 0.005) ;; -> "000.005" ;; 计算机编程、程序和数据书籍的杜威十进制分类 要了解格式化字符串的更多内容,请查看 java.util.Formatter 的 API 文档(http:// docs.oracle.com/javase/7/docs/api/java/util/Formatter.html)。 参阅 • 1.3 节“利用部件构建字符串”。 原生数据 | 11 • 1.28 节“利用 clj-time 格式化日期”。 1.7 按模式查找字符串 作者:Ryan Neufeld 问题 需要测试一个字符串,看看它的组成部分是否符合一个模式。 解决方案 要检查字符串中是否存在一个模式,请用 re-find,参数是期望的模式和要检查的字符串。 用正则表达式来表示期望的模式(如“foo”或“\d+”): ;; 所有连续的数字 (re-fi nd #"\d+" "I've just finished reading Fahrenheit 451") ;; -> "451" (re-fi nd #"Bees" "Beads aren't cheap.") ;; -> nil 讨论 要检查字符串是否包含一个模式,re-find 非常方便。它以正则表达式模式和字符串为参 数,返回模式的第一个匹配或 nil。 如果条件更严格,要求整个字符串匹配一个模式,请使用 re-matches。它和 re-find 不一 样,不是匹配字符串的任意部分,而是仅匹配整个字符串。 ;; 在 fi nd 中,#"\w+" 是任何连续的单词字符 (re-fi nd #"\w+" "my-param") ;; -> "my" ;; 但在 m atches 中,#"\w+" 意味着 "全是单词字符 " (re-matches #"\w+" "my-param") ;; -> nil (re-matches #"\w+" "justLetters") ;; -> "justLetters" 参阅 • java.lang.Pattern 的 API 文档(http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pat tern.html),其中定义了 Java 支持的正则表达式语法(Clojure 的正则表达式一样)。 12 | 第 1 章 • 1.8 节“利用正则表达式从字符串中取出值”,探讨了利用正则表达式从字符串中提取值。 • 1.9 节“对字符串执行查找和替换”。 1.8 利用正则表达式从字符串中取出值 作者:Ryan Neufeld 问题 需要提取匹配指定模式的字符串部分。 解决方案 使用 re-seq,参数是一个正则表达式模式和一个字符串,得到一系列连续的匹配: ;; 从句子中提取简单的单词 (re-seq #"\w+" "My Favorite Things") ;; -> ("My" "Favorite" "Things") ;; 提取简单的 7 位电话号码 (re-seq #"\d{3}-\d{4}" "My phone number is 555-1234.") ;; -> ("555-1234") 带有匹配组(括号)的正则表达式将返回一个向量,包含所有的完全匹配: ;; 提取所有的 Twitter 用户名和 # 标签 (defn mentions [tweet] (re-seq #"(@|#)(\w+)" tweet)) (mentions "So long, @earth, and thanks for all the #fish. #goodbyes") ;; -> (["@earth" "@" "earth"] ["#fish" "#" "fish"] ["#goodbyes" "#" "goodbyes"]) 讨论 提供一个简单的模式(没有匹配组的), re-seq 会返回一系列的匹配。这是惰性匹配,充 分体现了 Clojure 的强大。对一个巨大的字符串调用 re-seq 时,不会马上扫描整个字符串, 可以增量式地处理这些值,或者将计算工作推迟到应用程序后面的部分。 如果给定的正则表达式包含匹配组,re-seq 的做法就有点不一样。不要担心,结果序列 仍然是惰性的——不过其值将是向量,而非单调的字符串。向量的第一个值总是完整的匹 配,不论是否包含匹配组 。后续的值是匹配组括号所捕获的字符串。这些捕获的值将按括 号的顺序出现,除非有嵌套的情况。请看下面的例子: ;; 利用正则表达式来捕获和分解电话号码及其标题。 (def re-phone-number #"(\w+): \((\d{3})\) (\d{3}-\d{4})") 原生数据 | 13 (re-seq re-phone-number "Home: (919) 555-1234, Work: (191) 555-1234") ;; -> (["Home: (919) 555-1234" "Home" "919" "555-1234"] ;; ["Work: (191) 555-1234" "Work" "191" "555-1234"]) 如果只要在字符串中寻找一次匹配,那就使用 re-find。它的行为几乎和 re-seq 一样,但 只返回第一次匹配的一个值,而不是一系列的匹配值。 除了 re-seq 以外,还有另一种方法可以迭代访问字符串中的所有匹配。可以在 re-matcher 上反复调用 re-find,但我们不建议这样做,因为它不太符合 Clojure 的习惯。改变一个 re-matcher 对象,然后反复调用 re-find 就是不对,它完全违反了纯函数式的原则。我们 强烈建议使用 re-seq,而不是 re-matcher 和 re-find,除非真有好的理由。 参阅 • 1.7 节“按模式查找字符串”,查找了字符串中出现的模式。 • 1.9 节“对字符串执行查找和替换”,利用了正则表达式来查找和替换字符串的某些部分。 • java.lang.Pattern 的 API 文档(http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern. html),其中定义了 Java 支持的正则表达式语法(Clojure 的正则表达式一样)。 1.9 对字符串执行查找和替换 作者:Ryan Neufeld 问题 需要修改字符串的某些部分,使它们匹配某种确定的模式。 解决方案 如果要选择性地替换字符串中的某些部分,多才多艺的 clojure.string/replace 就是我们 要找的函数。 对于简单的模式,用 replace 时带一个普通的字符串,作为它的匹配模式: (def about-me "My favorite color is green!") (clojure.string/replace about-me "green" "red") ;; -> "My favorite color is red!" (defn de-canadianize [s] (clojure.string/replace s "ou" "o")) (de-canadianize (str "Those Canadian neighbours have coloured behaviour" " when it comes to word endings")) ;; -> "Those Canadian neighbors have colored behavior when it comes to word ;; endings" 14 | 第 1 章 简单的字符串替换只能完成这样的功能。如果需要替代的模式中包含某些可变性,就需要 寻找更强大的武器:正则表达式。使用 Clojure 的正则表达式文法(#"...")来指定正则表 达式模式: (defn linkify-comment "Add Markdown-style links for any GitHub issue numbers present in comment" [repo comment] (clojure.string/replace comment #"#(\d+)" (str "[#$1](https://github.com/" repo "/issues/$1)"))) (linkify -comment "next/big-thing" "As soon as we fix #42 and #1337 we should be set to release!") ;; -> "As soon as we fix ;; [#42](https://github.com/next/big-thing/issues/42) and ;; [#1337](https://github.com/next/big-thing/issues/1337) we ;; should be set to release!" 讨论 replace 是字符串函数中最强大、最复杂的之一。这种复杂性主要来自于它可以进行不同 的匹配和替换。 如果传入一个字符串来匹配,replace 期待一个字符串来替换。在被查字符串中,所有发 生的匹配都会被直接替换成替代字符串。 如果传入一个字符(如 \c 或 \n)来匹配,replace 期待一个字符来替换。就像字符串替换 字符串一样,replace 的字符替换字符的模式也是直接替换的。 如果传入一个正则表达式来匹配,replace 就有趣得多了。正则表达式匹配的一种可能替 换是一个字符串,就像在 linkify-comment 的例子中那样。这个字符串将特殊的字符组合 (如 $1 和 $2)解释为变量,并替换成匹配结果中的匹配组。在 linkify-comment 的例子中, 所有数字符号(#)后跟着连续的数字(\d+)被括号捕获,在替代时作为 $1 提供。 如果传入一个正则表达式来匹配,也可以提供一个函数作为替换,而不是一个字符串。 在 Clojure 中,如果能传入一个函数作为参数,整个世界都由你作主了。可以在可复用 (并且可测试)的函数中捕获替换,根据情况传入不同的函数,甚至传入一个映射表来控 制替换: ;; linkify -comment 重写,替换时使用独立的函数 (defn linkify [repo [full-match id]] (str "[" full-match "](https://github.com/" repo "/issues/" id ")")) (defn linkify-comment [repo comment] (clojure.string/replace comment #"#(\d+)" (partial linkify repo))) 如果你以前没用过正则表达式,用一下就会立刻喜欢上它。在修改字符串时,正则表达式 原生数据 | 15 是强大的工具,非常灵活。就像所有强大的新工具那样,很容易被滥用。因为它们简明而 紧凑的语法,很容易导致正则表达式既难以解读,又很容易犯错。应该谨慎使用正则表达 式,并且只在完全理解其语法时才使用。 要学习和掌握正则表达式的语法,Jeffrey Friedl 的 Mastering Regular Expressions, 3 rd ed. (O’Reilly) 是一本很好的书。 参阅 • 1.7 节“按模式查找字符串”。 • clojure.string/replace-first,该函数与 clojure.string/replace 几乎一样,但只替 换第一次匹配。 • java.lang.Pattern 的 API 文 档(http://docs.oracle.com/javase/7/docs/api/java/util/regex/ Pattern.html),其中定义了 Java 支持的正则表达式语法(Clojure 的正则表达式一样)。 1.10 将字符串切分成部分 作者:Ryan Neufeld 问题 需要将字符串切分成若干部分。 解决方案 使用 clojure.string/split,将字符串切分成一组子串。split 接受两个参数,一个是待切 分的字符串,另一个是切分依据的正则表达式: (clojure.string/split "HEADER1,HEADER2,HEADER3" #",") ;; -> ["HEADER1" "HEADER2" "HEADER3"] (clojure.string/split "Spaces Newlines\n\n" #"\s+") ;; -> ["Spaces" "Newlines"] 讨论 除了简单地按正则表达式切分之外,split 允许你控制切分的次数。你可以利用可选的 limit 参数做到这一点。limit 最明显的效果就是限制结果集合里值的个数。也就是说, limit 并非总像你期望的那样工作,而且即使不提供这个参数,也是有意义的。 没有 limit 时,split 函数将返回所有可能的切分,但排除尾部的空匹配: ;; 按空白字符切分,没有指明 limit,执行了隐式的去除空白字符操作 16 | 第 1 章 (clojure.string/split "field1 field2 field3 " #"\s+") ;; -> ["field1" "field2" "field3"] 如果你想要的就是所有匹配,包括尾部的空匹配,那么可以将 limit 指定为 -1。 ;; 在 CSV 解析时,行末的空匹配仍然是有意义的 (clojure.string/split "ryan,neufeld," #"," -1) ;; -> ["ryan" "neufeld" ""] 将 limit 指定为其他正数,导致 split 最多返回 limit 个子串。 (def data-delimiters #"[ :-]") ;; 没有 l imit,按所有定界符切分 (clojure.string/split "2013-04-05 14:39" data-delimiters) ;; -> ["2013" "04" "05" "14" "39"] ;; Limit 为 1,返回包含这个字符串的集合 (clojure.string/split "2013-04-05 14:39" data-delimiters 1) ;; -> ["2013-04-05 14:39"] ;; Limit 为 2 (clojure.string/split "2013-04-05 14:39" data-delimiters 2) ;; -> ["2013" "04-05 14:39"] ;; Limit 为 100 (clojure.string/split "2013-04-05 14:39" data-delimiters 100) ;; -> ["2013" "04" "05" "14" "39"] 参阅 • clojure.string 命名空间 API 文档(http://clojure.github.io/clojure/clojure.string-api.html)。 • 1.7 节“按模式查找字符串”。 • 1.8 节“利用正则表达式从字符串中取出值”。 1.11 基于数量为字符串加复数 作者:Ryan Neufeld 问题 需要基于数量为单词添加复数,如“0 eggs”或“1 chicken”。 解决方案 如果需要得到 Ruby on Rails 风格的复数,请使用 Roman Scherer 的 inflections 库(https:// github.com/r0man/inflections-clj)。 原生数据 | 17 要继续这个实例,先用 lein-try 开始 REPL3: $ lein try inflections 使用 inflections.core/pluralize 时带一个计数参数,如果计数不为 1,尝试给出单词的 复数: (require '[inflections.core :as inf]) (inf/ pluralize 1 "monkey") ;; -> "1 monkey" (inf/ pluralize 12 "monkey") ;; -> "12 monkeys" 如果有特殊或非标准的复数形式,可以为 pluralize 提供第 3 个可选参数,指定自己的复 数形式: (inf/pluralize 1 "box" "boxen") ;; -> "1 box" (inf/ pluralize 3 "box" "boxen") ;; -> "3 boxen" 讨论 对于展现给用户的文字,变形是关键。让程序或网站的输出人性化,有益于建立值得信任 的专业形象。对于用户友好的、人性化的文字, Ruby on Rails(http://rubyonrails.org/)用 它的 Active Support::Inflections 类,建立了一个黄金标准。 Inflections#pluralize 就 是这样一种变形,但 Inflections 充满了听起来讨人喜爱的方法,以“ ize”结尾,改变字 符串的形态。在 Clojure 环境中,Inflections 几乎提供了所有这些功能。 Inflections 库中有两个有趣的函数,plural 和 singular。这两个函数有点像单词复数的 upper-case 和 lower-case。plural 将单词转变成复数形式,singular 将单词转换成单数 形式 。这些转换基于inflections.plural 中的一些规则。你可以利用 inflections.core/ plural,添加自己的复数规则! (inf/plural "box") ;; -> "boxes" ;; 以 ' ox' 结尾的单词复数加 'en'(而不是 'es') (inf/ plural! #"(ox)(?i)$" "$1en") (inf/ plural "box") ;; -> "boxen" 注 3:如果还没有安装 lein-try,请按照前言中“我们的金童 lein-try”的指令安装。
还剩40页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 3 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

ncnf

贡献于2015-11-27

下载需要 3 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf