使用Enlive作为模板引擎

在所有的clojure web开发例子里,对模板的介绍都很少。很多的简单例子都是以hiccup作为页面生成的手段。hiccup是个clojure的html DSL,例子里用这样的DSL生成页面确实很酷,可是他是real world吗,当然不是。

好在clojure世界里早就有了enlive,它不仅是一个通过css selector解析html的库,本身也可以作为模板引擎应用在web开发中。我不知道这种通过css selector的方式是否是enlive首创,不过他实在是非常新颖独特,而且平滑了页面设计和程序的集成。

例如这样一个模板 index.html:

<div id="cc">Sample Text</div>

在clojure程序中,使用enlive的deftemplate

(deftemplate index "index.html"
  [ctx]
  [:div#cc] (content (:data ctx)))

在控制器里,可以很MVC地渲染页面

(index {:data "rendered text"})

除了content用于渲染文本,还有html-content可以渲染含html标签的内容,以及set-attr用来修改页面元素的属性。

和传统的模板引擎相比,最大的不同是enlive里没有嵌入模板的直观的控制流,没有循环和条件判断,但是并非不可实现。

循环输出一组list

页面 list.html

<ul id="the-list">
<li class="list-item"></li>
</ul>

定义一个enlive的snippet

(defsnippet item-model "page.html" [:.list-item]
  [ctx]
  [:.list-item] (content (:data ctx)))

在页面模板里

(deftemplate list-page "list.html"
  [ctx]
  [:ul#the-list] (content (map item-model (:some-list ctx))))

这样在页面里列表项会被循环输出,而在页面设计时这里可以放任意个li,并且直接交给后台作为模板。

条件判断

页面,设计时显示所有的内容 msg.html

<span id="msg">只在一定条件下显示</span>

在模板中通过clojure的if进行判断

(deftemplate msg "msg.html"
  [ctx]
  [:span#msg] (if (:show ctx) identity (html-content "")))

解决了这两个问题,基本上用enlive作为模板引擎就没有障碍了。不过enlive也有一点小问题,其一可能是性能的问题,方便的selector显然要比传统的模板语言消耗更多的CPU。另外,在开发过程里,页面文件在服务器启动后不能热加载,修改页面必须重启ring才能看到。也许有时间的话,可以给它加一个reload选项。

使用Clojure Thread Macro的心得

Thread Macro是clojure里一个很强大的宏,他帮助你简化嵌套函数的调用,比如

(str (inc (count [:a :b])))

就可以利用thread macro简写成

(-> [:a :b] count inc str)

->>和->类似,区别在于->>把值作为函数的最后一个参数传入。

简单的功能介绍完了,接下来就遇到问题了。我需要功能,能够接受一个或多个函数,然后把这些函数组成一个pipeline。这时很自然想到->是一个好帮手。也许只需要一个类似这样的form就可以了: #(apply -> % [funcs])。结果失败了,因为->是个宏,所以根本不能用apply。于是想到有apply-macro吗?有,或者说曾经有过。在contrib中曾经有一个apply-macro,不过被强烈不推荐使用。到这里,这条路堵死了,惟一的办法就是把->放到API之外,放到用户代码里去。

放到用户代码里,你需要写一个详细的说明文档并且告诉用户他必须这么做。然而在clojure世界里有一个更好的办法就是再写一个宏把->包装起来。这么做看似多此一举,其实是保持了API的一致性。通过宏,我们可以把自己的API延伸到用户代码中去。或者说,通过一个类似DSL的宏,给一些并不优雅的API一个缓冲,也为API日后的演化留下空间。

这里还要扯开一句关于宏的开发。clojure中所谓code is data,主要就是体现在宏里。原本在多数其他语言里,宏是不能求值的。但是在clojure里,由于code is data的缘故,宏是可以求值的。所有的输入数据都是list,你可以做first/reverse这样的操作。但是有一点要注意的是,宏中求得的值和代码里的值是不一样的。例如{:a inc}这样一个字面量,在宏里是可以通过:a做求值的,然后这里得到的并非一个函数(function),而是一个符号(symbol)。再者,调试宏的时候你可能会被这样的结果困惑:

(defmacro a [f] (println (:a f)))
(a {:a 1}) ; ==> prints 1
(def b {:a 1})
(a b) ; ==> prints nil

字面量可以,同值的变量就不行了。原因还是那句,宏里不能求值。

继续谈->。这个宏其实远没有你想象的那么驯服。遇到复杂一点的情况:

(def m {:a inc})
(-> 2 (get m :a) str)

这个写法对吗?str是个函数,(get m :a)返回的是inc也是个函数,貌似正确。运行之后却报错get的参数数量错误。所以千万不要忘了->是个宏,(get m :a)这里是不会求值到inc的,直接作为一个list被宏吞下去。在宏里只能通过符号的组合变化来生成代码,那么一不小心,就没有inc什么事了。

于是,你可能想到这里需要一个确切的函数,就好比这样:

(def m {:a inc})
(-> 2 (fn [x] ((get m :a) x)))

也许这样就好多了,我们放了一个匿名函数,并不要求宏去求值,因为这个匿名函数会被宏生成到新的代码里。里面的get也会在运行时求值。看似没什么问题,可是一运行还是没有期待的结果,居然返回了一个匿名函数!而对这个匿名函数求值得到的也是一个错误的结果!简直有点无厘头了。

呵呵,不故弄玄虚了。我们用macroexpand看看发生了什么。

这是用匿名函数包装以前

(macroexpand-1 '(-> 2 (get m :a)))
(get 2 m :a)

->居然只是简单地把2放到了get这个form里面!

再看看用匿名函数包装后的结果

(macroexpand-1 '(-> 2 (fn [x] ((get m :a) x))))
(fn 2 [x] ((get m :a) x))

和刚才一样,2被放到了第一个form的第一个参数位置!得到的是一个非法的form。

那么既然->只是简单地把第一个参数放到后面form的首个参数的位置,那么这个宏正确的使用方法其实是

(def m {:a inc})
(-> 2 ((fn [x] ((get m :a) x))))

再加一层括号!

(macroexpand-1 '(-> 2 ((fn [x] ((get m :a) x)))))
((fn [x] ((get m :a) x)) 2)

可见,->虽然是个功能强大的宏,但宏终归只是宏,和函数的区别是明显的。在使用的时候,不能完全按照函数的习惯。

如果你想了解实际的代码,可以参考slacker 0.3.0里的这个interceptor框架:
https://github.com/sunng87/slacker/blob/master/src/slacker/interceptor.clj
上面提到的难处,多半也都是在开发这个框架时亲身经历的。

Clojure RPC, prototyping and early thoughts

Last week, I prototyped an RPC framework, slacker, by clojure and for clojure.

What I did ?

Suppose you have a sets of clojure functions to expose. Define them under a namespace:

(ns slapi)
(defn timestamp []
  (System/currentTimeMillis))

;; ...more functions

Expose the namespace with slacker server, on port 2104:

(use 'slacker.server)
(start-slacker-server (the-ns 'slapi) 2104)

On the client side, we use the `defremote` macro to create a facade for `timestamp` function. This API will keep the client code consistent with local mode.

(use 'slacker.client)
(def sc (slackerc "localhost" 2104))
(defremote sc timestamp)
(timestamp)

Internally, slacker uses aleph for transport and carbonite for serialization. I forked carbonite and made it compatible with clojure 1.2 because the aleph mainline is still running on 1.2.

Going further

High-Order Functions

In clojure, functions are treated as first class value. Within memory, you can pass function as parameter to another function. However, this is not supported by serialization framework. So is it possible to add support for that in future?

Lazy sequence as parameter

This is another interesting feature in clojure function call. You can pass a lazy-sequence to clojure function. In RPC, it requires parameters to be evaluated on the server side.

(defn get-first [& args] (first args))
(apply get-first (range))

Example copied from StackOverflow

Coordinated states between several remote servers

With RPC, we can update states on several servers. So do we need something like distributed dosync:

(defremote a1 update-a1-state)
(defremote a2 update-a2-state)
(dosync-distributed
  (update-a1-state some-value)
  (update-a2-state some-value))

I’m not sure if this is a valid scenario in real world but I think it’s an interesting topic.(distributed STM?)

Conclusion

RPC is the first step to distributed clojure world. I will keep you updated with my prototype.

Spark in common lisp

还是关于spark的,一石激起千层浪,每个人心中都有一个spark。其实spark脚本刚出来的时候问题很多,但是就是因为产生了共鸣,众人拾柴pull request多。像redis的作者antirez也忍不住自己用c写了一个aspark

说完了别人的,那么来看看我的:clspark,common-lisp的spark。原本是打算用clojure写,但是想到jvm的启动速度,把这个机会留给我的第一个common lisp程序吧。

其实很简单。

common lisp的核心库里没有split,所以这里从cl-cookbook拷贝了一个split的实现,坦白说我还看不太懂这个loop的写法。loop是common lisp中最尴尬的form,因为他的形式太多。这点在clojure中是不存在的。比较一下就能发现,在语言层面,clojure是相对现代得多的lisp方言。

Grails的核心依赖必须保证项目中版本一致!

好久没写语录式大标题了,实在是今天玩Grails受害很深很深。

Grails因为本身是通过ivy来管理依赖的,虽然后续的版本和maven的集成不断加深,但是本身还是通过ivy来解析pom.xml。也就是说最后的活还是ivy干的。如果你的项目比较大,选择了通过maven来管理项目,而你的依赖中又牵扯了很多其他的依赖,那么你危险了。

:: UNRESOLVED DEPENDENCIES ::

:: log4j#log4j;1.2.16: configuration not found in log4j#log4j;1.2.16: ‘master’. It was required from …;1.7.0 runtime

对maven用户这是一条多么发指的报错!(比如这个

它的原因其实是你的dependency中有!=1.2.16版本你的log4j,比如1.2.12,即使你在pom里将它添加到了exclusion中也没有用,必须要保持版本的一致!至于你用什么办法让他们保持一致,还是干脆放弃用maven管理你的grails项目:It’s up to you.

同样的问题还出现在其他核心依赖上,例如commons-pool。