ObjectPascalというよりもDelphi限定の話になりますが、
変な最適化をしない素直なコードを吐く、とっても純朴なコンパイラです。
故に…ちょいと怪しい操作をインラインアセンブラで埋めこんでやれば、
物の見事に、ちょっと信じられないような機能を追加できます。
というわけで、極悪コードシリーズです。
驚きの新機能から役たたずの環境依存コードまで。
まあ、ネタがそんなにあるわけでは無いですけど。
λ演算、というのがあります。
理屈はよくわかりませんが、関数に関数を渡して、計算を進めていく事を指すようです。
Pythonの予約語lambdaなどが有名ではないでしょうか。
Pythonではfrom __future__ import nested_scopes
と書くことで、親関数のローカル変数に触れることができます。
無論、本家の関数型言語、たとえばHaskellは、中途半端な式すら関数として渡すことができたり、より強力です。
渡される方の式をλ式、関数を引数に持つ関数を高階関数と呼ぶようです。
*1
手続き型の普通の言語、でも、ある程度の真似はできます。 しかし制御文を作成できるまでのパワーは、やはり関数型言語でなければありません。 制御文を作るには、式、または、ブロックを、引数に渡せることが条件です。 関数の呼出し前にそれらが実行されてはダメ。 で、関数ポインタを使えば、もしグローバル変数しか考えないのであれば、万事OKです。 それは要するに普通のコールバック関数ですけどね。 上述の言語の何がスゴイって、1)関数をその場で無名で定義できることと、2)コールバックされた式中で呼びだし元のローカル変数に触れられることです。 これが揃えば、高階関数を制御文のように使えることになります。
ところで、Pascal系の言語では、関数内関数が定義できるのが普通です。 Delphiも例に漏れずで、関数内関数はC++BuilderではなくDelphiを選ぶ理由のひとつにはなっているかもしれません。 *2
で…これから書くことを知ると、あなたはもうC++は使えなくなるかもしれません。 関数内関数を使って、自作制御文を実現できるとしたら、いかがですか?
前述の用件1)は、「その場で」「無名で」とは程遠いですが、我慢できないというほどでもないでしょう。 元々Pascal系言語は記述が面倒なのが特徴ですし。 重要なのは、用件2)のほうです。これは一見、コンパイラのサポートが無いとどうしようもなく思えます。 実行効率も悪そうです。 そもそも、Adaならいざ知らずPascalは関数内関数のポインタを取れなかった筈──。
まずは見てもらいましょう。太字に注目。
program Project1; {$APPTYPE CONSOLE} uses Windows; function GetContext: Pointer; register; asm mov EAX, EBP end; procedure Callback(P, Context: Pointer); register; asm push EDX call EAX pop ECX end; procedure Loop(P, EBP: Pointer); var I: Integer; begin for I := 0 to 9 do Callback(P, EBP) end; procedure Outer1; var S: string; procedure Inner1; begin WriteLn('*', S, '*') end; begin S := 'Hello'; Loop(@Inner1, GetContext) end; function EnumProc(Wnd: HWND; var D: TMethod): LongBool; stdcall; asm mov EDX, D mov EAX, TMethod[EDX].Data push EAX mov EAX, Wnd mov ECX, TMethod[EDX].Code call ECX pop ECX end; procedure EnumWindows(Proc, Context: Pointer); var D: TMethod; begin D.Code := Proc; D.Data := Context; Windows.EnumWindows(@EnumProc, LPARAM(@D)) end; procedure Outer2; var A: array of HWND; I: Integer; T: array[0..100] of Char; function Inner2(Wnd: HWND): LongBool; begin if IsWindowVisible(Wnd) then begin SetLength(A, Length(A) + 1); A[High(A)] := Wnd end; Result := True end; begin EnumWindows(@Inner2, GetContext); for I := High(A) downto Low(A) do begin GetClassName(A[I], T, 100); WriteLn(T) end end; begin Outer1; Outer2 end.
一見、スタックは大丈夫なのかと思われるかも知れません。 何ならちょっと手を動かして試してごらんあれ。関数内関数のポインタを普通の手続き型変数に入れてコール! …まあ、多分、親のローカル変数に触ろうとした時点で落ちるでしょう。 (親のローカル変数にさえ触らなければ無害ですが)
動作としては、要するに親のEBPを最初にpushしてやるだけなのですけどね。 関数内関数内部では、push EBP; mov EBP, ESP というお決まりのスタックフレーム作成コードを経て、最終的にコンパイラが吐いたコードでは[EBP + 8]経由で親のローカル変数にアクセスしています。 CPUウィンドウで追ってやれば一目瞭然です。 register(fastcall)の時もpushです。解放は常に呼びだし側で行ないます。 何故かcdecl風味。 このお陰で、関数内関数で親のローカル変数を使ってさえいなければ、関数内関数を普通の関数としてポインタを渡せますし、逆に、このテクニックで作成した高階関数に、関数内関数では無く普通の関数を渡しても無害です。
本来なら、この手の記述には、グローバル変数を使うか、データへのポインタを別に引き渡し、伝播してもらうしか無かったわけです。 もうコールバック関数のために、わざわざ構造体を作る必要は無いです。 …いや、機能的にはその通りなのですが、得られるメリットは、そんな言葉では説明し切れません。 応用は果てしなく広がっています。 Iterator, Template Methodをはじめとするデザインパターンの一部もこれで置き換えることができます。 オブジェクト指向言語と関数型言語でアプローチが違う例ですが、今や、書き易い方を選び、併用が可能です。 この開放感みたいなものは、危険なコードと自覚していても、最早後戻りできない魅力があります。
ただし、これは関数内でこっそり使うようなテクニックでは無く、外に向けてこそ便利な機能なので、こんなものが使えるとなると書き方が一変してしまいます。 CもPascalもJavaも構造化BASICも、構造化言語としての機能はだいたい同じ、という前提を覆してしまうわけです。Algolの系譜すら揺らぎかねません。
嘘臭い煽り文句は置いといて、ネイティブだからできる技なんだよなー。 やっぱ.NETは面白く無い方向に進んでいると思える今日このごろ。 趣味プログラマの戯言でございます。 実際に僕はこれがあることを前提にコードを書いているので、Delphi.NETに移行できないかも…。 Borlandさん、言語機能としてのλ関数を実装してくれませんか? 関数内関数という下地があるし、Selfが関数ポインタにくっついたprocedure of object記法もあるし、その延長で、EBPが関数ポインタにくっついたのを作るだけなので、おねがいします。 C++Builderの事なんかこの際忘れてっ。
ですので、D言語が、delegateに関数内関数を渡せる仕様なのは嬉しかったです。 Delphiのx86ネイティブ版が無くなったら、次はDだな…。
考えてみれば呼びだし側もPascalで書けます。
asm push Context end
と asm pop Context end
で挟んで、その間で、普通に呼び出せばいいだけです。
↓こんな感じ。
function EnumProc(Wnd: HWND; var D: TMethod): LongBool; stdcall; type TProc = function(Wnd: HWND): LongBool; var Context: Pointer; begin Context := D.Data; asm push Context end; Result := TProc(D.Code)(Wnd); asm pop Context end; end;
なにげなくCodeCentralをうろついていたら、Jim Fergusonなる人物が同じことをやっておられる。 僕より早く。 これは悔しい。
微妙に違うのは、Fergusonさんは最適化をOFFにすることで、高階関数側の最初で親のEBPを取得していること。 その為に、僕のコードで言うところのContextパラメータが不要になっています。 最適化OFFとどちらがいいかは微妙なところなので、甲乙付け難しとしておきましょう。 …僕だって呼ばれた側が呼び出しもとのEBPを知れることぐらい気付いてましたよ。 だけど最適化がかかると旧EBPの保存場所が特定できなくなるのであきらめたんですよ。 最適化を切る英断はできませんでしたねえ。
追加パラメータみたいな事は、cdeclが楽ですね。 これですと、普通の関数にコールバックする時は、単なるデータポインタとしても使えますし。
function EnumProc(Wnd: HWND; var D: TMethod): LongBool; stdcall; type TProc = function(Wnd: HWND; Context: Pointer): LongBool; cdecl; var Context: Pointer; begin Context := D.Data; Result := TProc(D.Code)(Wnd, Context); end;
呼び出される側もcdeclを付ける必要があります。
2002-10-22 | 追記1 |
2002-10-29 | 文章を推敲 |
2003-03-31 | 追記2 |
2003-06-24 | 表現を少し直す |
2003-08-03 | 表現をかなり直す |
2003-11-09 | 追記3 |
*1 間違ってるかも。間違ってたら教えてね。
*2 もっとも、最強の理由は、高速コンパイルなのですが。