根据 Toby Davies 论文 Homoiconicity, Lazyness and First-Class Macros 的说法,反射(Reflection)其实是通过允许在运行时存取法式数据,以改动法式行为的法式设想手艺[1]。他认为,反射其实是一种“语义同像(Semantic Homoiconicity)”。具有语义同像性的语言必需把法式中的一些内部形态,好比符号表、指令指针,表露给法式员。如许,法式员就能够把那些工具当做 First-Class-Value 而很容易地操做它们。
如许看来,反射并非一门语言中需要的特征:因为请留意,反射原来就是一种在运行时(Runtime)展示出来的行为罢了,而大大都静态语言在编译时,就把反射所需要的信息给剥离了出来(想一想,为了利用 gdb 调试,我们是不是还得额外添加一个 -g 选项,让 gcc 保留调试信息?)。所以说,供给反射那种特征,无外乎是出于为了进步消费力、进步编程时的灵敏性等考量。
进步消费力?进步编程时的灵敏性?口说无凭,你能处理几个例子么?稍安勿躁,我将用我最喜好的两个语言 Ruby 和 Scheme 来为你演示那种魔法。
Ruby 里面有个叫
what_methods的 gem,它的用处如下:
irb> require what_methods => true irb> [1, 2, 3, 4].what? 4 [1, 2, 3, 4].last == 4 [1, 2, 3, 4].pop == 4 [1, 2, 3, 4].length == 4 [1, 2, 3, 4].size == 4 [1, 2, 3, 4].count == 4 [1, 2, 3, 4].max == 4 => [:last, :pop, :length, :size, :count, :max]有没有觉得很奇异?其其实 Ruby 中实现如许的功用十分好做,可能只需要数行代码:
class Object def what?(value) methods.select do |method| begin dup.send(method) == value rescue false end end end end起首我们翻开 Object 类,让每个 Ruby 对象都有 what? 办法,然后我们通过 methods 办法获得对象的所有办法构成的数组,利用 select 高阶函数选出契合要求的办法。有的办法不是无参的,所以我们加上异常处置 begin..rescue..end 来躲避 ArgumentError ;用 dup 办法深拷贝对象,制止毁坏性办法改动原对象形态;挪用 send 办法,施行办法挪用并判断返回值能否为我们期望的 value 。
我们无妨来测试一下我们写的 what? 办法:
irb> the_fuck = 123 => 123 irb> "123".what? the_fuck 123=> [:to_i, :to_f, :to_r, :to_c] # to_i 向 Integer 转换 # to_f 向 Float 转换 # to_r 向 Rational 转换 # to_c 向 Complex 转换恩,仿佛能够一般利用,给出的成果也十分合理;至于尺度输出中乱入的123,那是因为反射出的办法中有个用于将对象输出至 $> 的 display 的办法,它也被我们挪用了。
你也许会说:“啊,那只是一些投契取巧地 trick ,那种魔术不敷以震撼我。”你无妨深切研究充溢着 Reflection 的 Ruby on Rails 的源码——那也是 Ruby 界公认的用元编程(Metaprogramming)进步消费力的更佳理论。但我如今要打住对 Ruby 的讨论,而转而用 Scheme 为你展现一个更为巧妙而奇异的“魔术”:若是你觉得操做符号表幼稚而浅薄的话,我们来看看在运行时操做“指令指针”会有什么样的图景。
“指令指针”,狭义上来说就是EIP、PC,也就是下一条要施行指令的地址,但说广义了,或者说素质上来说,就是法式的运行流。R5RS 中那么描述 Scheme 语言:
Scheme 是第一种被普遍利用的,采用第一级逃逸过程(Escape Procedure)的法式设想语言。第一级逃逸过程能够合成所有已知的挨次控造构造。
在大大都其他语言中只在幕后起感化的继续,在 Scheme 中也拥有“第一级”形态,那是 Scheme 的一个独树一帜的特征。继续能够用于实现大量的差别高级控造构造,如非部分退出(Non-local exits)、回溯(Backtracking)和协做法式(Coroutine)等。
Scheme 中的“指令指针”,就是继续(Continuation),在运行时操做那些 continuation ,最常利用的就是 call-with-current-continuation ,也就是我们常说的 call/cc 。
领会 Lisp/Scheme 的法式员必然晓得,我们写代码的时候是没有像 C 语言那样的 return 关键字让我们能够间接跳出函数。但 call/cc 的维基词条上就给出了若何用继续来实现 C 气概的 return 。
(define (f return) (return 2) 3) (display (f (lambda (x) x))) ; displays 3 (display (call-with-current-continuation f)) ; displays 2代码量少得可怜,不外良多同窗仍然不知所云。简单来说,我们能够把最初一行代码笼统地看成 (display e) ,此中 e 是 (call/cc f) 对吧?
为了求值 (display e) ,我需要对参数求值,那时候就有一个继续(记为
c1c_1)——理论上来说,对任何一个表达式求值,都有一个继续在期待那个表达式的值:我希望得到求值表达式 e 的成果。对 C 语言函数挪用机造比力熟悉的法式员在此时应该有如许的觉解:若是表达式 e 是函数挪用的话,我们需要施行压栈操做,此中,我们需要把 caller 的 EIP 也给压入栈中,以便 e 求值完毕后,能返回到 caller 中。
如今,我们求值 e ,即 (call/cc f) 。此时的 current-continuation 是啥?就是
c1c_1。call/cc 把那个继续传递给函数 f ,也就是施行 (f c1) ,留意 Scheme 中的 Continuation 是 first-class 的,那也就意味着你能够把它当参数传递。同时也需要留意,挪用 (f c1) 的时候也会有个继续——类比一下,C 语言的函数挪用会压栈——但那里的挪用是 tail-call ,产生的继续是能够归约到
c1c_1上去的。
我们进入 f 的内部,需要求值 (return 2) ,留意,那里也有一个继续
c3c_3期待着那个表达式求得的值。不外你还记得吗?我们传入的 return 不外是一个继续、一个逃逸过程。对逃逸过程的应用,招致法式会忽略表达式其时起感化的任何表达式——
c3c_3被无情地忽略,然后把 2 放到
c1c_1处的 EAX ,然后弹栈,回到
c1c_1那里的 caller 中去。如许,(display e) 那里求得 e 的值就是 2 ,而不是我们料想中的 3 !
那里,我用了 C 语言的函数挪用机造做了简单的类比,那是为了让各人有了如许的印象:原来应该对法式员通明的机造,如今却表露给了法式员,使得我们有才能去把持它,让它可以更灵敏地完成我们的工做。
所以说,反射那种工具,确实毁坏了封拆的初志,但他们两者之间并非绝对的对立。想一想,我们封拆、隐藏细节、成立笼统屏障,无外乎都是为了降低法式的复杂度——那是工程上的折衷,因为面临廊腰缦回的法式构造,人脑太不敷用了。但是在大量的理论中我们发现,我们笼统出来的通用形式并非银弹,良多问题在它构建的框架之下处理起来就十分费事——若是 C 语言仅仅根据通俗的压栈去向理函数挪用,那么立即是
(λx.(xx)λx.(xx))(\lambda x .(x \ x) \ \lambda x .(x \ x) )如许能在
O(1)O(1)空间内完成的 tail-call ,也会形成仓库溢出。
所以有了反射那么一手,把良多难以预测的问题留到运行时,动态地去考虑去处理。那也就使得我们在穷途末路时,还能够有一道后门开着让我们大模大样地进入。
Reference:
[1] Toby Davies, Homoiconicity, Lazyness and First-Class Macros.
[2] 王咏刚 译,算法语言 Scheme 修订^5陈述
[3] G. E. Revesz,
Lambda-calculus, Combinators and Functional Programming