Delphi
embarcadero
objectpascal
DelphiDay 22

Delphi とラムダ式とクロージャ


はじめに

これは Delphi Advent Calendar 2018 の 22 日目の記事です。

Delphi 10.3 Rio で型推論可能なインライン変数宣言ができるようになったのは 12 日のアドベントカレンダーで書いた通りなのですが、無名メソッド関連でちょっと疑問も出てきました。

今回の記事はテクニカルな内容ではありません。識者の方はコメント欄で私の疑問に答えて頂けると幸いです。


疑問

…の前に、Delphi に詳しくない方のためにちょっとだけ予備知識を。


  • 多くのプログラミング言語で言う所の "関数" は、Delphi (Pascal) だと戻り値を持たない procedure (手続き) と戻り値のある function (関数) の二つに区別されています。

  • Delphi のクラス (class) には手続きや関数を実装する事ができます。これはメソッドと呼ばれます。

  • クラスの静的クラスメソッド (class method ;static;) はメソッドと呼ばれますが、実態はクラスに属する手続きや関数です。なんだい、そのトゲナシトゲトゲみたいな呼び方は。

  • Delphi の構造体 (record) には手続きや関数を実装する事ができます。これもメソッドと呼ばれます。

  • 手続き / 関数をひっくるめて関数と呼ばれることがあります (面倒くさいので)。

  • 手続き / 関数 / クラスやレコードのメソッドをひっくるめてメソッドと呼ばれることがあります (面倒くさいので)。

  • Delphi の無名関数は無名メソッドと呼ばれており、メソッドとしても普通の手続き / 関数としても使えます。

  • Delphi には手続き型 (関数ポインタ)、メソッドポインタ型 (of object)、無名メソッド型 (reference to) があります。

…ややこしいですね。でも今回の記事では用語を正確に使わないと混乱すると思います。


Q1: ラムダ式について

Wikipedia の無名関数の項ラムダ式には各言語の例が記述されています。

C++ のラムダ式のコードはこう書かれています。

auto add = [](int x, int y) { return x + y; };

std::cout << add(2, 3) << std::endl;

-> 記号と戻り値の型を省略しないとこうなります。

auto add = [](int x, int y) -> int { return x + y; };

std::cout << add(2, 3) << std::endl;

Delphi 10.3 Rio ではこのような記述が可能です。

var add := function (x, y: Integer): Integer begin Exit(x + y) end;

writeln(add(2, 3));

Delphi のコレはラムダ式と言えるのか?言えないのであれば何を満たせばラムダ式と言えるのかを知りたいです。

なお、10.2 Tokyo 以前 (2009 以降) だと、

program Project1;

{$APPTYPE CONSOLE}

uses
SysUtils;

var
add: TFunc<Integer, Integer, Integer>;
begin
add := function (x, y: Integer): Integer begin Exit(x + y) end;
writeln(add(2, 3));
end.

こんな書き方になります。TFunc というのは C# のデリゲート Func に相当します。同じく TProc は デリゲート Action に相当します。

個人的な意見としては Delphi のはラムダ式とは言えないと思っています。単に Delphi の無名メソッドの書き方が C++ のラムダ式の省略記法に似通っているだけで "ラムダ" ではないと思います。

JavaScript のコレは無名関数ですしね。

var add = function(x, y){ return x + y; }; 

alert(add(2, 3));

C++ の -> あるいは C# の => に相当する演算子 (または JavaScript のアロー関数式) がないので Delphi のは "無名メソッド型への無名メソッドの代入" でしかないんじゃないかなと。三項演算子と IfThen() / Iif() くらいの違いがあると思います。

// C++

auto add = [](int x, int y) { return x + y; }; // ラムダ式
auto add = [](int x, int y) -> int { return x + y; }; // ラムダ式

// JavaScript
var add = function(x, y){ return x + y; }; // 無名関数
var add = (x, y) => x + y; // ラムダ式

// C#
Func<int, int, int> add = delegate (int x, int y) { return x + y; }; // 無名関数
Func<int, int, int> add = (x, y) => x + y; // ラムダ式

// Delphi
var add: TFunc<Integer, Integer, Integer> := function (x, y: Integer): Integer begin Exit(x + y) end; // 型推論しない場合
var add := function (x, y: Integer): Integer begin Exit(x + y) end; // 型推論した場合

もっと言うなら、Delphi の場合 [入力パラメータ (左辺)] [ラムダ演算子] [文 (または式) (右辺)] の形式で無名メソッドが書ければそれはラムダなのだと思います。Delphi の疑似コードだとこんな感じです (ラムダ演算子として => を使っています)。

var add := (x, y: Integer) => begin Exit(x + y) end: Integer; // 疑似コード

文が単文なら begin end は省略可能...みたいな。

var add := (x, y: Integer) => Exit(x + y): Integer; // 疑似コード

文のトコが式なら戻り値の型も省略可能...みたいな。

var add := (x, y: Integer) => x + y; // 疑似コード

省略しないとこんな感じ...みたいな。

var add := (x, y: Integer) => (x + y): Integer; // 疑似コード

ジェネリックラムダ...みたいな。

var add := <T>(x, y: T) => x + y; // 疑似コード

パースの時にバックトラックが発生するだろうし Delphi では絶対に採用されない文法だろうなぁ...そして上二つはラムダじゃなくてラムダかなぁ...。

ラムダ式の話はさておき、10.3 Rio では型推論が可能になり、しかも無名メソッド型に対する型推論が可能なので割とスッキリと書けるのですね。

正直、従来の Delphi の無名メソッドは "メソッド" の方はともかく "関数/手続き" の方は使い勝手が悪かったように思います。10.3 Rio でインライン変数宣言が可能になったので、"関数/手続き" の方の無名メソッド (ややこしいけど無名関数の事) の利用価値も高まったかもしれません。


Q2: クロージャについて

クロージャで実現できる機能の中には、Pascal (Delphi) の関数内関数 (入れ子関数) で実現できるものがあります。

procedure Outer;

var
x: Integer;
procedure Inner;
begin
Writeln(x);
Inc(x);
end;
begin
Inner(); // 0
Inner(); // 1
Inner(); // 2
end;

これがクロージャと言えないのは解るとして、Delphi 10.3 Rio ではこのような記述が可能です。

program Project1;

{$APPTYPE CONSOLE}

uses
System.SysUtils;

function Func(const Value: Integer): TProc;
begin
var x := Value;
Result := procedure
begin
Writeln(x);
Inc(x);
end;
end;

begin
var Inner1 := Func(1);
Inner1(); // 1
Inner1(); // 2
Inner1(); // 3

var Inner2 := Func(10);
Inner2(); // 10
Inner2(); // 11
Inner2(); // 12

Inner1(); // 4
Inner2(); // 13
Inner1(); // 5
Inner2(); // 14

readln;
end.

フィボナッチ数列は以下のように書けます。

program Project1;

{$APPTYPE CONSOLE}

uses
System.SysUtils;

function Fibonacci: TFunc<Integer>;
begin
var Prev := 0;
var Last := 1;
Result := function: Integer
begin
var Tmp := Last;
Last := Prev + Tmp;
Prev := Tmp;
result := Last;
end;
end;

begin
var f := Fibonacci();

for var i:=0 to 9 do
Writeln(f());

readln;
end.

out パラメータを使い、エンクロージャを関数ではなく手続きで書くことも可能です。

program Project1;

{$APPTYPE CONSOLE}

uses
System.SysUtils;

procedure Fibonacci(out Inner: TFunc<Integer>);
begin
var Prev := 0;
var Last := 1;
Inner := function: Integer
begin
var Tmp := Last;
Last := Prev + Tmp;
Prev := Tmp;
result := Last;
end;
end;

begin
var f: TFunc<Integer>;
Fibonacci(f);

for var i:=0 to 9 do
Writeln(f());

readln;
end.

Delphi のコレはクロージャと言えるのか?言えないのであれば何を満たせばクロージャと言えるのかを知りたいです。

なお、10.2 Tokyo 以前 (2009 以降) だと、

program Project1;

{$APPTYPE CONSOLE}

uses
SysUtils;

function Fibonacci: TFunc<Integer>;
var
Prev, Last: Integer;
begin
Prev := 0;
Last := 1;
Result := function: Integer
var
Tmp: Integer;
begin
Tmp := Last;
Last := Prev + Tmp;
Prev := Tmp;
result := Last;
end;
end;

var
i: Integer;
f: TFunc<Integer>;
begin
f := Fibonacci();

for i:=0 to 9 do
Writeln(f());

readln;
end.

こんな書き方になります。

個人的な意見としては Delphi のコレはクロージャの要件を満たしていると思います。


Q3: 無名メソッドの再帰について

3 つ目は Delphi の無名メソッドで再帰を書くことは可能なのか? という疑問です。

例えば、フィボナッチ数を求める以下のコードはコンパイルエラーになります。

program Fibonacci;

{$APPTYPE CONSOLE}

uses
System.SysUtils, System.Math;

begin
var f := function (n: Integer): Integer
begin
Result := IfThen(n < 2, n, f(n - 1) + f(n - 2));
end;

Writeln(f(5));
end.

また、以下のような書き方だと実行時にスタックオーバーフローになります。

program Fibonacci;

{$APPTYPE CONSOLE}

uses
System.SysUtils, System.Math;

begin
var f: TFunc<Integer, Integer>;
f := function (n: Integer): Integer
begin
Result := IfThen(n < 2, n, f(n - 1) + f(n - 2));
end;

Writeln(f(5));
end.

無名再帰を書いた方が楽になる場面というのがあまり想像がつかないのですが、できたらできたで面白いかと思っています。

個人的な意見としては Delphi での無名再帰はできないんじゃないかと思っています。

※ ↑ できました。コメ欄を参照の事。


おわりに

ラムダしきとかクロージャのせつめい(ていぎ)にかいてあることが、さいとによってまちまちなのでこんらんしているのです。おしえてえらいひと!

そういえばむめいメソッドをそくじっこうする、こんなのは

program Project1;

{$APPTYPE CONSOLE}

uses
System.SysUtils;

begin
(procedure
begin
writeln('Hello, world.')
end)();

var v := (function (v: Integer): Integer begin result := v + 4 end)(5) +
(function (v1, v2: Integer): Integer begin result := v1 * v2 end)(7, 13);
writeln(v);
end.

JavaScript ではいっふぃー(IIFE) っていうんだってね。

See Also: