浅谈元编程

为什么会突然想到写这篇文章呢?其实是因为我曾经向一位朋友推荐学习一下ruby/lisp这类支持元编程的语言,尽管可能永远不会在生产环境中用到,但是可以让人学习用另一种思路思考问题,然后友人就要求我解释我学了之后思考问题的角度有何不同,当时确实把我难住了,因为这个问题的确不好描述。

对于这个问题,我思前想后好几周后,我还是决定从理性和感性两个方面,稍微描述一下,什么是元编程?学习了元编程之后你的思考到底可能在什么地方和别人产生差异?

P.S. 鉴于ruby在元编程领域的强大能力,本文将用ruby来辅助我的描述,即便是没有ruby基础,我也会尽量描述清楚不影响理解;至于为什么不用lisp,那是因为它在这行当走得太彻底了,彻底到我觉得自己现阶段没有能力描述清楚

结论放在前面

虽然我将要尝试向你描述下元编程,但还是有几条规则要写在前面,如果你觉得无法接受这几条规则,那么提早关闭页面比较明智。

1. 心态放谦卑,无论何时,先尝试去理解而不是拒绝

##### 2. 元编程并不是黑魔法, 元编程也是编程 ##### 3. 和学习游泳一样,如果不亲身去尝试,永远也无法真正理解 ##### 4. 即便你花时间精力取弄懂了一两门元编程语言,但你的工作环境可能永远都用不到,冷静两秒钟,确认自己并不是那么功利主义 ##### 5. 再次深呼吸,确认自己还是有兴趣了解下去

元编程的魔力

元编程的学院解释是:运行时操作程序构件的能力。这个类似于物理公理定义的说法比较令人费解,其实他的意思是这样的,通常,我们的程序写完进行编译链接后,它的运行规则就固定了,很难在运行时再去做任何修改。比如对于C语言来说,一个方法或者函数要执行的逻辑已经固定下来了,无法在不改动代码的情况下修改这个方法的逻辑;或者对于java这种语言也是一样的,一个类拥有的方法和属性是固定的,虽然java拥有了反射的能力(这可以看做已经具备了初步元编程能力),可以在运行时进行自省,但是也无法进一步拓展自身的逻辑和功能。可以看出,这些编程语言里,”程序逻辑”和”数据”是完全分割开来的,数据可以修改变动,而逻辑是冰冷凝固的,它们之间泾渭分明,区分十分明显。 而所谓元编程,就是抹除这种界限的能力。

改变宇宙公理

假设我们所处的世界真的是被AI控制,整个人类文明作为一枚电池被Matrix控制(电影《黑客帝国》),而AI构建的Matrix系统也的确是基于人类的数学公理构建起来的,而我们最基础的数学公理不外乎就是1 + 1 = 2:

1
puts 1 + 1  # => 显而易见输出2

假如AI的上帝建筑师某一天想更改这个系统升级为Matruix2,这次升级仅仅是想看看更改最简单的宇宙公理让1 + 1 = 3,他想看看这会对人类社会造成什么影响;试想如果建筑师不懂得元编程的能力,那么意味着Matrix2是一次彻底的重构:他必须重建所有数学公理,并且基于这些公理重新构建Matrix,这个工作量想想都觉得可怕。

那如果建筑师懂得元编程,他要做的事情就是给Matrix打个系统补丁,补丁文件内容只有简单几行代码:

1
2
3
4
5
6
module Patch
  def +(num)
    super.succ
  end
end
Fixnum.prepend Patch

这段代码的含义是对所有整数加法添加了一个补丁,每个加法运算都多加1,所以基于加法的公理均自动采纳这项变更。

Matrix应用这个补丁后,所有系统中加法都会多加1

1
puts 1 + 4 # => 输出为6

这就是元编程,你可以运用他修改程序逻辑,漆黑的运行时在你面前变得明朗起来了。

调用链

如果你觉得改变宇宙公理这个场景太过虚幻,并且你碰巧也没看过黑客帝国(oops,真是不幸,在学习各种编程技巧前我建议您真应该去看下这部电影),那么你肯定会质疑:好像我永远遇不到元编程的应用场景。那么好吧,我来举一个真实世界可能真会遇到的场景:

在当前分布式系统大行其道的今天,可能我们需要将系统每个函数调用时间记录下来,即函数A-->函数B-->函数C-->函数n我们想在每个函数入口和出口打点,这样我们就能够将这个调用栈串联起来

假设说我们的原系统是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass
  def hello(to)
    puts "Hello #{to}"
    self.talk to
  end
  def talk(to)
    puts "Talk to #{to}"
  end
end

MyClass.new.hello "jason"
# 输出为:
# Hello jason
# Talk to jason

很简单,我们也可以利用元编程的技术,用打点进出逻辑将所有函数包裹起来即可,也打这么一个补丁:

1
2
3
4
5
6
7
8
9
10
MyClass.class_eval do
  self.instance_methods(false).each do |mtd|
   alias_method "old_#{mtd}",mtd
   define_method mtd do |*args|
     puts "==========Enter #{mtd}@#{Time.now}=========="
     send "old_#{mtd}",*args
     puts "==========Leave #{mtd}@#{Time.now}=========="
   end
  end
end

再次运行原程序,输出则变成:

1
2
3
4
5
6
==========Enter hello@2017-08-02 23:23:25 +0800==========
Hello jason
==========Enter talk@2017-08-02 23:23:25 +0800==========
Talk to jason
==========Leave talk@2017-08-02 23:23:25 +0800==========
==========Leave hello@2017-08-02 23:23:25 +0800==========

毫无疑问,不借助元编程的能力,也有各种解决这个问题的办法,但是如果你懂得这项技术,那么你会发现问题可以被解决得如此优雅,不用侵入到原代码的任何逻辑,就可以修改代码逻辑。试想下如果你想修改的逻辑是第三方库的代码, 而你甚至没有他们的源码,此时如果没有元编程的能力,是很难做出逻辑调整的。

元编程改变了什么

回到感性的部分,为什么向没有接触过元编程的人那么难以解释这个概念呢?其实正是因为这个概念本身就处于受众知识网络之外,所以当试图使用类比推导来描述时很容易使得这个概念越发让人迷惑。

那么了解这个概念之后,它到底改变了我什么思考角度呢?其实是看问题不再那么理所当然,面临一个问题时亦或者是已经拿到解决方案了,还会反向质疑自己,是否存在一种可能性,这种可能性甚至是超越自身知识范畴的,或者是和自己思维习惯相悖离的,而这种可能性很可能就是更优雅的解决方案。

除了思考的角度,对编码工作本身有什么实质性的作用吗?很遗憾,可能并没有。生产环境中的C,java,golang等等开发,很难用到这项技术,甚至于即便你处在某个非常极客的公司,使用了ruby/lisp作为生产语言,泛滥使用元编程也会直接导致项目的不可维护,导致最终项目leader将元编程作为禁术封印起来。但这些都不应该成为阻碍你了解它的理由,拓宽自身的视野,这更重要。

写在最后

最后,我还是以元编程界一个禅语论道结束吧:

编程弟子在跟随元编程大师一年后,终于掌握了所有的元编程能力。秋意微凉,师徒二人于树下盘坐,弟子回想所学,愈发迷惑,问道: 师傅, 我还是不明白,到底什么是元编程?

树上飘落最后一片秋叶,老禅师睁开眼,轻声一叹:孩子,这世上哪有什么元编程,就是编程啊。

弟子顿悟,乃成…

P.S. 你真的不觉得人工智能的基础之一就是元编程吗? 机器学习总结规律,元编程再将这些规律内化,自身改变自身逻辑,这不就是AI进化的基础吗。

Comments