冒号课堂§3.2:超级范式

冒号课堂

第三课 常用范式(2)

 

3.2超级范式——提升语言的级别

智能繁衍:机器人生产机器人                                             ——题记

 

关键词:编程范式,模板元编程,元编程,语言导向式编程,产生式编程

摘要:元编程简谈

 

预览

·           元编程作为超级范式的一个体现是,它能提升语言的级别

·           如果说OOP的关键在于构造对象的概念,那么LOP的关键在于构造语言的语法

·           离开IDE就无法编写、编译或调试的程序员,如同卸盔下马后便失去战斗力的武士,是残缺和孱弱的

·           既然有重复的代码,不能从语法上提炼,不妨退一步从文字上提炼

·           元程序将程序作为数据来对待,能自我发现、自我赋权和自我升级,有着其他程序所不具备的自觉性、自适应性和智能性,可以说是一种最高级的程序

 

提问

  • 什么是元编程?它与通常的编程有何不同?
  • 元编程有何用处?它有哪些应用?
  • 相比自编的元程序,用IDE自动生成的代码有什么缺陷?
  • 语言导向式编程有何优点?它与元编程有何关系?
  • 元编程与产生式编程有何异同?
  • 为什么说元程序是一种最高级的程序?

 

讲解

问号忽然想起一事,问道:“有一本名为《C++模版元编程》的书,既然提到了模板,想来也属于泛型编程吧?”

冒号答道:“模板元编程Template Metaprogramming,与泛型编程密切相关但自成一派,隶属于另一种编程范式——元编程Metaprogramming),简称MP。此处的前缀‘meta-’常译作‘元’,其实就是‘超级’、‘行而上’的意思。比如,元数据Metadata)是关于数据的数据,元对象Metaobject)是关于对象的对象,依此类推,元编程自然是关于程序的程序,或者说是编写、操纵程序的程序。”

叹号皱着眉:“听着有点绕。”

冒号投影出另一段代码——

C++(元编程):

template <int N>

struct factorial

{

     enum { value = N * factorial<N - 1>::value };

};

 

template <>                // 特化specialization

struct factorial<0>    // 递归中止

{

     enum { value = 1 };

};

 

void main()

{

    cout << factorial<5>::value << endl; // 等价于 cout << 120 << endl;

}

 

“以上用模板元编程实现了阶乘运算。”冒号讲解道,“与前面三种核心范式的阶乘实现有着根本的不同:这里阶乘的值是在编译时而非运行时计算出来的。换句话说,这段代码以模板形式通过编译器生成了新的代码,并在编译期间获得执行。”

叹号大惑不解:“这又说明什么呢?”

冒号并不直接回答:“假设你需要批量处理用户文档,其格式结构预先给定,但既不像CSV(逗号分隔)那么简单,也不像XML那么标准,并且用户随时可能改变格式标准,请问如何设计这段程序?”

叹号略一思索,便回答:“三大模块:阅读器读出输入文档,解析器按照格式标准去解析,处理器对解析结果进行处理。 ”

“显然关键在解析器,如果你是从头做起,那么问题至少有四。”冒号扳着指头数:“第一、费时写解析器代码;第二、费时调试解析器代码;第三、如果用户更改格式标准,你得重复做上两件事;第四、如果这段程序是大型程序的一部分,任何改动都可能意味着软件的重新编译、连接、测试、打包、部署等等。如果因为你的缘故公司不得不频频发布补丁包的话,你的饭碗恐怕是朝不保夕了。”

还是句号机灵:“既然谈到了元编程,一定是利用元编程,根据不同的格式标准自动生成相应的解析器代码。不过——此法虽一劳永逸,但难度似乎不小啊。”

“思路对头!”冒号赞许道,“大家听说过LexYacc吗?它们能根据格式标准生成相应的解析器代码。更妙的是,格式标准不限于静态数据,甚至可以含有动态指令!这意味着用户不仅能定义业务数据格式,还能定义业务流程。”

“这敢情好!”叹号兴奋地说。

“如果知道LexYacc本来就是编写编译器和解释器的工具,你就不会惊讶于它们的强大了。顺带说一句,编译器本身就是元编程的典型范例——把高级语言转化为汇编语言或机器语言的程序,不就是能写程序的程序吗?”冒号引申开来,“更进一步地,我们可以定义自己的领域特定语言DSL,更加灵活方便地处理客户逻辑。”

逗号有点糊涂了:“领域特定语言?就是前两堂课提到的非通用编程语言吧?怎么和元编程也扯上关系了?”

“不是扯上关系,而是它们之间本来就有着千丝万缕的联系。”冒号纠正着,“相比第三代的通用编程语言,领域特定语言由于其在应用范围上和语法上的限制而显得简单、针对性强,有时被成为‘小语言’(little language),也是一种特高级语言very high-level programming language ,简称VHLL),属于第四代编程语言。”

冒号说到此处,逗号猛地一拍脑门:“哦,我明白了。第四代语言最终需要编译为机器语言,而编译器就是元编程的应用。 ”

“你只说对了一半。”冒号不疾不缓地说,“DSL一般不会一步到位地编译为第一代的机器语言或第二代的汇编语言,而是通过现成的编译器生成器compiler-compilercompiler generator)首先转化为第三代的高级语言。这样不仅大大降低了难度,也方便了程序的调试。刚才提到的YaccYet Another Compiler Compiler)便是这样的工具,能为解析器parser)产生C程序,多用于Unix下的编程。更现代的工具如ANTLR (ANother Tool for Language Recognition),能生成CC++JavaC#Python等多种语言的源程序。”

引号立刻联想到:“我记得框架Hiberate的必备库中就含有antlr.jar文件,与这个ANTLR有关吗?”

“说得正是!”冒号很满意学员完美的配合,“Hiberate中的HQLHibernate Query Language)是典型的DSL,需要通过ANTLR来解析。你们可以验证一下,在HibernateAPI中有org.hibernate.hql.antlrpackage,但在其发布的源代码中相应的目录下却看不到一个Java源文件。却是为何?盖因此package中所有的源代码都是在ant build中自动生成的,这些非人工编辑的文件是不会放在版本控制中的。”

众人茅塞顿开。

句号想通了一个逻辑:“元编程作为超级范式的一个体现是,它能提升语言的级别。比如,有了编译器的存在,汇编语言升级为第三代高级语言;同样借助YaccANTLR之类的元编程工具,第三代语言可以升级为第四代的DSL语言。”

冒号并未就此止步:“将这一模式发挥到极致,便是更加激进的语言导向式编程[1]Language-Oriented Programming,简称LOP)。这种编程范式的思路是:在建立一套DSL体系之后,直接用它们来编写软件,尽量不用通用语言。””

叹号莫明其妙:“想法近乎疯狂啊!放着好端端的通用语言不用,先造一套专用语言,这么做划算吗?”

 “如果一个大型系统涉及的领域十分专业,包含的业务逻辑十分复杂,为其定制DSL或许会磨刀不误砍柴工。我们通过下面的两个图比较一下这种范式与主流编程范式的不同之处。”冒号映出新的投影——



 

 “由于DSL比通用语言更简单、更抽象、更专业、更接近自然语言和声明式语言,开发效率显著提高,因此图中手工部分的时间相应减少。此外尤为关键的是,这种方式填补了专业程序员与业务分析员之间的鸿沟。要求一个非专业编程的业务分析员用DSL来开发固是勉为其难,但要做到读懂代码并审查其中的业务逻辑则已非难事。” 冒号细解个中要点,“如果说OOP的关键在于构造对象的概念,那么LOP的关键在于构造语言的语法。有人认为LOP是继OOP之后的下一个重要的编程范式,我们不妨拭目以待。”

句号整理了一下头绪:“能不能这么说:如果处理一些复杂、非标准格式的文档,可以考虑用元编程;如果整个业务逻辑复杂多变,可以考虑利用现有的DSL或创造新的DSL来处理业务,即所谓的语言导向式编程。”

“总结得不错,不过当特定格式的文档有了专门的解析器后,这种文档格式标准就可视为一种语言了,不是吗?这本质上就是DSL啊。”冒号出语点化。

句号顿时醒悟:“是啊,就像XMLHTML一样,能被程序认识的格式可不就是一种计算机语言嘛。”

冒号将话题延伸:“我们的想象力可以再狂野些,在文本DSL的基础上裹以图形界面,从而引进图形语言。如果再将部分业务逻辑开放给用户定制,那么你的客户会欣喜地发现,他们的经理只要点点鼠标就可以改变整个业务流程了,而这一切不仅不需要软件开发方或第三方的参与,连本公司的技术人员也免了。这时候倒是你的老板发愁了:你的设计太过完美,客户的后续开发费怕是赚不到啰。”

众人一乐。

问号继续发问:“还有其他元编程的应用吗?”

冒号随口举了几例:“元编程的例子比比皆是:许多IDEVisual StudioDelphiEclipse等均能通过向导、拖放控件等方式自动生成源码;UML建模工具将类图转换为代码;Servlet引擎将JSP转换为Java代码;包括SpringHibernateXDoclet在内的许多框架和工具都能从配置文件、annotation/attribute等中产生代码。”

引号仍不知足:“这些应用虽然典型,但都是些开发工具、框架引擎之类的基础软件,有没有平时编程就能用到的例子?”

 “当然有!”冒号坚定地答复,“有时程序中会出现大量的重复代码,却囿于语法上的限制无法进一步抽象化和模块化。如果采用手工编写或者单纯拷贝的方法,既费时又易错,显为下策。有时可借助IDE内置的代码生成功能,但一方面局限性很大,另一方面无法自动化版本化。”

问号插问:“什么叫版本化?”

冒号解释:“理想情况下,一个程序员对程序的贡献都应该保存在版本控制系统(version control system)中,以便跟踪、比较、改进、借鉴和再生成。在IDE下自动生成的代码本身可以被记录,但产生代码时的行为却不能被记录,几次简单的鼠标动作就能产生较大的代码差别,使得版本比较的意义大打折扣。顺便说一句,离开IDE就无法编写、编译或调试的程序员,如同卸盔下马后便失去战斗力的武士,是残缺和孱弱的。”

问号有些明白了:“这是因为鼠标行为本身在代码中是没有痕迹的。”

“不仅是鼠标行为,有些需要键盘交互的行为也是没有痕迹的。比如在命令行下用debugger来调试的行为无法被记录,也难以重复和自动化,只能作为权宜之策。相比之下,日志(logging)和单元测试unit test)具有明显的优势[2]。”冒号答完,立马重返主题,“回到上面的问题,既然有重复的代码,不能从语法上提炼,不妨退一步从文字上提炼。我们可以利用AWKPerl之类的擅长文字处理的脚本语言,当然也可以用JavaC等非脚本语言,再辅以XSLT之类的模板语言,自动生成重复代码。这样不仅灵活性强,而且生成代码的代码——也就是元程序代码可以被重用,元程序的数据来源也能版本化。”

句号深得要领:“就像Hibernate中的antlr包一样,真正的源码反而不在版本控制中了。一方面没有保存的必要——可以自动生成;另一方面没有比较的必要——元程序的数据来源的变化比实际源码的变化更简明、更直观。”

冒号继续推进:“另外,有时程序的结构需要动态改变,而C++JavaC#静态语言是不允许动态变更类的成员或实现代码的,利用元编程便可突破这种限制。”

逗号恍然大悟:“原来元编程就是编写能自动生成源代码的程序。”

“也不尽然。”冒号马上修正道,“自动生成源代码的编程也属于另一种编程范式——产生式编程Generative Programming[3]的范畴。区别在于后者更看重代码的生成,而元编程看重的是生成代码的可执行性。另外,除了在编译期间生成源代码的静态元编程,还有能在运行期间修改程序的动态元编程。从低级的汇编语言到一些高级的动态语言如PerlPythonRubyJavaScriptLispProlog等均支持此类功能。比如许多脚本语言都提供eval函数,可以在运行时将字符串作为表达式来运算[4]。”

问号突然问道:“编写病毒算不算元编程?”

“编写一个只是删除或感染文件的病毒,不必用到元编程。但如果要开发一个能自我变异的智能病毒,那就需要元编程了。不过你要是把元编程用在这方面,可别说是我教的。”冒号开了个玩笑。

引号自言自语:“程序的程序,就是程序的平方。”

“也可以是程序的立方,四次方……理论上是无限次方。在传统的编程中,运算是动态的,但程序本身是静态的;在元编程中,二者都是动态的。元程序将程序作为数据来对待,能自我发现、自我赋权和自我升级,有着其他程序所不具备的自觉性自适应性智能性,可以说是一种最高级的程序。它要求编程者超越常规的编程思维,在一种崭新的高度上理解编程。想象一下吧!”冒号激情勃发,“如果有一天机器人能自我学习、自我完善,甚至能生产新的机器人,实现‘智能繁衍’,是不是很美妙?”

“我怎么觉得特恐怖呢?岂止是程序员,所有地球人的饭碗都会被它们砸光了。”叹号此言一出,众皆忍俊不禁。

 

插语

[1] Martin Ward最早提出此范式,见参考文献【1】。

[2] 虽然调试与日志测试不是一码事,但合理的日志和单元测试能大量减少调试工作。

[3] 也译作“生成式编程”,属于自动编程(Automatic Programming)范畴。

[4] 考虑到eval过于广泛和强大,有些动态语言还提供其他更明确和更安全的元编程机制,如JavaScript可用字符串来构建FunctionRuby更是提供了define_methodinstance_eval class_evalmodule_eval等诸多元编程方法。

 

总结

  • 元编程是编写、操纵程序的程序。在传统的编程中,运算是动态的,但程序本身是静态的;在元编程中,二者都是动态的。
  • 元编程能减少手工编程,突破原语言的语法限制,提升语言的抽象级别与灵活性,从而提高程序员的生产效率。
  • 元编程有诸多应用:许多开发工具、框架引擎之类的基础软件都有自动生成源代码的功能;创造DSL以便更高效地处理专门领域的业务;自动生成重复代码;动态改变程序的语句、函数、类等等。
  • IDE下自动生成的代码通常局限性大且可读性差,小操作可能造成的源码上的大差异,削弱了版本控制的意义。用自编的无需人机交互的元程序来生成代码,只需将元程序的数据来源版本化,简明而直观。同时由于元程序可以随时修改,因此局限性小,更加灵活。
  • 语言导向式编程(LOP)通过创建一套专用语言DSL来编写程序。相比通用语言,DSL更简单、更抽象、更专业、更接近自然语言和声明式语言、开发效率更高,同时有助于专业程序员与业务分析员之间的合作。
  • 语言导向式编程一般通过元编程将专用语言转化为通用语言。
  • 产生式编程与静态元编程都能自动生成源代码。产生式编程强调代码的生成,元编程强调生成代码的可执行性。此外,动态元编程并不生成源代码,但能在运行期间修改程序。
  • 元程序将程序作为数据来对待,有着其他程序所不具备的自觉性、自适应性和智能性,可以说是一种最高级的程序。

 

“”参考

[1] Martin WardLanguage Oriented Programminghttp://www.cse.dmu.ac.uk/~mward/martin/papers/middle-out-t.pdf

[2] Sergey DmitrievLanguage Oriented Programming: The Next Programming Paradigmhttp://www.onboard.jetbrains.com/is1/articles/04/10/lop/mps.pdf

[3] WikipediaMetaprogramminghttp://en.wikipedia.org/wiki/Metaprogramming

posted on 2009-04-01 22:18  郑晖  阅读(2609)  评论(7编辑  收藏  举报

导航