Archive for 2009年6月

终结者 T-800

2009/06/03

工作台上又增加了一个启发灵感的东西。

这就是技术奇异点之后的情景。

界面开发中的DSL与定制控件

2009/06/02

“中等程度的定制化”太含糊,按俺的观点,基于DSL的方案就适合“不需要自己写控件”的场合,理由很简单:a) 控件质量有基本的保证,b) 不会有过于怪异的用法,比如Windows这边最喜欢干的多次重入同一个消息循环之类的。反过来像游戏UI那种基本上要求100%定制的还是免谈了。⋯⋯

——陈甫鸼对《界面开发中的DSL》的评论

定制控件(俗称自己写控件)被很多人——好吧,被我——看成界面开发领域专业水准和业余水平的分水岭。快速应用开发(RAD)的鼓吹者通常回避讨论定制控件。企业开发和原型产品绝少需要定制控件,而大多数用户天天使用的杀手级(killer)通用软件又绝少能避免引入定制控件。

在开发中对定制控件的需求一定程度体现了UI framework本身的功能和一致性的强弱。出于功能实现的目的必须引入一个定制控件意味着framework的内键控件不能满足基本需求。由于一致性原因引入定制控件一般是因为缺省控件的用户体验达不到需要的水准,这种情况下缺省控件的水准——比如控件的观感(look and feel)——一般在原型产品中可以容忍,但是会阻碍软件达到高可用性。由于功能原因引入定制控件不意味着framework本身的功能一定有缺陷,特定的应用需求不一定适合通过framework提供内键控件来满足。经常由于一致性原因引入定制控件往往意味着framework本身的品质不够精致。比如在Windows下无数的应用使用自己定制的滚动条(scroll-bar),而这种情况在Mac OS X里非常罕见。

UI DSL对定制控件并不友好。给定一个DSL,它与不同的定制控件配合的默契程度取决于该控件的定制程度。比如,如果一个定制控件仅仅修改内键控件观感(基本上就是override on-draw的处理)而保留内键控件其它一切行为,那么它还是可以通过DSL来制定位置、大小等大多数属性。在处理DSL的可视化工具里也能正确显示控件的位置、大小(观感还是缺省的,开发者设计的时候自己想像最终的效果吧)。所以如果系统的内键控件如何具有良好的一致性,就能避免很多的控件定制,从而让DSL能更好的发挥作用。而一个从外观到行为都自己实现,不以任何内建控件(除了基本的panel、group或者canvas)为基础的定制控件在DSL及其处理工具中能被处理到什么程度就很不好说了。

更深一层来说,DSL不能很好的处理定制控件是因为声明式(declarative)语言和控件的命令式行为的差异决定的。由于声明式语言的局限,DSL处理控件时通常自然而然的局限在只处理控件的初始状态。而处理DSL的可视化工具在决定控件的初始状态的时候往往依靠的是本身的知识。比如,一个可视化工具在设计时显示一个对话框和它上面的按钮的时候,这个工具通常并不能通过询问UI framework(比如Win32或者Cocoa)来得知这个对话框和这个按钮要如何显示(这样做通常意味着要运行正处在开发阶段的程序,这是昂贵的而且经常是违背因果的),相反,工具本身需要拥有如何显示按钮和对话框基本状态的知识。

这种方式有一些严重的问题。首先是同样的知识要在控件本身和DSL的定义以及处理DSL的工具中重复体现。保持多个部分体现的一致的知识并非是简单的事情。我们经常可以看到,在新版本的操作系统中开发时旧版本的可视化工具显示的控件和程序真正运行的时候显示的风格相差甚远。其次,DSL无法预测晚于其语言定义和处理工具出现的控件有什么样的运行或者初始状态,这让语言本身及其处理工具只能处理有限集合以内的内键控件。

解决这些问题的努力所知不多。Sun的JavaBean标准曾经做出过努力。JavaBean的方案是在控件的发布包中(JavaBean是一组字节码文件,打包成.jar)加入一组设计时信息(design-time information)。设计时信息包括控件的初始外观等信息,也由声明型语言定义。JavaBean标准对于一个JavaBean是否提供设计时信息是不强制的。在实践中JavaBean的方案并不成功。因为组件的粒度问题,软件组件从来没有像建筑零件那样做到预制化,所以设想中的独立组件市场(resuable component market)从来没有成为现实。没有市场的需求,大多数JavaBean都是开发者为自己的应用内部开发的定制控件,也就没有投资于提供设计时信息的动力。绝大多数内部使用甚至很多出售的JavaBean都不包含设计时信息。JavaBean的做法也没有真正解决重复实现的问题,虽然控件的运行时外在特征和设计时特征由统一的发布包提供,但是在包的内部仍然是由分离的代码来实现的。开发者仍然需要额外的保持多处代码的功能同步。

现在的DSL对定制控件的应用大多数是用占位件(placeholder control)来实现。DSL在定义中加入某些内建组件或者纯粹的占位件(例如,在可视化工具中表现为简单的矩形)。代码在运行时讲控件替换为定制控件的行为。由此,DSL可以定义定制组件非常有限的一些行为。

DSL本身在界面设计中的应用并不完美,对定制控件的处理尤其差。但是一些试图解决这些问题的努力最终因为缺乏动力而失败,从另一方面说明DSL本身的缺陷也许并非界面设计中开发者非常急迫解决的问题。这是我们在软件开发中经常遇到的一种情况:某种问题被提出,人们付出了相当的努力试图解决,但是提出的解决方案最后被废弃,虽然有事是因为解决方案自身的缺陷缺陷,但是更多的是因为使用者缺乏被采用这些方案的动力。这说明问题的提出过于理论化,问题的提出者高估了该问题带来的不便或者低估了人们对该问题的容忍程度,而解决方案本身的不完美又无法被其带来的益处所抵消。(说句题外话,文件系统领域,对于粒度比UNIX的user-group策略更细的permission control的需求就是这样一种问题,当理论者提出并试图用访问列表ACL来解决这个问题的时候,却发现大多数系统管理员都认为user-group策略是足够的而缺乏配置ACL的动力。)

界面开发中的DSL

2009/06/01

没有NIB文件的Cocoa应用是可行的,但是非常困难。⋯⋯在开始接触Cocoa的20年间,我几乎每过3个月就会看到有人讨论这样的问题。

三个阵营欢迎你的加入:
1. IB一开始是令人痛苦的,但是你会渐渐离不开它。
2. IB起码聊胜于无,不要白不要。
3. IB有利有弊,逆来顺受吧。

上面是从一次关于Cocoa XIB(以前称为NIB)的长篇邮件列表讨论中摘出来的两段。尽管这个题为《Writing Cocoa apps w/o using Interface Builder》的问题错误的发到了Xcode列表(本该是Cocoa-dev列表),而且其间也不断的有人指出这一点,但是没有阻止每个回信者(包括第一句就指出问题发错了地方的)都慷慨的发表对XIB的看法。最终有30人次发言,不说Xcode列表本来就没有Cocoa-dev热闹,即使是Cocoa-dev列表也少有这么吸引人的话题。这种情景让我想到专用语言(domain-specific language)在界面设计中应该如何应用仍然是一个未有定论的话题。

我的早期接触

我接触的第一种DSL是Windows的资源文件(.rc文件)。当时看来——现在也多少如此——在纯粹的C/C++环境中引入一个语言异类把开发过程搞得非常难以理解,令人厌恶而恐惧。直到发现资源文件描述的东西用C API也统统能实现,对它的恐惧心理稍减,厌恶感更甚。这种DSL简直是毫无用处,多此一举。

除了SQL和IDL之外,我接触的第二种DSL是北电内部开发的一种把一种类型的Java对象转换成另一种Java对象的语言。当时的研发中心还不正式属于北电,开发过程也不太严格,程序员可以对很多事情自作主张。在发现Java的代码也能提供这个DSL的功能之后,我马上下定决心不使用这个DSL(因为没有办法完全绕过,最终写了一个非常小的不做任何转换的DSL文件)。结果是非常令我满意的,那些用了这个DSL的同事除了无穷无尽的bug,什么也没得到,因为这个DSL不光需要编译,而且要先编译成Java代码,再编译成bytecode,既失去了调试所需的透明度,又没有节省任何编译时间。

这就是DSL在我的早期开发生活中给我留下的印象。严格的说,引起我反感的不是全部的DSL,仅仅是具有下面特征的:首先,这种DSL是声明式(declarative)语言,而不是命令式的(imperative)。它们不是图灵完备的,只能根据预先定义的一些规则(rules)来粗糙的描述使用者要达到的目的,而无法精细的控制每一步(step);其次,它们的所有功能,至少是绝大部分功能,都能被某种命令式语言替代。比如Windows资源文件的功能能够被直接调用Win32 API的C代码完全替代。从这个方面来说,我并不反感像SQL或者IDL这样的DSL。因为它们是为了完成命令式语言无法很好完成的工作而设计的独有接口,并非仅仅是命令式语言的某种速写符。

在接触DSL很久之后才了解DSL,decalratvie和imperative这样的名词。一开始的概念就是“只有程序代码最好”。

DSL与UI

Java Swing是众多UI framework里面完全不使用DSL的一个罕见的例子。这是Sun追求100%纯Java的副作用。这样做的好处在于程序员不用再学习专门的DSL。 程序的编译和分发也更加简单统一。带来的问题是定义UI的代码十分冗长。特别是命令式语言和UI定义的对应方式不直观,这样用可视化工具产生的UI代码除 了本身比手工编写的代码更为冗长之外,每种可视化工具产生代码的方式也不相同。用命令式的步骤来理解产生的UI效果,让程序的可读性很差。

用命令式语言表示UI的问题,也就是引入UI DSL的初衷。尽管有类似北电那种情况的特例,DSL的绝大部分都是在图形界面设计方面的。这是由图形界面设计的本质决定的。UI的布局部分很适合声明式语言。

Windows的资源文件有了20多年的历史。作为UI开发还不为人们熟悉的时代的产物,当它出现的时候,人们把使用它作为一种新奇的技能,还来不及权衡其利弊。20多年之后,资源文件已经很少使用。它的语法和功能都过于陈旧,只能用来偶尔声明一些比较简单的对话框。对于复杂的国际化和本地化需求,资源文件也是疲于应付。资源文件无法提供具有很强动态特性的UI。后来的framwork一般都拥有运行时的自动化布局功能。自动化布局让纯粹代码表示的UI变得勉强可以接受,不致可读性非常差。这也是Java能够完全放弃DSL的一个必要条件。

现代的UI framework基本都使用DSL,大多采用基于XML的语法。比如GTK和Cocoa。这些DSL从本质上说仍然是代码的速记符——完全脱离这些DSL用纯代码实现的UI是理论上可行的。但是就像一开头说的,完全脱离DSL是不建议的,实际上非常困难的。

DSL的利弊

DSL让程序的代码量,尤其是大体类似的冗余代码量大大减少。减少代码量就意味着减少引入错误的机会和减少维护量。但是同时,DSL让程序失去了一部分透明度。由于失去透明度带来的维护开销是否能够被代码量减少带来的收益抵消是一个很难衡量的问题。

DSL对于版本控制也不是很友好。UI DSL一般由可视化工具生成而很少手工编写。通过可视化工具做出的很小的改动往往造成DSL文件的很大变化。这种“雪崩效应”给版本控制和历史改动的跟踪带来了很大的麻烦。两个方面可以解决这个问题。第一,可视化工具尽可能消除“雪崩效应”;第二,可视化工具支持DSL的双向修改,即经过手工修改的合法的DSL文件可以被可视化工具正常操作,这样开发者在做出很小的改动时可以直接手工修改,出现“雪崩效应”时也可以用手工补救。

DSL对于跨平台开发非常不利。跨平台开发使用的语言绝大多数是命令式的。其一般的策略是把各个平台特有的API封装起来,对上提供统一的接口。由于跨平台部分采用命令式语言,接口本身和支持接口的平台特有API也最好是命令式的才能让整个架构的集成更为自然。所以跨平台开发中对于跨平台接口以下层次中DSL的使用总是极力避免。像Cocoa这样的framework并没有成为跨平台的接口,通常作为平台特有的API处于跨平台接口之下,这种情景中对绕过XIB的需求最为强烈。而对于GTK这种本身支持多平台的framework,这种需求就小得多。

DSL适合中等程度UI定制化。对于UI控制度极高的场合,比如游戏设计,UI DSL没有什么市场,整体UI都是用命令式语言编写。对于一些prototype UI,人们往往习惯用命令式语言调用预设定的alert popup或者简单可控的对话框。

对DSL是否利大于弊我仍然持怀疑态度。但是Java Swing遭到了失败(尽管有其它原因),越来越多的framework采用DSL,可视化工具的更多应用,人们不可避免的越来越习惯和无法脱离UI DSL。

工作台

2009/06/01

 

我的工作台(名称取自海外著名 Web 设计师们的工作台)。书桌是老婆和我一起在宜家选的。显示器是妈妈送的。MacBook应该算是老婆送的(用她赚的外快)。Das Vader是在巴黎出差的时候带回来的。

 

C++恶性录——之一

2009/06/01

C语言是一种简单优美的语言,但是它也有很多问题。C++试图解决C语言的某些问题,并为此付出了努力。这种努力值得钦佩,但是其结果不如人意。由于对C++的大量投资,人们往往对C++在表面上解决了C问题的假想夸大其辞,而对于C++继承C的问题过分忽视。

C++对C的兼容性是一个错误。这不是因为C++继承了C的错误,而是C++的继承导致了C++特有的错误。C的很多策略在其简单的环境下是可控的。当C++拥有比C高一个数量级的复杂度的时候,这些策略就成为了致命的问题。所以,C++对C的继承和兼容并不意味着C++“至少比C好”。C++在(表面上)解决一个问题的时候引入了(实质上)更多的问题

分离编译(separate compilation)是C语言编译的基本策略。在此基础上C借助make等工具实现了增量构建(incremental build)。分离编译中,不同编译单元(unit of translation)中的信息共享通过头文件来完成;最后的信息汇总通过连接器完成。分离编译——特别是在借助于头文件这种低级语言处理方式,并且最终必须通过静态连接生成可执行文件的情况下——有它自身的局限性,但是在一个相对简洁的语言中,还是可以维护的。

在C的分离编译中,一个编译单元可以引用另一个编译单元中的实现。后者对其包含的实现(比如函数,全局变量)输出symbol。前者引用symbol。二者间symbol的一致性通过头文件保证。连接器通过symbol的对应来进行连接。如果两个不同的编译单元输出了同样的symbol,连接器会报错。如果一个编译单元引用的symbol没有被找到,编译器或者连接器会报错。这样的结果是,放在头文件中的实现往往会导致连接时的重复symbol定义错误。所以,部分的是头文件的设计初衷,部分的是这种简单的连接策略的结果,头文件在C语言中半强制性的地位是包含类型声明,而不能包含类型的实现。C语言的简单机制保证了绝大多数编译单元间不一致造成的错误能在编译和连接阶段被发现,同时提倡了头文件功能的统一。

在C语言没有提供保护的广大领域中,容器的类型安全是最为C++的鼓吹者津津乐道的。C++解决容器类型安全的方法不是为语言处理加入容器类型安全的特性,而是发明了模板这个meta-feature。通过模板,C++的设计者感到满足——因为面对问题,C++没有头疼医头、脚疼医脚,不是简单的ad-hoc解决,而是引入了优美的新的基本概念,从逻辑基础上一举解决这个问题,还得到了编译器图灵完备的语言。

但是有些东西超出了C++设计者的预料。由于模板在编译时才能具体展开,所以模板的实现必须放在头文件中。使用模板的每个编译单元都必须包含实现这些模板的头文件。模板的问题在于,引用模板就是实现模板。所以,当单元A和单元B同时引用std::vector<a_class>的时候,它们不是在引用同一个类型,而是各自实现了一个类型。这样的结果是C++的连接器不能像C连接器那样,简单地对不同的编译单元实现同一个symbol的情况报错,C++的连接器必须决定保留某个编译单元的实现而抛弃其它单元的实现(严格的说并非完全抛弃,如果不同的编译单元使用了某个模板类不同的成员函数,这些不同的成员函数的实现会被合并)。

当不同的编译单元引用了同名的模板附带同名的类型参数,但是这些模板或者类型参数在不同的单元中的定义不同(比如不同的编译单元使用了不同的编译宏、或者引用了含有同样名字模板的不同头文件),结果是灾难性的。任何一个单元的编译都是成功的。连接呢?当你遇到一个具体的编译器之前,你绝对不知道;当在一个莫名其妙的错误上花费无数时间之后,你也许会猜到几分。在Visual C++上对不同的编译单元打开或者关闭_HAS_ITERATOR_DEBUGGING,你会发现,连接默默的获得了成功,但是(幸运的话)程序会马上崩溃。

是的,C也会有类似的问题。但是C的问题仅仅局限在struct定义,而不会延伸到函数实现。大多数由于包含不同struct实现而造成的分离编译问题实际上都能在编译时发现。因为struct本身具有接口的性质,而接口的错误往往会造成同一个编译单元内部信息的不一致,从而在编译时被发现。但是模板不一致造成的问题往往属于实现问题,在编译时被隐藏,在连接时又被考虑不周的连接器忽视。C++在C的基础上引入了新概念,但是新概念并不是基于更智能的编译策略,而是一种简单的即时(on-the-fly)类型生成。这种代入风格[1]的扩展把C语言中无关痛痒的瑕疵发展成了致命的缺陷。C++的初衷是增强C的静态类型系统,把更多的错误扼杀在运行时之前。但是由于失去控制的复杂度,最终导致一些在C语言中本已解决的问题退化为运行时的问题。

注:[1]关于代入风格的讨论会随后整理。代入风格即处理输入信息的时候局部分离处理。和委托风格对应(即处理输入信息时全局统一处理)。

第一天

2009/06/01

这个Blog的开端和我一开始设想的有一定距离。

今天无意中打开Matt Legend Gemmell的Blog,看到优雅的排版,让我对自己在国内某个网站上的博客突发憎恶之情。急不可待的想开一个Wordpress的Blog。用独立的虚拟主机,查了一下价格果然负担不起。用支持MySQL和PHP的租赁空间,收费还是有点心疼。在wordpress.com上开了一个Blog,回到家里一看果然被墙了。

最终找到了这个wordpress-mu的Blog服务。不知道哪一天也会倒掉或者被墙。尽量能让身边20%的人有80%的时间看到我的Blog就好。