庄周梦蝶

生活、程序、未来

跑步两月记

| Comments

扯淡篇

4月底开始恢复跑步,体检检查出轻度脂肪肝,刚步入 30 ,这个征兆不是特别好。因此,还是恢复跑步。原来在北京,是不敢跑懒得跑,雾霾太厉害,并且离奥森公园还是有点距离。到了昆山,虽然空气还是不理想,但是比北京还是好点,加上离家不远有个河边公园,人少车少,跑步的理想场所。

跑到现在,加上三周前开始去健身房锻炼,原来是不敢去健身房的,以为肌肉猛男很多,我这样的胖子估计很丢人。其实呢,健身房里猛男不少,胖子更不少,完全没人在意你。跑步的成绩从原来的 8,9 分钟每公里(跟快走差不多)的平均速度,还心率狂飙的差劲情况,进展到现在可以定速 6 分钟每公里保持个接近一个小时(也就是 10 公里在一个小时左右跑完),心率维持在150左右,自己都很得意。当然,跟跑步圣经里的老马比起来,那差距还是很大。

最近三周没去跑公路了,一方面是夏天到了,在户外心率会狂飙,有中暑晕倒的可能,另一方面是空气开始糟糕。都在健身房跑步机上跑。还有就是请了私教,尝试一个课时(12节)的训练,加强下力量,顺便减脂。总体来讲,体验不错,考虑再续一个季度。不过价格真的很贵,需要说服老婆支持。

既然决心坚持下去,也给自己定了个目标,11 月份一定要去参加杭州或者上海马拉松的半程比赛(21公里)。以我现在的耐力,完成比赛应该没有问题,成绩不在乎,重在参与嘛(弱者的自我安慰)。还没有尝试过 15 公里 和 20 公里的 LSD 训练,争取这个月内试一次。

装备篇

作为装备控,没跑多远的路,就开始在跑步圣经等论坛上看各种装备评测,最终备齐了一套,对我这样的初级跑着还是奢侈了一点。

  • GPS 手表:佳明 FR 620,配心率带。专业跑步手表,除了心率、距离、配速、卡路里之外,还有步频、垂直幅度、触底时间、虚拟伙伴和最大耗氧量测试等更专业的统计数据。如果准备长期跑,入这样一个跑步手表还是必须的。
  • 佳明 FootPod 计步器,因为在跑步机上跑,虽然 FR620 的手表和心率带都内置加速器,但是距离偏差还是比较大,因此还是淘宝上入了个计步器,校准之后相对精确了一些。
  • 跑鞋,片面强调跑鞋作用不可信,但是一双好的鞋子来保护脆弱的膝盖还是必要的,当然,强者都是赤脚跑步。在北京的时候就买了双 adidas marathon 10,这样的长跑鞋对我这样的胖子来说其实不合适。这鞋子回弹不错,相对较硬了点。去健身房之后,一口气入了两双跑鞋: Ascis nimbus 15 和 Mizuno wave legend。前者适合我这种外翻的脚型,并且是我这辈子穿过最舒服的鞋子,按照夸张点的说法是穿着它就像一路踩着“狗屎”走路。后者相对较硬,只有跑起来才能感觉到回弹十足,我穿着他跟着教练练器械。Nimbus 担心损耗很快,还是舍不得穿着健身,只用来跑步。
  • 运动衣裤:淘宝上随便淘的耐克跑步短裤和T-shirt,这个没啥特别的,只要合身、透气,穿着舒服就好。

回忆篇

很多人说跑步是孤独者的运动。因为一个人跑,可能会比较闷。不过这个对我从来不是问题,独处是一种习惯。我在初中的时候,三年都坚持晨跑,天还没亮,就沿着乡村公路跑过坟场、山丘,跑步给我的生活带来了很多变化。工作后,在广州的时候也坚持夜跑天河公园,去了杭州才没有坚持运动,主要住的地方实在没有什么公园场地,我又不愿意在公路跑,也还不敢去健身房。在帝都更不用说了,雾霾加上工作繁忙,又没有心思锻炼,更是彻底荒废。眼看着体重直线上升。

现在恢复跑步,对我来说不是特别困难的事情,享受跑步时候的呼吸和汗水,希望能 keep running 下去。

最近做的一些 clojure 开源项目

| Comments

最近总结下最近的一些开发经验,形成几个 clojure 的开源项目:

clj.qrcode:二维码生成

1
2
3
4
5
6
7
8
9
10
(use 'clj.qrgen)
(as-file (from "hello world"))
(as-bytes (from "hello world"))
(from (vcard "John Doe"
             :email "john.doe@example.org"
             :address "John Doe Street 1, 5678 Doestown"
             :title "Mister"
             :company "John Doe Inc."
             :phonenumber "1234"
             :website "www.example.org"))

secure-rand:安全随机数生成器

1
2
3
4
5
6
7
8
9
(ns test
  (:refer-clojure :exclude [rand rand-int rand-nth])
  (:use [secure-rand.core :only [rand rand-int rand-nth]]))

(rand)
(rand 10)
(rand-int 100)
(rand-nth (range 10))
(secure-rand.core/base64 32)

clj.qiniu:七牛云存储 SDK

1
2
3
(require '[clj.qiniu :as qiniu])
(qiniu/set-config! :access-key "your qiniu access key" :secret-key "your qiniu secret key")
(qiniu/upload-bucket bucket key file)

希望能给一些使用 clojure 开发的朋友带来帮助。

Clojure Web 编程之安全篇

| Comments

最近关注这方面稍微多了点,大概总结下。

基本原则

浏览器的安全机制:

  • 同源策略:host、port、protocol、sub domain都能影响。
  • 沙箱模型,比如 Chrome 的多进程模型
  • 恶意网址拦截,现代浏览器基本上都有提供,Google也提供了开发API查询黑名单。

Clojure的Web开发本质上是基于 Java 的 Servlet 模型,因此也同样遵循 Java 的安全编程模型。这里主要描述 Clojure 里的常见防御策略,具体的漏洞请参考《白帽子讲Web安全》等书籍。

Swift 编程之闭包(翻译)

| Comments

‌‌最近Apple新出的 Swift 语言非常火,我们公司也在陆续组织翻译《The swift programming language》 这本官方电子书,为公司博客增加一些人气。这篇博客就是我负责的其中之一的产物。在公司博客放了一份,这里也放一份。

闭包

闭包(Closures)是可以在你的代码里传递和使用的,自包含的功能代码块。Swift 里的闭包跟 C 和 Objective-C 里的 block 类似,也就是其他语言里的所谓的 lambda 。

闭包可以从定义的上下文(Context)里捕获和存储常量或者变量的引用。这被称为“闭合”(closing over)了这些常量和变量,这也是“闭包”名称的由来。Swift 帮你处理了所有捕获相关的内存管理。

注意

1
不用担心你不理解“捕获”这个概念。会在下文里详细解释。

函数一章中介绍的全局和嵌套函数,其实是闭包的特殊形式,闭包表现为三种形式:

  • 拥有一个函数名,并且不捕获任何值的闭包称为全局函数。
  • 拥有一个函数名,并且从外部函数捕获值的闭包,称为嵌套函数。
  • 使用轻量级语法编写,并且没有命名的闭包表达式,可以从周围上下文中捕获值

Swift 的闭包表达式语法拥有一个干净、清晰风格,针对大多数应用场景里做了优化,倾向于简明、整洁的语法:

  • 根据上下文信息,为参数和返回值的自动做类型推断。
  • 单一闭包表达式的隐式返回。
  • 参数的速记法。
  • 拖尾闭包(Trailing Closure)语法。

下面我们开始详细介绍。

Clojure里使用curator做Leader节点选举

| Comments

Curator 框架刚出来的时候,我就用它帮 Storm 重构了 zookeeper 模块。使用 zookeeper,如果用 java 语言,curator 框架是最佳选择。

最近在做一个节点选举的功能,在几个节点之间选举一个 leader 来跑一个独占服务。原来的方案是直接利用 hostname 匹配,跟配置的 hostname 一致的固定某台机器来执行。Failover 靠人肉和自动化脚本。为了做让 failover 自动化,自动选举节点是更好的方案。理所当然,我尝试在 clojure 里使用 curator 框架。 curator 提供了 Leader Election功能,我要做的只是封装这个Java API,在clojure里更好地使用。

首先,肯定是继承LeaderSelectorListenerAdapter 来实现 LeaderSelectorListener ,监听本节点是否成功获取 leadership,当本节点成功被选举的时候,LeaderSelectorListenertakeLeadership方法将调用,你应该阻塞这个方法,直到:

  • 你想释放自己的 leadership,比如节点重启了,或者独占服务异常了,想重新发起选举。
  • Zookeeper session 状态异常,比如 SUSPENDED 或者 LOST ,需要解除阻塞,重新发起选举。

继承LeaderSelectorListenerAdapter我们用 proxy 函数,阻塞呢?Clojure提供了promise,当 promise 没有值的时候, deref 调用会阻塞, promise 本质上是一个CountDownLatch。我们就利用它来阻塞 takeLeadership 方法,封装下这个过程:

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
;;保存curator框架客户端的atom
(defonce ^:private curator-framework (atom nil))
;;保存LeaderSelector列表的atom
(defonce ^:private leader-selectors (atom []))

(defn elect-leader
   "参与leader选举,如果被选举为leader,调用aquire函数,释放leadership的时候调用release函数。
   path表示参与选举节点共同使用的zookeeper上的路径。"
  [path aquire release]
  (if (nil? @curator-framework)
    (throw (IllegalStateException. "Please call start at first."))
    (let [p (promise)
          listener (proxy [LeaderSelectorListenerAdapter] []
                                             (takeLeadership [fk]
                                               (aquire fk)
                                               (try
                                                 @p
                                                 (catch InterruptedException _
                                                   (release fk)))))
          selector (LeaderSelector. @curator-framework path listener)]
      (swap! leader-selectors conj [p selector])
      (doto selector
        ;;u/hostname是一个工具方法,用于获取本机hostname,方便调试和分析
        (.setId (u/hostname))
        (.autoRequeue)
        (.start)))))

关键性的代码就是takeLeadership方法的实现,传入的aquire方法最好是非阻塞或者可中断(释放 leadership 的时候,curator 会尝试中断 takeLeadership 方法,解除阻塞)。为了保持独占,我们对p这个 promise 做阻塞性的deref调用,如果阻塞被中断 catch 到InterruptedException,我们就调用传入的release方法,你可以在release里做一些释放 leadership 后的资源清理的工作。

最后,奉上一些创建和销毁 curator client 的辅助代码:

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
(defn ^CuratorFramework mk-client
  "Create curator framework client."
  []
  (let [builder (CuratorFrameworkFactory/builder)]
    (doto builder
      (.connectString (env :zk-connect "localhost:2181"))
      (.connectionTimeoutMs (config/int-var :zk-connect-timeout 10000))
      (.sessionTimeoutMs (config/int-var :zk-session-timeout 10000))
      (.retryPolicy (ExponentialBackoffRetry. (config/int-var :zk-retry-interval 1000) (config/int-var :zk-retry-times 5))))
    (let [fk (.build builder)]
      (.start fk)
      fk)))

(defn start
  "Initialize curator framework client."
  []
  (reset! curator-framework (mk-client)))


(defn stop
  "Stop all elections,release leadership if they have.
  Then shutdown the curator framework client."
  []
  (when-let [promise-selectors (seq @leader-selectors)]
    (doseq [[p s] promise-selectors]
      (deliver p true)
      (.close s)))
  (reset! leader-selectors [])
  (when @curator-framework
    (.close @curator-framework))
  (reset! curator-framework nil))

关键性的stop方法,使用deliver给 promise 传递一个true值来解除阻塞,从而释放了leadership,让其他节点发起新一轮选举。

拥抱 Octopress,再见 WordPress

| Comments

花了点时间,从 WordPress 迁移到 Octopress,跟我们公司的官方博客刚好相反 :D 。主要是实在折腾不好WP的那一堆插件。也习惯用 mou.app 写博客了。

迁移过程中,这个工具 wordpress-to-jekyll-exporter 帮了大忙,自动从 WordPress 导出数据成 Octopress 需要的格式,相当方便。

使用了 solarized-octopress-theme 主题,再折腾下 多说 评论框,基本就完成了。

继续博客之旅。

Volatile in clojure

| Comments

Java 1.5重新理顺了内存模型,使得volatile关键字的行为更清晰和明确。怎么在Java里使用volatile,可以看看这篇旧文《Java理论与实践:正确使用Volatile》。

在Clojure里又怎么声明一个volatile变量呢?答案是volatile-mutable的metadata。一段Java代码:

public class Person {
  volatile long age;
}

等价的Clojure代码是:

(deftype Person [^:volatile-mutable ^long age])

也可以写成:

(deftype Person [^{:volatile-mutable true :tag long} age])

具体到编译器

boolean isVolatile(LocalBinding lb){
return RT.booleanCast(RT.contains(fields, lb.sym)) &&
           RT.booleanCast(RT.get(lb.sym.meta(), Keyword.intern("volatile-mutable")));
}

如果有volatile-mutable标记,就给access modifier加上AC_VOLATILE。

Type hint in macro

| Comments

Type hint(类型提示)是Clojure性能优化的关键手段之一,关于type hint可以看看我过去写的这篇博客《Clojure笔记:用好Type Hint》。

不过旧文里没有提到怎么在宏里面使用type hint,我们试试:

user=> (set! *warn-on-reflection* true)
true
user=> (defmacro str-len
         [s]
         `(.length ^String ~s))
#'user/str-len
user=> (str-len "test")
4
user=> (def a "test")
#'user/a
user=> (str-len a)
Reflection warning, NO_SOURCE_PATH:11:1 - reference to field length can't be resolved.
4

我们打开了反射警告,定义了一个宏用来获取参数字符串的长度,里面用了Type Hint标记s的类型为String。然后获取字符串”test”的长度。当直接传入”test”的没有反射警告,返回长度4。当绑定了”test”到a,然后获取a长度的时候,反射警告产生了reference to field length can't be resolved

为什么字面量(literal)的字符串”test”就不会产生反射警告,而var a却会呢?道理很简单,macro会在编译的时候展开,因此(str-len "test")等价于(.length "test"),这个表达式无需type hint就可以得到答案4。事实上如果你给字面量加上type hint,clojure会报错:

user=> (.length ^String "test")
IllegalArgumentException Metadata can only be applied to IMetas....

最关键的是,str-len里的type hint其实被“忽略”了,所以当定义a的时候去调用str-len,反射仍然产生。

正确在宏里使用type hint的方法是使用metadata,所谓type hint宏本质上是给var的metadata加上tag值,因此我们可以用with-meta函数来给符号s加上type hint:

user=> (defmacro str-len
     [s]
     `(.length ~(with-meta s {:tag `String})))
user=> (str-len a)
4

Cool! It works。但是:

user=> (str-len "test")
ClassCastException java.lang.String cannot be cast to clojure.lang.IObj

这是因为with-meta只能作用在clojure的对象上,而Java的String是没有实现IObj接口的,因此无法打上tag。有没有两全其美的办法呢?答案是有的,我们知道,任何问题都可以通过抽象一层来解决:

(defmacro str-len
  [s]
  `(let [^String x# ~s]
         (.length x#)))

引入中间symbol来打上tag。

Ejabberd作为推送服务的优化手段

| Comments

AVOS Cloud目前还在用Ejabberd做Android的消息推送服务。当时选择Ejabberd,是因为Ejabberd是一个发展很长时间的XMPP实现,并且基于Erlang,设想能在我们自主研发的Push Server起来之间顶上一段时间。

我们自主研发的Push Server预计本月中旬就上线了。但是Ejabberd却先顶不住了。Ejabberd做推送,本身就有劣势,比如XMPP协议的冗余,XMPP协议本来就是IM协议,对推送这个简单的场景还是太复杂了一些。Ejabberd Cluster对于IM这样的场景很有优势,对于一个Ejabberd Cluster内机器来说,A用户连在A服务器,B用户连在B服务器,两者之间的通讯,Ejabberd都帮你处理了,会自动路由AB之间的消息。Ejabberd会在整个Cluster内每个节点维护session数据,每次session的变更都会在节点之间复制。但是对于推送服务来说,没有刚才提到的AB用户的通讯问题,都是服务端往client推送数据,client没有上行的数据,这个session维护的代价反而是没有必要了。

因此,使用Ejabberd推送的第一条优化手段: 不使用Ejabberd Cluster。反而采用多个独立节点的结构,每个节点之间都是独立,不组成集群。因为节点之间独立,用户数据却还需要共享,那么放弃mnesia,而采用odbc数据库的方式存储用户数据就成为自然的选择了。

那么在采用多节点之后,会出现一个问题?推送服务怎么知道应该往哪个节点推送消息,换句话说,推送服务怎么知道要推送的设备连在哪台服务器上?我们的解决办法是引入一个Router服务,它来告诉推送服务和终端设备链接到哪台服务,采用一致性哈希算法来分配连接,减少节点变更带来的影响。最后,我们使用一个简陋但是简单的办法来感知节点是否有效。这个简陋的办法就是使用crontab脚本每隔几秒向router服务发送该机器上ejabberd端口处于ESTABLISHED或LISTEN状态的链接数(netstat命令)。

优化第二条:将数据库服务跑在单独的服务器上。我们使用MySQL,MySQL的优化这里就不提了,innodb配置参数,并发线程、事务级别等等。最好也对数据库做个异步slave备份。

优化第三条,去除不必要的module,我们只用到ejabberd的数据下行和用户账户管理功能,其他模块都可以去掉,下面是一个可以去掉的模块清单:

mod_caps
mod_pubsub
mod_vcard_odbc
mod_roster_odbc
mod_privacy_odbc
mod_proxy65
mod_disco
mod_echo
mod_http_fileserver
mod_shared_roster
mod_service_log
mod_irc
mod_muc
mod_vcard_odbc
mod_adhoc
mod_configure
mod_last

如果你想记录每个账户最后一次连接的时间,可以保留mod_last模块,使用odbc方式记录。

优化第四点,ejabberd.cfg的参数优化(一般在/etc/ejabberd/ejabberd.cfg),包括:

  • loglevel, 建议设置成3,只打印warn级别以上日志。
  • listen模块,ejabberd_c2s的backlog参数,设置TCP backlog队列长度,建议设置得大一些。因为移动网络下,连接断开重连相对频繁,backlog队列较大可以保证服务端的连接accept能力。
  • shaper模块,设置流量控制,默认的normal是1.000 B/s远远不能满足需求。
  • max_fsm_queue参数,控制出队消息队列长度,建议不要设置(undefined),或者设置得较大。
  • odbc相关参数: odbc_pool_size控制连接池大小,设置大一些没关系。odbc_keepalive_interval检测连接是否存活,跟MySQL的wait_timeout保持一致,odbc_start_interval设置重连间隔,默认即可。
  • 权限控制(Access)相关参数:max_user_offline_messages设置最大保存的离线消息数目,根据你的应用需要,设置一个合理的值,我们是设置成5。max_user_sessions设置每个用户最多登录的session数目,建议设置小于等于2,防止重复登录。rigister权限控制给admin使用,预先创建一批admin账号,授予创建用户的权限。每个设备用一个uuid标示,通过这些admin账号注册到ejabberd上。

优化第五条,erlang运行时参数优化(一般在/etc/default/ejabberd文件):

  • POLL=true,开启网络层启用Polling,linux一般是epoll,BSD系统是kqueue。编译Erlang的时候必须设置启用才能启用此选项。
  • SMP=auto|enable,设置成auto或者enable,测试表明,启用SMP在多核机器上能极大地改善吞吐量。
  • ERL_MAX_PORTS,每个TCP链接都将耗费2到3个的erlang port,因此这个参数也设置得大一些,>=30万。
  • PROCESSES,进程数目尽管设置得大一些,最好是ERL_MAX_PORTS的数倍。
  • ERL_MAX_ETS_TABLES: ets表数目,如果日志中看到too many db tables的时候,可以增大此参数,默认1400。

优化第六条,TCP内核参数的优化,注意下列参数:

net.ipv4.tcp_syncookies=1
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_tw_recycle=0
net.ipv4.tcp_timestamps=1
net.ipv4.tcp_max_syn_backlog=65535
net.ipv4.tcp_fin_timeout=30

具体含义请自行Google,切记,不要同时开启tcp_tw_recycle字段和tcp_timestamps字段,建议开启tcp_timestamps,原因参考雕梁哥的这篇博客

如果有开启防火墙iptables,还需要注意net.netfilter.nf_conntrack_max参数,这是用来限制防火墙跟踪链接的状态表的大小,对于推送服务来说保持大量长连接是很容易超过默认的65535的,如何配置请参考这篇文章

最后,更多的优化手段,其实就需要深入ejabberd源码了,比如存储的优化,使用redis作为离线消息存储等;erlang语言和vm层面的优化,系统层面的优化(CPU绑定、内核参数进一步调整等)。这就超过我目前的能力范围了,我们的新推送服务基于Netty和Clojure开发,以后应该不会再跟ejabberd打交道了。整体上来说,ejabberd作为一个早期推送方案,支撑百万级别的推送是没有问题的,虽然可能成本会稍微高了一些。

Clojure 1.5学习综述

| Comments

这博客来的有点晚,clojure 1.5都发布一年多了。不过我最近才将系统的clojure版本从1.4升级到1.5.1,因此需要重新看一遍clojure 1.5带来的新东西。

Reducer库

要说clojure 1.5最大的变化应该说是引入了reducer库。单纯从这个库提供的函数如map,reduce,filter,remove,flattern等函数来说,跟clojure.core提供的这些函数的功能基本上是一致的,除了返回值略有不同。

为什么要引入reducer库?或者说引入reducer库带来的好处是什么?我想主要是性能。我们都知道clojure的数据结构,如vector、list、hash-map等都是immutable——每次更新都将产生一个新的数据结构,而不是修改其中的某个节点。为了减少不可变带来的开销,clojure将这些集合类都实现为持久数据结构(persist),每次更新都将复用大部分“老”的数据;并且引入了LazySeq,将map,filter等高阶函数开销分摊到每个迭代步骤中,而不是重复的生成中间数据结构;每个迭代步骤还是批量的,一次一个chunk(32个元素);通过这些手段来减少不可变数据结构的开销。

但是呢,Lazy和Chunk都引入了额外的对象创建开销,并且很多时候我们最终都要realized整个数据结构(比如反馈查询结果给用户,查询结果可能是个LazySeq),lazy的额外开销是不必要的;其次,map,reduce,remove等这些高阶函数都基于Sequence的抽象之上,基于Sequence提供的first和next操作来遍历迭代数据结构,这两个操作对于不同的数据结构来说未必是最佳的迭代方式,并且需要将其他结构都转为sequence,也是一个额外的开销。例如对于字符串,最好的迭代其实是使用String的charAt来迭代字符,但是map等函数都会调用seq函数将字符串转成Sequence,多了一层包装,然后统一以first/next的方式来迭代处理。

综上所述,core库提供的这些高阶函数,仍然是以“流”的方式在转换数据结构。而reducers则不是。它转换的是想要作用的“函数”,因此它是完全函数式的,而非迭代式。比如(map f coll),clojure.core转换的是coll集合成另一个集合,将f函数作用在coll上得到一个新集合。而reducer的map函数则是转换f函数,将f转换为支持reduce的方式来迭代数据结构,满足下列条件:

  • (f) 返回一个identity value。
  • (f ret [k] v)作用在reduce值和每个item上。

其他filter,remove也是这样,转换的都是f函数,而非集合。最终这些高阶函数生成一个所谓reducible,可以最终调用reduce或者into函数来得到“真正”的结果。关于这个实现,推荐看这篇博客源码

其次,reducer库还让每个数据结构自己决定处理方式,比如刚才提到的,String的最佳方式就是直接使用chartAt,java数组的最佳方式就是利用一系列array函数来原生操作数组等等,这些都放在protocol源码 里。现在core库里的reduce和into也是调用这些优化过的方式。

最后,reduce也是以eager方式,而非lazy的方式来生成集合,这也避免了lazy的开销。

Reducer库还基于fork/join框架提供了fold函数来并发处理数据集合,默认切分的子集合大小是512,fold接收reduce和combine函数,其中combine函数需要满足:

  • (combinef)返回一个初始值(identity value)
  • combinef需要是可结合的,满足算术结合律,例如加法运算。

fold的运行过程是并发的,但是结果将保持有序。

关于reducer库的例子,我这里就不举了,有兴趣看看官方博客和这篇博客就清楚了。

新的thread宏

clojure 1.5引入了几个新的thread宏,cond->和cond->>,as->以及some->和some->>等。这些也自己doc看看文档和例子就好,没有太多好提的,都是为了减少重复代码,提高代码的可读性而引入的。我对clojure标准库引入这些新宏持保留态度,核心库还是维持在较小的规模上更合适,不能因为哪个方便就随便加入。clojure 1.6貌似没有再加入一些新的thread宏。

gen-class和protocol的相关改进

  • gen-class增加指令exposes-methods导出protocted并final的方法,让通过gen-class生成的子类可以访问。
  • gen-class的constructors可以添加annotation元信息。
  • 允许定义标记protocol,类似java里的mark interface,没有任何方法,纯粹一个接口

看例子:

user=> (defprotocol M)
M
user=> (deftype T [a] M)
user.T
user=> (satisfies? M (T. 1))
true

hash-set和hash-map接收重复参数

在clojure 1.5之前,下面这个调用将失败:

user=> (hash-map :a 1 :b 2 :a 3)
IllegalArgumentException Duplicate key: :a

hash-set也是类似,报错信息告诉你重复的key,在clojure 1.5里这样可以了:

user=> (hash-map :a 1 :b 2 :a 3)
{:a 3, :b 2}    

但是字面量的仍然不行:

user=> #{1 1 2}
IllegalArgumentException Duplicate key: 1    

哪怕你是通过表达式生成:

user=> (def x 2)
#'user/x
user=> (def y 4)
#'user/y
user=> #{(inc x) (dec y)}
IllegalArgumentException Duplicate key: 3    

edn库

新的clojure.edn库,读取解析edn格式的数据,edn格式用在了Rich公司的主打产品Datomic数据库等。

性能改进

值得一提的一个是Multimethod使用读写锁来保护类型-方法派发表,原来是一个synchronized锁保护起来。读写锁对于multimethod表这种读远远多于写(一般不会再动态添加method)的场景,能大大地提升性能。

更多信息

请看完整的changelog

clojure 1.6处于alpha状态,有兴趣也可以看看它的changelog。

当然,还有core.async

async库应该说是使用clojure 1.5的一大理由,不过这是另一篇博客了……