Follow Your Hunch

Dec 17, 2010

    这篇博客的起由是前阵子在了解GWT(Google Web Toolkit)的时候,因为GWT内置了一个Java到Javascript的编译器,所以你可以在Java平台上进行开发,好处在于你并需要有对Javascript,跨浏览器等等很深的认识,甚至你可以是个文盲.当然这种好处的代价在于你以Java语言来开发客户端,这个过程极其貌似原来开发Swing的过程.所以下面是一段很常见的代码,给一个按钮添加一个点击事件:
final Button closeButton = new Button("Close");
   // Add a click handler to close button
   closeButton.addClickHandler(new ClickHandler() {
        public void onClick(ClickEvent event) {
           dialogBox.hide();
           sendButton.setEnabled(true);
           sendButton.setFocus(true);
        }
});
    看起来很简单的过程,当用户单击"Close"按钮时,事件就会出发,在处理体中就会隐藏掉对话框,发送按钮聚焦等,看起来就是我们写SWING的风格阿,多熟悉. 那但是当我跟着GWT官网的一些例子走时,我愈来愈发现当项目稍微大点(哪怕这还只是一个演示项目),这种添加事件的场景随处可见,漫天飞舞. 哪怕有时候我的事件处理简单到只是显示一个对话框,
 someButton().addClickHandler(new ClickHandler() {
   public void onClick(ClickEvent event) {
         //do sth
   }
});
        

Why Bother?

    我依然还得将这个模板写一遍,非常单调无聊.写多了,除了麻木,就会想想了,感觉这个太不直观了,简直就是要让你得大脑大圈,有没有更好的主意了?     鉴于自己经验不是很多,然后就问了问了JP,在Ruby中怎么弄的.讨论着讨论着,突然灵光一闪,想到一个极佳的案例,这个案例来自OSGi in Practice这本书, 简要的描述一下这个问题: 现代Java GUI编程大部分是单线程的,比如SWING和SWT,那么每当我们更新界面的某一个组件,而这个组件或者库是来自于GUI的库,比如更新一个label的信息: label.setText(newMsg) ,那么我们必须确保这个调用是发生在GUI的那个线程上面,也就是默认的"Event dispatch thread"之上. 所以如果这个调用在其他线程上,解决方安就是将这段代码传给GUI库支持的一个工具类SwingUtilities.在Swing中,需要做得就是将这段代码扔到一个Runnable的实例中的run方法体中,调用SwingUtitlies.invokeLater方法来执行.当然,我们可以先通过EventQueue.isDispatchThread来确认一下是否我们现在就是在GUI这个单线程上,如果在就不需要SwingUtilties,直接调用就好.     那如何在Java中实现对这一段模板的封装了(可以想象在SWING中有地方会需要这个处理)? 联想到刚才的按钮的一些单击事件的处理,你可以想到使用实现了Runnable接口的"匿名类"
 private void updateDisplay(Runnable action){
   if(EventQueue.isDispatchThread){
      action.run();
   }else{
      SwingUtitlies.invokeLater(action);
   }
}
那么以后我可以讲更新标签的动作就可以这么做了:
updateDisplay(new Runnable(){
  public void run(){
    label.setText("this way is just too CUMBERSOME!!!!!!!!!!!!!!!");
  }
});
    看起来还不错,那么以后其他的更新界面的东东都可以这么做. 但是写了几次了,还是感觉这个有点太笨拙了,就一行代码的动作都得写一些什么new Runnable ..... 看起来好分心阿. 那能不能这样了?
  private void updateDisplay(Expression expre){
     if(EventQueue.isDispatchThread){
         expr
     }else{
         SwingUtitlies.invokeLater(new Runnable(){
                  public  void run(){
                        expre;
                  }
         );
     }
}
    updateDisplay(lable.setText("this way is just what we wanted!"));
    那这个在Java中可以实现吗? 答案是在Java语言上是不行的,你无法阻止这条expression被立即执行,所以在进入updateDisplay方法体之前,这个expression也就是更新标签的调用是会立即被触发的.那使用匿名函数实际上让这个方法的调用者承担了更多责任,你得负责实例化这个匿名函数,然后讲你真正想执行的代码片段仍到里面,非常不可取.但问题是Java语言它没有将这个复杂性囊括到自己本身的库中,结果就是我们得写很多的匿名函数的Boilerplate code. 那你可以说我可以写个什么插件让IDE像自动产生getter和setters一样产生匿名类的插件不久可以了? 这个确实是一个很好的workaround,同时这个行为很符合insanity的定义,
Insanity: doing the same thing over and over again and expecting different results.
真正的问题还是Java语言不够强大,为什么我需要这么做,为什么将这些笨拙转嫁给了我们? 拜托,好歹你也遮遮丑嘛!         关键:     其实问题的关键在于我们能否在推迟方法参数中表达式的evaluation,让我们能操控表达式在何时被解析,而不是像Java语言中你没法做到,因为表达式在进入方法体之前就被解析了.     所以下面将介绍使用Scala和Clojure两种现在赤手可热的语言来实现我们所想要的方式.         
Scala Way
    首先我们来定义updateDisplay这个方法:
def  updateDisplay(action: => passedInAction) =
     if(EventQueue.isDispatchThread){
          action
      }else{
          SwingUtitlies.invokeLater(new Runnable(){def run = action});
      }
那现在你可以很随意的使用:
   updateDisplay(lable.setText("this way is the AWESOME Scala way!"));
简单到一行搞定,可读性也提高了,更重要的是你看到调用者都需要关心什么匿名函数.     Scala是怎么做到作为方法参数中表达式的这样一个延迟解析或者执行了?     关键在于updateDisplay定义方法参数的片段,我们使用action : => passedInAction .那在Scala中,前一个action实际上定义了别名,它指向了后面传入方法体的真正参数. 为什么不是action : passedInAction?因为在Scala中这种方式表达的效果跟Java语言的解析是一样的,passedInAction会在进入方法体之前被执行,在Scala中称为"Call-by-value".而action: => passedInAction 相对前者增加了一个' => ' 符号,魔力在于,它使得passInAction不会被立即解析,而是当你在方法体中真正调用action这个别名时,它才会被触发执行,这种方式在Scala中称为"Call-by-name".     这样在看起来代码就非常简洁明了,非常棒!         
Clojure Way
    相对于Scala,Clojure作为一门Lisp的方言,自然而然拥有Macro系统.尽管Scala没有macro的概念,但是它拥有很好非好强大的facilitators,就像前面看到的"cally-by-name",来避免使用Macro.Macro有些时候就是怪兽,(当然我不知道是不是Scala的作者也正有此意),因此怎么去写好Macro就需要 一定的规则或指导Rules for writing macro.  使用Macro有几个要注意的地方,我选取其中一个
you can write any macro that makes life easier for your callers when compared with an equivalent function
在Clojure中,下面便是Macro的实现
(defmacro update_display [expr]
   `(let [is_on_dispatch_thread?  (EventQueue/isDispatchThread)]
        if is_on_dispatch_thread?
            ~expr
           (. SwingUtitlies invokelate
              (proxy [Runnable] [] (run [] ~expr)))))
调用:
    (. update_display (. label setText "this is the BRILLIANT Clojure way!"))
    乍看起来还不习惯了,那么多括号来括号去的,这些括号便是Lisp的风格.读起来的话,如果你熟悉LISP的风格,那么读起来也还是蛮直白的,let用语绑定前面一个别名到后面的那个值,我们首先调用(EventQueue/isDispatchThread),实际上因为Clojure跟Java这样一个interoper*,它实际是调用EventQueue.isDispatchThread()其中/表示静态方法调用.如果is_on_dispatch_thread?为真,也就是在GUI的线程上,那么直接调用expr,也就是(. label setText "this is the BRILLIANT Clojure way!"), 如果不为真,则将此段代码封装到一个实现Runnable的接口的类中,也就是proxy关键字的作用.(ps:我现在对Clojure也不是非常熟,可能有更简洁的写法.)     同样的,Clojure实现的方式也是很简直明了的.         

Follow Your Hunch

    我记得原来郑晔有讲过他学习新语言的时候的一个感受,就是Scala里发现根本就需要有Visitor模式的存在.为什么了?因为它强大的平台已经讲这些 额外的复杂性都考虑进去了,让你--作为一个调用者Caller更加专注于你的领域,简化你的使用.谈到这个,在Java中如果你发现一些重复出现的模 式,然后你想去封装这些模式,你所能操控的最小粒度只能在方法级别上.比如,if 是Java中一个关键字,它定义有自己的语法,当你发现有关于if使用的重复出现的模式时,你是没有任何办法去封装这个模式,因为if 它不是一个方法,更不是一个数据.你不能创建一个unless 关键字,相反你只能通过 instead of using     unless (tickExpired()) 你所能做得便是:     if(!tickExpired()) 或者 写一个相反的方法来提高刻可读性     if(tickNotExpired()) 的方式来绕过这个问题. 也就是你不可能给Java语言添加新的功能,而这正是Clojure中Macro可以做到的,因为一切的关键字都是Clojure数据 "Code as data".迫使你的大脑做从if(!foo)到unless(foo)的转换,看起来是一个很微小的问题,但是它隐藏的真正的问题是:
Every distinct syntactic form in the language inhibits your ability to encapsulate recurring patterns involving that form.
我想引用Stuart Halloway在Programming Clojure 中的关于macro的一段总结:
The Wikipedia defines a design pattern to be a “general reusable solution
to a commonly occurring problem in software design.” It goes on to
state that a “design pattern is not a finished design that can be transformed
directly (emphasis added) into code.”
他继而说道:
That is where macros fit in. Macros provide a layer of indirection so that
you can automate the common parts of any recurring pattern. Macros
and code-as-data work together, enabling you to reprogram your language
on the fly to encapsulate patterns.
这个也使我想起来米高原来做过一个讲座有提到"跟着直觉"走,如果你感觉很不爽,你应该问问有是不是什么地方不对,有没有提高的地方.同时也使我想起了Twitter的创始人Evan Williams在TED大会坐的一个讲座Listening to Twitter users中提到的"Follow your hunch".
跟着直觉走
    PS: 我本人对于Scala并不是很了解,对于Clojure的了解程度尽在于读了读<Programming Clojure>,所以这里如果有解释不是非常清楚的地方,请各位帮忙指出来.         

引用

关于Scala:     郑晔翻译的 : Scala程序设计:Java虚拟机多核编程实战 Scala 'Call-by-name' and 'Call-by-value'     What's the difference between a lazy argument, a no-arg function argument, and a lazy value? 关于Clojure:     Programming Clojure, by: Stuart Halloway Rich Hockey 的讲座     Clojure for Java programmer Uncle Bob关于为什么选择Clojure的文章:      The Clean Coder: Why Clojure?