# Delphi とラムダ式とクロージャ --- tags: Delphi プログラミング embarcadero objectpascal created_at: 2018-12-16 updated_at: 2024-10-31 --- # はじめに これは [Delphi Advent Calendar 2018](https://qiita.com/advent-calendar/2018/delphi) の 22 日目の記事です。 Delphi 10.3 Rio で型推論可能なインライン変数宣言ができるようになったのは [12 日のアドベントカレンダー](./2802677fe1cfaa4d1332.md)で書いた通りなのですが、無名メソッド関連でちょっと疑問も出てきました。 今回の記事はテクニカルな内容ではありません。識者の方はコメント欄で私の疑問に答えて頂けると幸いです。 # 疑問 …の前に、Delphi に詳しくない方のためにちょっとだけ予備知識を。 - 多くのプログラミング言語で言う所の "関数" は、Delphi (Pascal) だと結果の返らない **procedure** (手続き) と結果の返る (戻り値のある) **function** (関数) の二つに区別されています。「結果の返らないものは関数ではないだろう?」という理屈です。これらを区別しない呼び方は **ルーチン (routine)** です。 - ルーチンを**関数**と呼ぶことがあります (多くの言語ではそう呼ぶので)。 - Delphi のクラス (**class**) にはルーチンを実装する事ができます。これはメソッドと呼ばれます。 - クラスの静的クラスメソッド (**class** method ;**static**;) はメソッドと呼ばれますが、実態はクラスに属するルーチンです。なんだい、その[トゲナシトゲトゲ](https://ja.wikipedia.org/wiki/%E3%83%88%E3%82%B2%E3%83%8F%E3%83%A0%E3%82%B7%E4%BA%9C%E7%A7%91#%E3%83%88%E3%82%B2%E3%83%8A%E3%82%B7%E3%83%88%E3%82%B2%E3%83%88%E3%82%B2)みたいな呼び方は。 - Delphi の構造体 (**record**) にはルーチンを実装する事ができます。これもメソッドと呼ばれます。 - ルーチンとメソッドをひっくるめて**メソッド**と呼ぶことがあります (どこに実装されているかが重要でない場合)。 - Delphi の無名関数は**無名メソッド**と呼ばれており、メソッドとしても普通のルーチンとしても使えます。 - Delphi には手続き型 (関数ポインタ)、メソッドポインタ型 (**of object**)、メソッド参照型 (**reference to**) があります。 | | Delphi | 多くの言語 | |:---|:---:|:-:| | ルーチン (サブルーチン) | ルーチン | 関数 | | 実行時に値を返さないルーチン | 手続き | (void) 関数 | | 実行時に値を返すルーチン | 関数 | 関数 | | ルーチンに与えるもの | パラメータ | 引数 | | 関数が返すもの | 結果 | 戻り値 | …ややこしいですね。でも今回の記事では用語を正確に使わないと混乱すると思います。 - [手続き型 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E6%89%8B%E7%B6%9A%E3%81%8D%E5%9E%8B) - [メソッド (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%EF%BC%88Delphi%EF%BC%89) - [クラスとオブジェクト(Delphi)(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%A8%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%EF%BC%88Delphi%EF%BC%89) - [レコード型 (高度) (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E6%A7%8B%E9%80%A0%E5%8C%96%E5%9E%8B%EF%BC%88Delphi%EF%BC%89#.E3.83.AC.E3.82.B3.E3.83.BC.E3.83.89.E5.9E.8B.EF.BC.88.E9.AB.98.E5.BA.A6.EF.BC.89) - [Delphi での無名メソッド (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/Delphi_%E3%81%A7%E3%81%AE%E7%84%A1%E5%90%8D%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89) - [関数ポインタとメソッドポインタの相互代入のおはなし。(全力わはー)](http://d.hatena.ne.jp/tales/20121205/1354636256) ## Q1: ラムダ式について [Wikipedia の無名関数の項](https://ja.wikipedia.org/wiki/%E7%84%A1%E5%90%8D%E9%96%A2%E6%95%B0)の[ラムダ式](https://ja.wikipedia.org/wiki/%E7%84%A1%E5%90%8D%E9%96%A2%E6%95%B0#%E3%83%A9%E3%83%A0%E3%83%80%E5%BC%8F)には各言語の例が記述されています。 C++ のラムダ式のコードはこう書かれています。 ```cpp auto add = [](int x, int y) { return x + y; }; std::cout << add(2, 3) << std::endl; ``` -> 記号と戻り値の型を省略しないとこうなります。 ```cpp auto add = [](int x, int y) -> int { return x + y; }; std::cout << add(2, 3) << std::endl; ``` Delphi 10.3 Rio ではこのような記述が可能です。 ```pascal var add := function (x, y: Integer): Integer begin Exit(x + y) end; writeln(add(2, 3)); ``` **Delphi のコレはラムダ式と言えるのか?言えないのであれば何を満たせばラムダ式と言えるのかを知りたいです。** なお、10.2 Tokyo 以前 (2009 以降) だと、 ```pascal:Project1.dpr program Project1; {$APPTYPE CONSOLE} uses SysUtils; var add: TFunc; 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 のコレは無名関数ですしね。 ```javascript var add = function(x, y){ return x + y; }; alert(add(2, 3)); ``` C++ の -> あるいは C# の => に相当する演算子 (または JavaScript のアロー関数式) がないので Delphi のは "メソッド参照型への無名メソッドの代入" でしかないんじゃないかなと。三項演算子と [IfThen()](http://docwiki.embarcadero.com/Libraries/ja/System.Math.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 add = delegate (int x, int y) { return x + y; }; // 無名関数 Func add = (x, y) => x + y; // ラムダ式 // Delphi var add: TFunc := function (x, y: Integer): Integer begin Exit(x + y) end; // 型推論しない場合 var add := function (x, y: Integer): Integer begin Exit(x + y) end; // 型推論した場合 ``` もっと言うなら、Delphi の場合 ```[入力パラメータ (左辺)] [ラムダ演算子] [文 (または式) (右辺)]``` の形式で無名メソッドが書ければそれはラムダ**式**なのだと思います。Delphi の疑似コードだとこんな感じです (ラムダ演算子として => を使っています)。 ```pascal var add := (x, y: Integer) => begin Exit(x + y) end: Integer; // 疑似コード ``` 文が単文なら **begin** **end** は省略可能...みたいな。 ```pascal var add := (x, y: Integer) => Exit(x + y): Integer; // 疑似コード ``` 文のトコが式なら結果の型も省略可能...みたいな。 ```pascal var add := (x, y: Integer) => x + y; // 疑似コード ``` 省略しないとこんな感じ...みたいな。 ```pascal var add := (x, y: Integer) => (x + y): Integer; // 疑似コード ``` ジェネリックラムダ...みたいな。 ```pascal var add := (x, y: T) => x + y; // 疑似コード ``` パースの時にバックトラックが発生するだろうし Delphi では絶対に採用されない文法だろうなぁ...そして上二つはラムダ**式**じゃなくてラムダ**文**かなぁ...。 ラムダ式の話はさておき、10.3 Rio では型推論が可能になり、しかもメソッド参照型に対する型推論が可能なので割とスッキリと書けるのですね。 正直、従来の Delphi の無名メソッドは "メソッド" の方はともかく "関数/手続き" の方は使い勝手が悪かったように思います。10.3 Rio でインライン変数宣言が可能になったので、"関数/手続き" の方の無名メソッド (ややこしいけど無名関数の事) の利用価値も高まったかもしれません。 - [無名関数 (Wikipedia)](https://ja.wikipedia.org/wiki/%E7%84%A1%E5%90%8D%E9%96%A2%E6%95%B0) - [ラムダ式 - C++日本語リファレンス (cpprefjp)](https://cpprefjp.github.io/lang/cpp11/lambda_expressions.html) - [ラムダ式 (C# プログラミング ガイド) (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions) - [三項演算子が (ちょっと) 羨ましい Delphi ユーザーの集い (Togetter)](https://togetter.com/li/851276) - [デリゲート (プログラミング) (Wikipedia)](https://ja.wikipedia.org/wiki/%E3%83%87%E3%83%AA%E3%82%B2%E3%83%BC%E3%83%88_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)) ## Q2: クロージャについて クロージャで実現できる機能の中には、Pascal (Delphi) の**関数内関数** (入れ子関数) で実現できるものがあります。 ```pascal 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 ではこのような記述が可能です。 ```pascal:Project1.dpr 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. ``` フィボナッチ数列は以下のように書けます。 ```pascal:Project1.dpr program Project1; {$APPTYPE CONSOLE} uses System.SysUtils; function Fibonacci: TFunc; 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** パラメータを使い、エンクロージャを関数ではなく手続きで書くことも可能です。 ```pascal:Project1.dpr program Project1; {$APPTYPE CONSOLE} uses System.SysUtils; procedure Fibonacci(out Inner: TFunc); 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; Fibonacci(f); for var i:=0 to 9 do Writeln(f()); readln; end. ``` **Delphi のコレはクロージャと言えるのか?言えないのであれば何を満たせばクロージャと言えるのかを知りたいです。** なお、10.2 Tokyo 以前 (2009 以降) だと、 ```pascal:Project1.dpr program Project1; {$APPTYPE CONSOLE} uses SysUtils; function Fibonacci: TFunc; 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; begin f := Fibonacci(); for i:=0 to 9 do Writeln(f()); readln; end. ``` こんな書き方になります。 個人的な意見としては Delphi のコレはクロージャの要件を満たしていると思います。 - [クロージャ (Wikipedia)](https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3) - [無名メソッド変数のバインディング (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/Delphi_%E3%81%A7%E3%81%AE%E7%84%A1%E5%90%8D%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89#.E7.84.A1.E5.90.8D.E3.83.A1.E3.82.BD.E3.83.83.E3.83.89.E5.A4.89.E6.95.B0.E3.81.AE.E3.83.90.E3.82.A4.E3.83.B3.E3.83.87.E3.82.A3.E3.83.B3.E3.82.B0) - [Pascal(delphi)の関数内関数はクロージャなの? (lethevert is a programmer)](http://d.hatena.ne.jp/lethevert/20060121/p2) - [out パラメータ (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%EF%BC%88Delphi%EF%BC%89#out_.E3.83.91.E3.83.A9.E3.83.A1.E3.83.BC.E3.82.BF) - [入れ子関数 (Wikipedia: en)](https://en.wikipedia.org/wiki/Nested_function) - [関数内関数のススメ (Qiita)](https://qiita.com/fintopo/items/00f16303221f5b2e7192) ## Q3: 無名メソッドの再帰について 3 つ目は **Delphi の無名メソッドで再帰を書くことは可能なのか?** という疑問です。 - [無名再帰 (Wikipedia)](https://ja.wikipedia.org/wiki/%E7%84%A1%E5%90%8D%E5%86%8D%E5%B8%B0) 例えば、フィボナッチ数を求める以下のコードはコンパイルエラーになります。 ```pascal 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. ``` また、以下のような書き方だと実行時にスタックオーバーフローになります。 ```pascal program Fibonacci; {$APPTYPE CONSOLE} uses System.SysUtils, System.Math; begin var f: TFunc; f := function (n: Integer): Integer begin Result := IfThen(n < 2, n, f(n - 1) + f(n - 2)); end; Writeln(f(5)); end. ``` 無名再帰を書いた方が楽になる場面というのがあまり想像がつかないのですが、できたらできたで面白いかと思っています。 個人的な意見としては Delphi での無名再帰はできないんじゃないかと思っています。 ※ ↑ できました。コメ欄を参照の事。 # おわりに ラムダしきとかクロージャのせつめい(ていぎ)にかいてあることが、さいとによってまちまちなのでこんらんしているのです。おしえてえらいひと! そういえばむめいメソッドをそくじっこうする、こんなのは ```pascal:Project1.dpr 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:** - [Immediately invoked function expression (Wikipedia: en)](https://en.wikipedia.org/wiki/Immediately_invoked_function_expression) - [即時実行関数式 (Wikipedia)](https://ja.wikipedia.org/wiki/%E5%8D%B3%E6%99%82%E5%AE%9F%E8%A1%8C%E9%96%A2%E6%95%B0%E5%BC%8F) - [無名メソッドをその場で呼び出す。(全力わはー)](http://d.hatena.ne.jp/tales/20091026/1256570237) - [ジェネリック関数を作る。(全力わはー)](http://d.hatena.ne.jp/tales/20160812/1470932247) - [Delphi をワンライナー書けるっぽくする (Qiita: @pik)](https://qiita.com/pik/items/e40d95f63508c75f2736) - [PHP 5.3 のラムダとクロージャーを活用する (IBM)](https://www.ibm.com/developerworks/jp/opensource/library/os-php-lambda/index.html)