関数オブジェクトから処理内容を取得して書き換える―を Squeak Smalltalk で

ただの言語には興味ありません。この中に「関数オブジェクトから関数の処理内容を取得して書き換えられる言語」「グローバル変数クロージャを使わずに、呼ぶ度に動作が変わる関数を作れる言語」「標準入出力へのIOが標準では存在しない言語」があったら僕に報告しなさい、以上。

Twitter / tana


実に面白い。やってみよう。

趣旨をはずしているかもしれないけれど(たぶんはずしているだろうけど^^;)、きにしない。


■関数オブジェクトから関数の処理内容を取得して書き換える

Squeak Smalltalk では、クラスに「>> #メソッド名」(あるいは「compiledMethodAt: #メソッド名」)というメッセージをを送信することで、関数メソッドならぬ「メソッドオブジェクト」を得ることができます。

fact := Integer >> #factorial   "=> a CompiledMethod (3603) "


ちなみにメソッドオブジェクトを静的にコールするには #valueWithReceiver:arguments: を使います。

10 factorial   "=> 3628800 " "通常の動的コール"
fact valueWithReceiver: 10 arguments: #()   "=> 3628800 " "静的コール"


このメソッドオブジェクトに対しさらに getSource を送信することで Smalltalk コードのかたちで、あるいは symbolic を送信することでバイトコードプログラムとして、その処理の内容を知ることができます。

fact getSource asString
=>
factorial
    "Answer the factorial of the receiver."

    self = 0 ifTrue: [^ 1].
    self > 0 ifTrue: [^ self * (self - 1) factorial].
    self error: 'Not valid for negative integers'
fact symbolic
=> 
25 <70> self
26 <75> pushConstant: 0
27 <B6> send: =
28 <99> jumpFalse: 31
29 <76> pushConstant: 1
30 <7C> returnTop
31 <70> self
32 <75> pushConstant: 0
33 <B3> send: >
34 <9E> jumpFalse: 42
35 <70> self
36 <70> self
37 <76> pushConstant: 1
38 <B1> send: -
39 <D0> send: factorial
40 <B8> send: *
41 <7C> returnTop
42 <70> self
43 <22> pushConstant: 'Not valid for negative integers'
44 <E1> send: error:
45 <87> pop
46 <78> returnSelf


さらにメソッドオブジェクトの実体は単なるバイト列なので、直接いじるのもよし―

fact byteAt: 37 put: 16r77.   "1を減ずるところを2を減じるように変更"
fact decompile

=>
factorial
self = 0
ifTrue: [^ 1].
self > 0
ifTrue: [^ self * (self - 2) factorial].
self error: 'Not valid for negative integers'

10 factorial   "=> 3840 "
fact valueWithReceiver: 10 arguments: #()   "=> 3840 "
fact byteAt: 37 put: 16r76   "元に戻す"
10 factorial   "=> 3628800 "
fact valueWithReceiver: 10 arguments: #()   "=> 3628800 "


(通常のメソッド再定義との意味の違いをほとんど見いだせませんが―)メソッドオブジェクトから元となったノードを得て、それに修正を加えて再コンパイル後、#become: ですげ替えるのもよし―

node := fact methodNode.
expr := node block statements second arguments first statements first expr.
encoder := Encoder new init: Integer context: nil notifying: nil.
expr selector: (encoder encodeSelector: #+).   "乗算を加算に変更"
fact become: (alternative := node generate)
10 factorial   "=> 56 "
fact valueWithReceiver: 10 arguments: #()   "=> 56 "
fact become: alternative   "元に戻す"
10 factorial   "=> 3628800 "
fact valueWithReceiver: 10 arguments: #()   "=> 3628800 "


まったく別の処理をコンパイルして同様にすげ替えるのもよし―

alternative := (Parser new parse: 'factorial ^self' class: Integer) generate.
fact become: alternative.
10 factorial   "=> 10 "
fact valueWithReceiver: 10 arguments: #()   "=> 10 "
fact become: alternative   "元に戻す"
10 factorial   "=> 3628800 "
fact valueWithReceiver: 10 arguments: #()   "=> 3628800 "


と、お好きなアプローチで処理を書き換えることが可能です。

なお、Smalltalk で関数オブジェクトと言うと他にも「ブロック」という無名関数オブジェクトがありますが、その実体もじつはメソッドなので原則としてこれと同じことが(もうちょっとややこしくはなりそうですが―)できるはずです。試してませんけど。^^;


■呼ぶ度に動作が変わる関数を作る

厳密には「呼ぶ度に動作が変わる」という“一定の”動作を記述しているだけなので、題意からは外れているかもしれませんが,呼ぶ度に処理内容が変わるという解釈で、自己を書き換えて呼ぶ度に動作を変える関数を記述してみます。

UndefinedObject >> flipFlop
    | method |
    method := thisContext method.
    method byteAt: 49 put: ((method byteAt: 49) bitXor: 3).
    ^false
nil flipFlop   "=> true "
nil flipFlop   "=> false "
nil flipFlop   "=> true "
nil flipFlop   "=> false "
nil flipFlop   "=> true "
...