Meta Programming 主要解决什么了问题?
程序员文章站
2022-05-23 17:46:08
...
Metaprogramming is the writing of computer programs that write or manipulate other programs (or themselves) as their data, or that do part of the work at compile time that would otherwise be done at runtime. In some cases, this allows programmers to minimize the number of lines of code to express a solution (hence reducing development time), or it gives programs greater flexibility to efficiently handle new situations without recompilation.没有接触过meta programming, 感觉"manipulate other programs (or themselves) as their data"这个说法有点虚, 能否给一些确切的例子说明其妙用之处?
另外, 为什么说LISP与Ruby在meta programming上比Python做得要好?
回复内容:
如果把元编程定义为对程序进行操作的编程,那么,似乎没有理由把eval排除于元编程之外。至于到底是应该编译期还是运行期,我觉得还真是无所谓的。
元编程显然是多提供了一个抽象层次,相当于让你不仅可以通过一个层次去抽象问题,还能通过另一个层次对你的问题模型进行抽象。如果用的好,这似乎提供了巨大的能量。不过,这也似乎超出了大多数人的脑部运作规则,不太适合大面积推广。
至于monad,如果专注于它可以重定义;也就是定义运算序列的能力的话,也勉强可以算是元编程吧。
说个题外话:
上面有人提到REBOL,说它的元编程不好,我的感觉明显不同。由于REBOL采用最简语法作为它的form,这就导致了自定义dialect可以做到非常自然和直观,因而其元编程更自然,更漂亮。 Ruby元编程这本书的引言就讲的其实挺清楚的。
我到是建议你看完这本书,相信你能够理解的,里面例子还不错,这本书我还没看完。我现在手边就是。
顺便说说Lisp,大家都知道“代码及数据”这回事,其实就是这样子一回事。元编程的定义就是写出编写代码的代码,lisp里面quote可以阻止求值,到需要使用的时候用eval就行了。然后在运行的时候操作自己的代码也是他另一个含义。可以call 它叫动态元编程,以此区分代码生成器以及编译器方式的静态元编程。 批量生成各种代码(类,方法,测试等等),减少编程时的重复操作。各种DSL,code generator,scalfolding,project bootstraping都受益于meta programming。 编程(programming)是对于某类问题解决方法的抽象。
而元编程(meta programming)则是对于某类解决方法的抽象。
也就是说,元编程是对于编程的抽象。
至于说到DSL这部分我不敢苟同,诚然,ruby圈的DSL有泛滥的趋势,为了DSL而创造DSL、为了炫技而DSL,但是这些不是重点。DSL的出现是高度抽象的必然结果,脱离了抽象的DSL自然会死亡。
举几个栗子:
ORM
我有现在有N个实体类,我需要把这些实体类持久化储存。所以我写了N个DAO。后来发现,DAO这些东西好像很无聊嘛,无非就是那么几步,根据实体类名称创建表,根据属性创建数据库字段,然后增删改查。(ORM)
为了解决这些“相似的重复劳动”,我们需要某种机制,能够将我的一个"meta program"转化成具体的某个program。这就是抽象层次的差别。
ORM的基本步骤:根据实体类类名确定数据库表名,根据实体类属性确定数据库字段,统一的insert\delete\find\update 方法,根据属性与数据库字段的对应关系完成数据恢复与保存。在java中,我们通过反射拿到类名和数据库字段,利用泛型+集成得到insert\delete\find\update以及各种公共方法。结果就是你只需要简单继承一个父类,就可以完成DAO的操作。 (GitHub - satyan/sugar: Insanely easy way to work with Android Database. 这里有一个例子)
语法分析与词法分析
语法分析与词法也是一类具有很大相似性但是又有区别的事情。一个典型的例子就是lex&yacc(flex&bison), lex是一个词法分析器,yacc是一个语法的分析器(“分析器”的措辞可能有些不严谨)。对于词法/语法分析而言所以,lex接受一个.l文件,yacc接受一个.y文件,他们的结果是生成.c代码。通过DSL来生成代码,这是编译型语言做的事情(对于c++了解不多,尤其是c++新标准引入了许多新特性,据说可以达到动态语言一样的便捷程度)分析器的本质是一个有限状态机,状态机的核心工作应该是划分状态,通过meta programming,我们可以避免编写状态机的“重复劳动”(严格来讲应该是相似劳动)
《Ruby元编程》中的一个例子:擦屁股专员的重复代码
你是一个擦屁股专员,你接手一个老旧的设备信息系统,它维护者每台计算机的的设备信息与价格。这些数据来自一个古老的数据源(DataSource)。所以你有一堆类似:
ds=DataSource.new
ds.get_mouse_info(workstation_id)
ds.get_mouse_price(workstation_id)
ds.get_keyboard_info(workstation_id)
ds.get_keyboard_price(workstation_id)
这样的函数还有一打,计算机的每一个零部件都有这样的一个函数。
为了配合新的报表系统,你需要返回一个封装好的对象,这个对象针对每一个部件都有一个独立的方法返回一个包含描述和价格的字符串。很显而易见,如果不想想什么办法的,未来的很长一段时间你都在拷贝、粘贴代码的泥潭中挣扎。
每个方法都类似,每一个名为xxx的方法,首先通过ds.get_xxx_info获得信息,然后ds.get_xxx_price获得价格,然后拼接字符串...
解决方案:对于ruby,利用动态派发(send)\动态创建方法,甚至我们可以通过获取内省来获取datasource有哪些方法,得知有哪些部件,然后为这些部件创建方法。作为回报,你根本无需去创建\维护一个部件列表,如果有人在DS类中增加了一个新部件,你的Computer类将会自动的支持它。
---------------------------------------------------探讨------------------------------------------------------
对于javascript程序员而言 ,依旧可以采用同样的模式,内省+动态添加函数。
py这方面用的不多,就不说了,理论讲应该是类似的。
java比较麻烦的是你不能动态添加方法,所以只能退而求其次,提供一个部件列表和一个String get_info(String component)函数。通过反射来获取部件列表和调用函数。
c语言除了生成代码以外,似乎别无他路。
ActiveRecord
用过的人都说好。它里面的许多花样与魔法都是利用元编程实现的。 原则上说,目前能看到的 Meta Programming 有两种,第一种专注于重新构成语法,即 DSL,Lisp/Scheme 属于此类,这几年的新科状元 Ruby 也属于这一派,甚至还有走得更远但相对小众的 REBOL Language;第二种专注于在编译期进行计算和分派以达到针对特定场景的自动代码选择优化,C++ 的模板属于此类。C 的宏则兼有两者,但能力都比较弱。据我见过的情况,学院派的比较认可前一种,后一种则借 C++ 的东风用得更广泛。
问题引言里的那句话,我认为是在说 C++,因为它强调了 do part of the work at compile time。但 Python 的 Meta-programming,我闻所未闻。Python 1.2 开始用到现在,我确实没有听说 Python 有什么构建 DSL 的能力,更遑论所谓 Meta-programming。Python 社区的文化,也向来没有在语法上玩花样的传统。于是大略查了查,它说的是应该是第三项东西:类结构动态调整,本质来说,就是反射(reflection)。参照 DeveloperWorks 上的这一篇:Metaclass programming in Python,以及这一篇:Metaprogramming。不难看出,这两篇文章谈的是完全另一个话题。因此谈所谓 Lisp 和 Ruby 在 Meta-programming 上为什么比 Python 做得好,在我看来是一个伪命题,因为两者说的 Meta-programming 的内涵和外延都不一样。
在实际工作中,无论哪一种 Meta-programming,我都持否定态度。但理由和前面朋友的观点不同。我认为 Meta-programming 破坏了交流中的一项基本原则:同样的编程语言,应当有同样的交流习惯。过于随意地构造 DSL,让程序员之间的交流变得更加困难。这一方面的典型例子是 REBOL,几种 DSL 语法差异明显,重新学习的成本过高,也实际上助长了社区的分裂倾向。闲来无事,自己玩玩也就算了;放在工作里,害人害己。
至于 Lisp/Scheme 家族的 Meta Programming,与其说是做得更好,倒不如说是语法结构过于简单导致其怎么变换都是长得一个样子。关于这一点,不妨对比一下 REBOL,它构造出来的几个 DSL 语法上没有多少相似之处。不过,对我来说,Lisp/Scheme 这种语法简单其实才是我喜欢的 Scheme:编译器的设计者可以把更多的精力花在运行时和优化上。
还是那句话,我们写程序是为了解决问题。抽象和表达方式是手段,不是目的。汲汲于语法上的花头,是耍滑头。 我平时也不怎么用这个东西,主要是我喜欢的语言meta programming的能力也不大。为什么说LISP和Ruby的meta programming比Python好是因为,他们拥有“在编译时或者运行时重新读取自己的代码然后修改重新编译的能力”,譬如说LISP的宏,譬如说Ruby的DSL的写法,譬如说F#的computation expression,譬如说C#的linq和Expression
不过我觉得这个功能用处不大,没事还是不要凑合了。 不清楚元编程要解决什么问题,但是可以说说Ruby元编程的应用:
1,Rails完全是成就于Ruby的元编程能力之上。
2,Ruby社区涌现出的各种优秀工具,无不借了元编程之力。
3,在Ruby中,元编程不特殊,它就是编程。
以上所说只是针对于说元编程没大用的那些答案。 Lisp的程序和数据都是基于List,所以只要能够生成list结构,就能相当于能够生成代码,而Lisp的真正强大的宏机制,就是运行时的代码展开与求值。
几乎任何一个稍微大一点的Lisp程序多少都会有一些用来定义宏或者用宏编写的代码。
Ruby的元编程继承自Lisp和Smalltalk,一方面可以通过eval来动态执行代码,另外可以通过构造的语言闭包来打开和关闭作用域。而且有非常简洁和内省/反射机制来对程序的运行时状态判断,进而辅助代码的生成。
C++的模板元编程的作用机制在于编译期,通过模板对类型和数据的计算来进行展开生成代码,所以才会有十分强大和通用的STL(标准模板库)的出现。
Python在某种程度上还是可以进行元编程的(修改元类/数据?),只是灵活程度不够高,所以少有人用,Python 的哲学态度决定了这个社区会尽量选择远离元编程这东西。
确切的例子比如Ruby的ActiveRecord,参见Active Record Query Interface和rails/activerecord。
绝大多数的Ruby DSL都应用了Metaprogramming技巧的,另有书籍:Ruby元编程 。
如果深入研究,元编程的作用还是很大的。抽象能力比之于简单的代码提升了不止一个层级。 这样来说吧:
工业时代初期,是这样的情况:
人生产机器,机器生产产品。
到了后期:
人生产机器,机器生产机器,机器生产产品。
显然,机器生产机器是一个了不起的进步,这大大的解放了劳动力。
元编程就类似于机器生产机器,简单说就是程序可以自己改变自己或者生成新的程序。这样的当然大大的解放了程序员。
当然,事实上在元编程之前,程序员就发明了大量的辅助工具来帮助自己编程,例如IDE什么的。只是后来程序员觉得IDE啥的还是太弱了,就能查找替换跳到引用套用模板啥的,要是程序可以自己生产自己该多好。
====================科普完毕,下面是不负责任的畅想=====================
事实上meta-programming一直都不是一个定义良好的概念,主要是meta(元)这个词本身的含义就很模糊。
元一般被理解为更基本的,更基础的。元编程就是比程序更基础的的编程,也就是程序生成程序。从这个角度上来说,无论是C++的模板、C的宏、.NET的Emit和Compiler Provider、JavaScript的字符串拼接和eval,甚至于什么动网代码生成器,都是程序生成程序。
但是一般而言,要称得上元编程还是要满足一定的条件,当然,由于元编程这个概念并非是定义良好,这些条件也只能算得上是什么共识。这个语言的特性被A当作是元编程,而B不这么认为是很正常的事情,下面列举的这些条件,仅仅只是站在我的角度认为的:
1、具备元编程能力的语言,必须是可以产生同一程序设计语言代码的语言,而不能是用一种语言产生另一种语言的程序。这就把代码生成器排除在外了。
2、具备元编程能力的语言,元编程产生的程序代码,必须能够通过词法分析。这一条把C的宏排除在外和JavaScript的eval排除在外了。
……
可以看出来,到底算得上元编程,采用不同的限制条件可以得到不同的结果。当我们讨论一个程序设计语言是不是可以元编程时,条件1基本是可以达成共识,条件2就各有各的看法。
话说元编程最初产生的时候,单纯只是为了减轻程序员的负担,C++的模板最初只是重载的一个延伸。由于强类型的缘故,C语言必须针对不同的类型编写一大堆重复的代码,重载解决了这些重复代码的名称污染,而泛型则开始解决这些代码的重复。但C++标委会那群家伙显然不会像Anders那样止步于泛型,高端大气上档次一直是C++努力不懈的追求,所以泛型就被搞成了模板,更进一步的,程序员也一起来Happy,就玩出了C++模板元编程。
先写到这里吧, 元编程就是写出可以产生代码的代码。有了这样一层抽象,会让你的程序更有扩展性,适应更多的未知的场合。java里的反射,c++里的STL的模板,还是ruby里各种神奇的转发代理,这些都是元编程的应用。rails的成功构建于ruby强大的元编程,这也是rails为什么没有选择其他语言的原因。