はじめに
これは Delphi Advent Calendar 2018 の 22 日目の記事です。
Delphi 10.3 Rio で型推論可能なインライン変数宣言ができるようになったのは 12 日のアドベントカレンダーで書いた通りなのですが、無名メソッド関連でちょっと疑問も出てきました。
今回の記事はテクニカルな内容ではありません。識者の方はコメント欄で私の疑問に答えて頂けると幸いです。
疑問
…の前に、Delphi に詳しくない方のためにちょっとだけ予備知識を。
- 多くのプログラミング言語で言う所の "関数" は、Delphi (Pascal) だと結果の返らない procedure (手続き) と結果の返る (戻り値のある) function (関数) の二つに区別されています。「結果の返らないものは関数ではないだろう?」という理屈です。これらを区別しない呼び方は ルーチン (routine) です。
- ルーチンを関数と呼ぶことがあります (多くの言語ではそう呼ぶので)。
- Delphi のクラス (class) にはルーチンを実装する事ができます。これはメソッドと呼ばれます。
- クラスの静的クラスメソッド (class method ;static;) はメソッドと呼ばれますが、実態はクラスに属するルーチンです。なんだい、そのトゲナシトゲトゲみたいな呼び方は。
- Delphi の構造体 (record) にはルーチンを実装する事ができます。これもメソッドと呼ばれます。
- ルーチンとメソッドをひっくるめてメソッドと呼ぶことがあります (どこに実装されているかが重要でない場合)。
- Delphi の無名関数は無名メソッドと呼ばれており、メソッドとしても普通のルーチンとしても使えます。
- Delphi には手続き型 (関数ポインタ)、メソッドポインタ型 (of object)、メソッド参照型 (reference to) があります。
Delphi | 多くの言語 | |
---|---|---|
ルーチン (サブルーチン) | ルーチン | 関数 |
実行時に値を返さないルーチン | 手続き | (void) 関数 |
実行時に値を返すルーチン | 関数 | 関数 |
ルーチンに与えるもの | パラメータ | 引数 |
関数が返すもの | 結果 | 戻り値 |
…ややこしいですね。でも今回の記事では用語を正確に使わないと混乱すると思います。
- 手続き型 (DocWiki)
- メソッド (DocWiki)
- クラスとオブジェクト(Delphi)(DocWiki)
- レコード型 (高度) (DocWiki)
- Delphi での無名メソッド (DocWiki)
- 関数ポインタとメソッドポインタの相互代入のおはなし。(全力わはー)
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 でインライン変数宣言が可能になったので、"関数/手続き" の方の無名メソッド (ややこしいけど無名関数の事) の利用価値も高まったかもしれません。
- 無名関数 (Wikipedia)
- ラムダ式 - C++日本語リファレンス (cpprefjp)
- ラムダ式 (C# プログラミング ガイド) (docs.microsoft.com)
- 三項演算子が (ちょっと) 羨ましい Delphi ユーザーの集い (Togetter)
- デリゲート (プログラミング) (Wikipedia)
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 Counter(const Value: Integer): TProc;
begin
var x := Value;
Result := procedure
begin
Writeln(x);
Inc(x);
end;
end;
begin
var Up1 := Counter(1);
Up1(); // 1
Up1(); // 2
Up1(); // 3
var Up2 := Counter(10);
Up2(); // 10
Up2(); // 11
Up2(); // 12
Up1(); // 4
Up2(); // 13
Up1(); // 5
Up2(); // 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 のコレはクロージャの要件を満たしていると思います。
- クロージャ (Wikipedia)
- 無名メソッド変数のバインディング (DocWiki)
- Pascal(delphi)の関数内関数はクロージャなの? (lethevert is a programmer)
- out パラメータ (DocWiki)
- 入れ子関数 (Wikipedia: en)
- 関数内関数のススメ (Qiita)
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: