4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

はじめに

これは 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) 関数
実行時に値を返すルーチン 関数 関数
ルーチンに与えるもの パラメータ 引数
関数が返すもの 結果 戻り値

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

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:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
4
Help us understand the problem. What are the problem?