./index.html ../index.html

ObjectPascal Magic Programming
λ with INNER FUNCTION

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だな…。

追記1

考えてみれば呼びだし側も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;

追記2

なにげなくCodeCentralをうろついていたら、Jim Fergusonなる人物が同じことをやっておられる。 僕より早く。 これは悔しい。

微妙に違うのは、Fergusonさんは最適化をOFFにすることで、高階関数側の最初で親のEBPを取得していること。 その為に、僕のコードで言うところのContextパラメータが不要になっています。 最適化OFFとどちらがいいかは微妙なところなので、甲乙付け難しとしておきましょう。 …僕だって呼ばれた側が呼び出しもとのEBPを知れることぐらい気付いてましたよ。 だけど最適化がかかると旧EBPの保存場所が特定できなくなるのであきらめたんですよ。 最適化を切る英断はできませんでしたねえ。

追記3

追加パラメータみたいな事は、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を付ける必要があります。


*1 間違ってるかも。間違ってたら教えてね。

*2 もっとも、最強の理由は、高速コンパイルなのですが。