LoginSignup
4
5

<11> 手続きと関数 (標準 Pascal 範囲内での Delphi 入門)

Last updated at Posted at 2019-03-11

11. 手続きと関数

手続き関数はいわゆるサブルーチン (副プログラム) です。手続き (procedure) は実行時に値を返さないルーチンです。関数 (function) は実行時に値を返すルーチンです。

手続き・関数 =
  手続き・関数ヘッダ ";" (ブロック | 指令) ";".
手続き・関数ヘッダ =
  (手続きヘッダ | 関数ヘッダ).

Delphi だと正確には次のようになっています。

手続き・関数 =
  手続き・関数ヘッダ ";" [ブロック ";"].
手続き・関数ヘッダ =
  (手続きヘッダ | 関数ヘッダ) [";" 指令指定].
指令指定 =
  [指令] [ヒント指令].

手続き・関数はプログラムブロックの手続き・関数宣言部に書かれます。

11.1. 手続き (Procedures)

これまでは標準手続きを "使って" きましたが、この節では手続きの作り方を述べます。

手続き宣言はプログラム部分を定義してそれに名前を関連付ける働きがあります。宣言した手続きは手続き呼び出し文によって実行されます。手続きの宣言は program と同じ形をしていますが、プログラムヘッダの代わりに手続きヘッダで始まります。

手続きヘッダ =
  procedure 識別子 [仮パラメータリスト].

手続きの例は次のようになります。

ProcedureTest.pas
program ProcedureTest(output);
var
  A: Integer; {*4}
  B: Integer; {*4, *5}
  procedure Test; {*1}     
  var {*2}
    i: Integer; {*3}
    B: Integer; {*3, *5}
  begin                
    B := 0;
    for i:=1 to 10 do
      B := B + i;
    A := A + B;
  end; { Test }
begin
  A := 1;
  B := 2;
  Test; { *6 }
  Writeln(A);
  Writeln(B);
  Test; { *6 }
  Writeln(A);
  Writeln(B);
end.

実行結果は次の通りです。

56
2
111
2

上記のプログラムは簡単なものですが様々な事項が含まれています。

  1. 手続きヘッダ:
    パラメータのない手続き Test() です。
  2. ブロック:
    手続きは名前の付いたブロックです。プログラムブロックは ProcedureTest で、その中に手続きブロック Test があります。手続きのブロックにはプログラムブロックと同じように宣言部があります。
  3. ローカル変数 (局所変数):
    手続き Test() の局所的な変数は iB で、これらの変数は手続き Test() の有効範囲でしかアクセスできません。
  4. グローバル変数 (大域変数):
    変数 AB がメインプログラム (主プログラム) ProcedureTest で宣言された大域的な変数です。これらの変数はプログラムの全域で参照可能で、手続き Test() の中で A に値を代入しています。
  5. 有効範囲 (スコープ):
    変数 i はグローバル変数にもローカル変数にもありますが、それらは同じ変数ではありません。
  6. 手続き呼び出し文:
    メインプログラムが手続き **Test()**を 2 回実行しています。このようにプログラム部分を手続きにすると、同じ処理を何度も書かずに済みます。

手続き呼び出し文の構文は次の通りです。

手続き呼び出し文 =
  手続き名 (実パラメータリスト | 書き出しパラメータリスト).

書き出しパラメータリストWrite() および Writeln() のための特殊な構文です (12.3. 節)。

See also:

11.1.1. パラメータリスト (パラメータ並び / Parameter list)

先のプログラムは手続き内でグローバル変数を直接読み書きしており、計算の回数も変更できなかったので、パラメータを持つように変更してみました。

ProcedureTest2.pas
program ProcedureTest2(output);
var
  A: Integer;
  B: Integer;
  procedure Test(L: Integer; var V: Integer); { *1, *2, *4 }
  var
    i: Integer;
    B: Integer;
  begin
    B := 0;
    for i:=1 to L do
      B := B + i;
    V := V + B;
  end; { Test }
begin
  A := 1;
  B := 2;
  Test(10, A); { *3 }
  Writeln(A);
  Writeln(B);
  Test(20, A); { *3 }
  Writeln(A);
  Writeln(B);
end.
  1. 手続きヘッダ:
    パラメータリストを持つ形式になっています。

  2. 仮パラメータ (仮引数 / パラメータ / parameter / formal-parameter):
    パラメータリストには仮パラメータの名前とその型を書きます。手続き Test() は仮パラメータとして LV を持っています。
    image.png

  3. 実パラメータ (実引数 / 引数 / argument / actual-parameter):
    手続き呼び出し文には実パラメータのパラメータリストを含んでいます。実パラメータと仮パラメータは同じ並びである必要があります。渡せるパラメータには値パラメータ変数パラメータ手続きパラメータ関数パラメータ の 4 種類があります。Delphi にはこの他にも 定数パラメータout パラメータ等があります。
    image.png

  4. 値パラメータ (Value parameters) と変数パラメータ (Variable parameters):
    パラメータ L は値パラメータであるので、式 (変数を含む) を実パラメータとして渡せます。予約語 var が付加されたパラメータ V変数パラメータであり、手続き内から値を変更する事ができます。

仮パラメータリストの定義は次の通りです。各仮パラメータはセミコロン ; で区切ります。他言語の関数プロトタイプ宣言では , を使うので、最初は戸惑うかもしれません。

仮パラメータリスト =
  "(" 仮パラメータ {";" 仮パラメータ} ")".
仮パラメータ = 
  [var | CONST | OUT] パラメータ.
パラメータ = 
  識別子 {"," 識別子} [':' 型名].

同じ型を持つ仮パラメータはカンマで区切って指定する事ができます。例えば次の手続きヘッダは、

procedure TEST(a: Integer; b: Integer);

このように短く書く事ができます。変数宣言部と似た記述が可能です。

procedure TEST(a, b: Integer);

実パラメータリストの定義は次の通りです。各実パラメータはカンマ , で区切ります。

実パラメータリスト =
  "(" 実パラメータ {"," 実パラメータ} ")".
実パラメータ = 
  { [変数 | 式] }.

パラメータのない手続きの呼び出しでは ( ) を省略できます 1

DoSomething();
DoSomething;

パラメータの種類は次の通りです。

パラメータの種類 予約語 実パラメータ
(呼び出し時)
入力 出力
値パラメータ
(値渡し or 参照渡し2)
×
変数パラメータ
(参照渡し)
var 変数
定数パラメータ
(値渡し or 参照渡し2)
const ×
定数パラメータ
(参照渡し)
[Ref] const ×
out パラメータ
(参照渡し)
out 変数 ×

値パラメータ (予約語での修飾なし) は、手続きまたは関数の呼び出し時に渡す値に初期化されるローカル変数のような働きをします (値パラメータに代入可能です)。

ParamTest.pas
program ParamTest(output);
var
  i: Integer;
  procedure Proc(v: Integer);
  begin
    v := v * 2; { ローカル変数のように使える }
    writeln(v);
  end;
begin
  i := 3;
  Writeln(i); { 3 }
  proc(i);    { 6 } 
  Writeln(i); { 3 }
end.

Pascal の値パラメータは値渡しですが、Delphi の値パラメータは値渡しとは限りません。

変数パラメータ (var) は参照渡し (変数渡し) を強制しますが、定数パラメータ (const) は値渡しを強制するものではありません。パラメータへの代入 (出力) ができない事を強制します。定数パラメータに代入しようとするとコンパイルエラーになります。

Delphi XE3 以降では、const[Ref] 属性を付けて定数パラメータに参照渡しを強制させる事ができます。属性の指定は const [Ref][Ref] const どちらでも構いません。

out パラメータはパラメータへの代入 (出力) のみを許可します。渡された時に実パラメータは初期化されてしまうので実パラメータの値を参照 (入力) する事はできません。out パラメータは COM (Component Object Model) 以外の用途で使われることはあまりありません。

多くのケースで out パラメータは var パラメータと同等の動作 (入力が可能) になるため var の使用が推奨されています。もちろん COM (あるいは Delphi for .NET) で使用する場合においてはこの限りではありません。

ParamTest.pas
program ParamTest;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

var
  lv1, lv2, lv3, lv4, lv5: Integer;

procedure Test(            v1: Integer;
               var         v2: Integer;
               const       v3: Integer;
               [Ref] const v4: Integer;
               out         v5: Integer);
begin
  Writeln(Format('v1: %.8x : %.8x            ', [@lv1, @v1]));
  Writeln(Format('v2: %.8x : %.8x var        ', [@lv2, @v2]));
  Writeln(Format('v3: %.8x : %.8x const      ', [@lv3, @v3]));
  Writeln(Format('v4: %.8x : %.8x [Ref] const', [@lv4, @v4]));
  Writeln(Format('v5: %.8x : %.8x out        ', [@lv5, @v5]));
end;

begin
  Test(lv1, lv2, lv3, lv4, lv5);
end.

実行結果は次のようになります。
image.png
※サンプルコードを簡潔にするために外部関数 Format() を使っています。

See also:

11.1.2. 整合配列パラメータ (Conformant array parameters)

配列型のパラメータをとるルーチンを宣言するとき、そのパラメータ宣言でインデックスの型を指定することはできません。次の宣言はエラーになります。

procedure DoSomething(A: array [1..10] of Integer); 

type arr10 = array [1..10] of Integer; のように、あらかじめ配列型を定義しておけば、パラメータに指定することが可能になります。

procedure DoSomething(A: arr10); 

それとは別の問題として、ここまでに紹介した文法では 任意の長さの配列をパラメータにとるルーチン を作る事もできません。

上記の問題を解決するための仕組みが整合配列パラメータなのですが、Delphi ではこのパラメータ形式をサポートしていません。標準 Pascal においてもオプションです。

整合配列形式 = 
  パック状整合配列形式 | アンパック状整合配列形式.
パック状整合配列形式 =
  packed array "[" 添字型仕様 "]" of 型名. 
アンパック状整合配列形式 =
  array "[" 添字型仕様 { ";" 添字型仕様} "]" of (型名 | 整合配列形式).
添字型仕様 =
  識別子 ".." 識別子 ":" 順序型名

次の例は任意の長さの文字列 (文字配列) を受け取り、シーザー暗号で出力します。

program CaesarCipher(output);

type
  Positive = 1..MaxInt;

var
  Str1: packed array [1..3] of Char;
  Str2: packed array [1..12] of Char;
  
procedure CCStr(Str: packed array [M..N: Positive] of Char);
var
  i: Integer;
begin
  for i := M to N do
    Write(Chr(Ord(Str[i]) - 1));
  Writeln;  
end;

begin
  Str1 := 'IBM';
  CCStr(Str1);
  Str2 := 'Hello,World.';
  CCStr(Str2);
end.

実行結果は次のようになります (ideone.com で試せます)。

HAL
Gdkkn+Vnqkc-

標準 Pascal の規格で定義されている言語機能をすべて対応した上で、この整合配列パラメータに対応すれば 標準 Pascal 水準 1 (Standard Pascal Level 1)、対応しなければ 標準 Pascal 水準 0 (Standard Pascal Level 0) となります。

11.1.3. 再帰手続き (Recursive procedures)

ルーチン内でそのルーチン自身を呼び出すことを再帰呼出し (Recursive call)と言い、それを行う手続きを再帰手続きと呼びます。関数なら再帰関数です。

次のコードは階乗を求める再帰手続きです。

RecursiveProcedureTest.pas
program RecursiveProcedureTest(output);
var
  f: Integer;
  procedure fact(n: Integer; var v: Integer);
  begin
    if (n > 0) then
      begin
        v := v * n;
        fact(n - 1, v);
      end;  
  end;    
begin
  f := 1;
  fact(5, f);
  Writeln(f);
end.

11.1.4. 手続きパラメータ (Procedural parameters)

Delphi ではこのパラメータ形式をサポートしていません。また、標準 Pascal では標準手続き を実手続きパラメータとして渡せません。

パラメータの種類 予約語 仮パラメータ
(宣言時)
実パラメータ
(呼び出し時)
手続きパラメータ 手続きヘッダ 手続き
ProcParamTest.pas
program ProcParamTest(output);
var
  A: Integer;
  procedure Proc1(V: integer);
  begin
    A := A + (V * 3);
  end; { Proc1 }
  procedure Proc2(V: integer);
  begin
    A := A - (V * 5);
  end; { Proc2 }
  procedure Test(procedure Proc(V: integer));
  begin
    Proc(10);
  end; { Test }
begin
  A := 0;

  Test(Proc1);
  Writeln(A);

  Test(Proc2);
  Writeln(A);
end.

実行結果は次の通りです。

 30
-20

(11.1.5.) オープン配列パラメータ (Open Array Parameters)

Delphi では、配列パラメータを実現するための別の解としてオープン配列パラメータが実装されています。

オープン配列パラメータを使用すると、基底型が同じでサイズ (要素数) だけが異なる配列をルーチンに渡す事ができます。

procedure DoSomething(A: array of Integer);

オープン配列パラメータの構文は動的配列の構文に似ていますが同じではありません。実パラメータとして動的配列だけでなく静的配列も渡せます。ルーチンの中では次のようにして渡された配列を処理できます。

procedure Clear(var A: array of Real);
var
   I: Integer;
begin
  for I := 0 to High(A) do 
    A[I] := 0;
end;

パラメータに配列そのものを渡しているわけではないため、SetLength() で動的配列の要素数を変更したりする事はできず、

procedure Clear(var A: array of Real);
var
  I: Integer;
begin
  SetLength(A, 10);        // 変更できない
  for I := 0 to High(A) do
    A[I] := 0;
end;

var
  ra: array of Real;

begin
  SetLength(ra, 5); // 配列の要素数を 5 に
  Clear(ra);
end.

二次元のオープン配列パラメータなんていうものもありません。

procedure Clear(var A: array of array of Real); // NG

オープン配列パラメータを使ったルーチンには、オープン配列コンストラクタも渡すことができます。

See also:

(11.1.6.) 型可変オープン配列パラメータ (Variant Open Array Parameters)

型可変オープン配列パラメータ (型なしの配列パラメータ) を使用すると、基底型すら異なる配列を渡す事ができます。

procedure DoSomething(A: array of const);

array of constarray of TVarRec と同じなので、TVarRec.VType を使って型ごとに処理を分岐させる事ができます。

function MakeStr(const Args: array of const): string;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to High(Args) do
     with Args[I] do
        case VType of
          vtInteger:        Result := Result + IntToStr(VInteger);
          vtBoolean:        Result := Result + BoolToStr(VBoolean);
          vtChar:           Result := Result + VChar;
          vtExtended:       Result := Result + FloatToStr(VExtended^);
          vtString:         Result := Result + VString^;
          vtPChar:          Result := Result + VPChar;
          vtObject:         Result := Result + VObject.ClassName;
          vtClass:          Result := Result + VClass.ClassName;
          vtAnsiString:     Result := Result + string(VAnsiString);
          vtUnicodeString:  Result := Result + string(VUnicodeString);
          vtCurrency:       Result := Result + CurrToStr(VCurrency^);
          vtVariant:        Result := Result + string(VVariant^);
          vtInt64:          Result := Result + IntToStr(VInt64^);
        end;
end;

型可変オープン配列パラメータにもオープン配列コンストラクタを渡すことができます。

See also:

(11.1.7.) オープン配列コンストラクタ (Open Array Constructors)

オープン配列コンストラクタにより、関数および手続き呼び出し内で、直接、配列を構築することができます。それらは、オープン配列パラメータまたは、型可変オープン配列パラメータとしてのみ渡すことができます。

例えば次のような定義 (オープン配列パラメータ) があった場合、

procedure Add(A: array of Integer);

オープン配列コンストラクタを使って Add() 手続きを次の文で呼び出すことができます:

Add([5, 7, I, I + J]);

次のような定義 (型可変オープン配列パラメータ) なら、

procedure Add(A: array of const);

オープン配列コンストラクタを使って Add() 手続きを次の文で呼び出すことができます:

Add([5, 7, I, I + J]);
Add([100, 'A', 3.14]);

オープン配列コンストラクタを頻繁に使う最も有名なルーチンは System.SysUtils にある Format() 関数です。

  uses
    ..., System.SysUtils;

  s := Format('%s: %.4d', ['Hello', 123]);

See also:

(11.1.8.) デフォルトパラメータ (Default Parameters)

手続きや関数のヘッダー (仮パラメータ) で、デフォルトパラメータ値を指定できます。デフォルト値を指定できるのは、値パラメータと定数パラメータだけです。

たとえば、次のような宣言は

procedure FillArray(A: array of Integer; Value: Integer = 0);

次のいずれの呼び出しでも結果は同じになります。

FillArray(MyArray);
FillArray(MyArray, 0);

複数のパラメータを一度に宣言する場合には、デフォルト値は指定できません。 従って、次の宣言は無効です。

function MyFunction(X, Y: Real = 3.5): Real;  // syntax error

デフォルト値を指定するパラメータは、パラメータ リストの末尾に置かなければなりません。 つまり、あるパラメータにデフォルト値を指定したら、それ以降のパラメータにはすべてデフォルト値を指定する必要があります。 従って、次の宣言は無効です。

procedure MyProcedure(I: Integer = 1; S: string);  // syntax error

See also:

(11.1.9.) ルーチンのオーバーロード (Overloading Routines)

同じスコープ内で同じ名前のルーチンを複数宣言することができます。これは、オーバーロードと呼ばれます。オーバーロードされるルーチンは、overload 指令を付けて宣言する必要があり、パラメータ リストで区別できなければなりません。

 function Divide(X, Y: Real): Real; overload;
 begin
   Result := X / Y;
 end

 function Divide(X, Y: Integer): Integer; overload;
 begin
   Result := X div Y;
 end;

オーバーロードされたルーチンは、受け取るパラメータの数またはそれらのパラメータの型で区別できなければなりません。

See also:

(11.1.10.) オープン文字列パラメータ (Open String Parameters)

Delphi のオープン文字列パラメータは、ANSI 版 Delphi かつ String が短い文字列 (ShortString) である ({$H-}) 場合にのみ意味を持ちます。

次に示すコードのパラメータ S の string はString 型を示しているのではなく、オープン配列パラメータの文字列版である オープン文字列パラメータ を示しています。長さの異なる文字列変数をルーチンのパラメータとして渡す事ができます。

program OpenStringParameterTest(Output);
{$APPTYPE Console}

var
  S1: string[10];
  S2: string[20];

  procedure AssignStr(var S: string);
  begin
    S := '0123456789ABCDEF';
  end; { AssignStr }

begin
  AssignStr(S1); { '0123456789' }
  AssignStr(S2); { '0123456789ABCDEF' }
end.

コンパイラ指令 {$P} はパラメータの string をオープン文字列パラメータとみなすかどうかのスイッチです。デフォルトで{$P+} ですが、{$P-} だと string パラメータはただの変数パラメータとみなされます。

Delphi 1 では上記コードのコンパイルが通ったのですが、Delphi 2 以降では、コンパイルエラーになりました。S1 と S2 が 短い文字列 (ShortString) であるのに対し、S はオープン文字列パラメータではなく長い文字列 (string) の変数パラメータだからです。

この問題を解決するためには 2 つの方法があります。一つはコンパイラ指令 {$H-} を使い、string 型を短い文字列 (ShortString) とみなすようにする方法です。影響範囲が広いですが、大量のコードを移植しなければならない時には有効でしょう。

もう一つは OpenString 識別子を使う方法です。OpenString は正確には型ではなく、オープン文字列パラメータを示す識別子で、次のコードでは、パラメータ S はオープン文字列パラメータとして認識されます (型としては ShortString)。

  procedure AssignStr(var S: OpenString);
  begin
    S := '0123456789ABCDEF';
  end; { AssignStr }

”String” という識別子を使っていないため、コンパイラ指令 {$P} の影響も受けません。

コンパイラ指令 {$V} というのもあります。このオプションは本質的に "安全でないバージョンのオープン文字列パラメータ" を提供します。{$P+} が指定されている場合には、string パラメータはオープン文字列パラメータですので、{$V} の効果はありません。

program OpenStringParameterTest2(Output);
{$APPTYPE Console}

  procedure MyProc(var s: string);
  begin
    s := 'abcdefghijk';
  end; { MyProc }

var
  ss: string[5];
begin
  MyProc(ss);
end.

上記コードと {$P} および {$V} コンパイラ指令の関係は次の通りです。

{$P} {$V} 説明
{$P+} {$V+} or {$V-} オープン文字列パラメータ。
S への割り当てが実パラメータで宣言された
サイズを超えないことを保証する。
{$P-} {$V+} MyProc(ss) は型不一致の
コンパイルエラーを生成する。
{$P-} {$V-} MyProc はコンパイラエラーを生成しないが、
プログラム内で上書きエラーになり、
システムがクラッシュする可能性がある。

String が短い文字列 だったのは Delphi 1 とそれ以前の Turbo Pascal ですから、これらのコンパイラ指令はもはや気にする必要がないと思います。

むしろ Unicode 版 Delphi になってずいぶん経ちますが、未だにコンパイラ指令 {$H}{$P}{$V} がプロジェクトオプションに存在するのがとても不思議です。

See also:

(11.1.11.) Exit() 手続き

Delphi では Exit() 手続きを使って現在のルーチンから抜ける事ができます。メインプログラムで実行するとプログラムが終了します。

procedure foo(a: Integer);
begin
  if (a < 0) and (a > 100) then
    Exit;
  ...
end;

See also:

(11.1.12.) Halt() 手続き

Delphi では Halt() 手続きを使ってプログラムを 異常終了 させる事ができます。パラメータとして終了コードを渡す事ができます。正常終了する手順は次の通りです。

  • VCL アプリケーションの場合:
    Application.Terminate() を呼び出します。
  • FMX アプリケーションの場合:
    Application.Terminate() を呼び出します。
  • コンソールアプリケーションの場合:
    メインプログラムで Exit() 手続きを呼び出します。

See also:

11.2. 関数 (Functions)

関数宣言はプログラム部分を定義してそれに名前を関連付ける働きがあります。宣言した関数は関数呼び出しによって実行されます。関数宣言は program とほぼ同じ形をしていますが、プログラムヘッダの代わりに関数ヘッダで始まります。

関数ヘッダ =
  functon 識別子 [仮パラメータリスト] ":" 型名.

ほぼ手続きと同じですが、関数は値を返す (結果 / 戻り値) ため、関数呼び出しは、代入や演算子内の式として使用できます。

結果型 (Result Type)

結果の型 (結果型) は単純型またはポインタ型の識別子である必要があります。標準 Pascal では結果型に構造化型は使えません。Delphi だと一旦型を定義すれば結果型に構造化型が使えます。

ResultTest.pas
program ResultTest;

type
  Str10 = array [1..10] of Char;
  TIDPass =
    record
      ID: Integer;
      Pass: Str10;
    end;

// NG
function Sub1: array [1..10] of Char;
begin
  ...
end;

// OK
function Sub1: Str10;
begin
  ...
end;

// NG
function Sub2: record ID: Integer; Pass: Str10 end;
begin
  ...
end;

// OK
function Sub2: TIDPass;
begin
  ...
end;

begin

end.

結果 (Result)

Pascal では関数が返す値を 結果 (result) と呼びます。他の言語では 戻り値 (return value) と呼ばれる事が多いですが、当記事では由緒正しく結果と表記しています。

関数で結果を返すには、標準 Pascal だと関数名に対して値を代入します。関数名は、その関数の結果を保持する特殊な変数として使用できます。

function Add(A, B: integer): Integer;
begin
  Add := A + B;
end;

Delphi では拡張構文 {$X} がデフォルトで有効 ({$X+}) になっているので、関数ブロックで定義済みの変数 Result を使って、関数の結果を保持することができます。Result 変数のスコープは関数ブロック内です。

function Add(A, B: integer): Integer;
begin
  Result := A + B;
end;

Result 変数を使うメリットは主に 2 つあります。

  1. 関数名の変更 (リファクタリング) が容易になる。
  2. 結果への代入と再帰関数を混同しなくなる。

Delphi 2009 以降では Exit() 手続きで、関数の結果をパラメータで受け取れるようになりました。C 言語の return 文のような記述が可能になっています。

function Add(A, B: integer): Integer;
begin
  Exit(A + B);
end;

See also:

関数呼び出し (Function Call)

4.2. 節 で説明した通り、標準 Pascal は結果が不要でも結果を利用しない関数は呼び出せません。つまり、標準 Pascal での関数は必ず式の中で使われるため、関数呼び出し文というのはありませんでした。Delphi では結果を破棄できるため、関数呼び出し文が成立します。

また、パラメータのない関数の呼び出しでは ( ) を省略できます 1

v := DoSomething();
v := DoSomething;

See also:

再帰関数 (Recursive functions)

次のコードは階乗を求める再帰関数です。

RecursiveFunctionTest.pas
program RecursiveFunctionTest(output);
var
  f: Integer;
  function fact(n: Integer): Integer;
  begin
    if (n = 0) then
      fact := 1
    else
      fact := 
      fact(n - 1) * n;
  end;    
begin
  f := fact(5);
  Writeln(f);
end.

関数名が代入文の左辺にある場合、コンパイラはそれを結果を記録するために使われているものと見なします。関数名がステートメントブロックのその他の場所にある場合、コンパイラはそれをその関数の再帰呼び出しと解釈します。

結果を返すのに Result 変数を使うと再帰呼び出しとの判別が容易になります。

RecursiveFunctionTest.pas
program RecursiveFunctionTest(output);
var
  f: Integer;
  function fact(n: Integer): Integer;
  begin
    if (n = 0) then
      Result := 1
    else
      Result := 
      fact(n - 1) * n;
  end;    
begin
  f := fact(5);
  Writeln(f);
end.

11.2.1. 関数パラメータ (Functional parameters)

手続きパラメータと同様、Delphi ではこのパラメータ形式をサポートしていません。標準 Pascal では標準関数 を実関数パラメータとして渡せません。

パラメータの種類 予約語 仮パラメータ
(宣言時)
実パラメータ
(呼び出し時)
関数パラメータ 関数ヘッダ 関数

次に挙げるコードは『J&W (初版・第 2 版)』にある関数パラメータの例です。

関数 zero() の第 1 パラメータには、function sin(x: Real): Real; のような関数が渡される事が想定されていますが、古い Pascal では関数パラメータは結果の型さえ合致していればよく、渡す関数のパラメータを記述する必要はありませんでした。

bisect.pas
{ program 11.6
  find zero of a function by bisection }
program bisect(input, output);

const eps = 1E-14;
var x, y: real;

function zero(function f: real; a, b: real): real;
var x, z: real; s: boolean;
begin
  s := f(a) < 0;
  repeat
    x := (a + b) / 2.0;
    z := f(x);
    if (z < 0) = s then a := x else b := x;
  until abs(a - b) < eps;
  zero := x;
end; { zero }

begin
  read(x, y); writeln(x, y, zero(sin, x, y));
  read(x, y); writeln(x, y, zero(cos, x, y));
end. { main }

標準 Pascal では関数パラメータを関数ヘッダと同じ書式で書く必要があります。ただし、Pascal-P5 では次のコードはコンパイル可能ですが実行時エラーが発生します。

bisect.pas
{ program 11.6
  find zero of a function by bisection }
program bisect(input, output);

const eps = 1E-14;
var x, y: real;

function zero(function f(v: real): real; a, b: real): real;
var x, z: real; s: boolean;
begin
  s := f(a) < 0;
  repeat
    x := (a + b) / 2.0; z := f(x);
    if (z < 0) = s then a := x else b := x;
  until abs(a - b) < eps;
  zero := x;
end; { zero }

begin
  read(x, y); writeln(x, y, zero(sin, x, y));
  read(x, y); writeln(x, y, zero(cos, x, y));
end. { main }

何故実行時エラーが発生するかというと、sin()cos()標準関数だからです。標準 Pascal では標準関数を関数パラメータとして渡せなくなっています 3。次のように標準関数をラッピングしてやれば実行時エラーは発生しません。

bisect.pas
{ program 11.6
  find zero of a function by bisection }
program bisect(input, output);

const eps = 1E-14;
var x, y: real;

function sin2(v: real): real;
begin
  sin2 := sin(v);
end; { sin2 }

function cos2(v: real): real;
begin
  cos2 := cos(v);
end; { cos2 }

function zero(function f(v: real): real; a, b: real): real;
var x, z: real; s: boolean;
begin
  s := f(a) < 0;
  repeat
    x := (a + b) / 2.0; z := f(x);
    if (z < 0) = s then a := x else b := x;
  until abs(a - b) < eps;
  zero := x;
end; { zero }

begin
  read(x, y); writeln(x, y, zero(sin2, x, y));
  read(x, y); writeln(x, y, zero(cos2, x, y));
end. { main }

詳細は後述しますが、Delphi では手続き型 (Procedural Types) を用いて次のように書く事ができます。パラメータの型や結果型を厳密に合わせる必要があるため、Real 型を Extended 型に変更しています。Delphi の sin()function Sin(const X: Extended): Extended; と定義されています。

bisect.dpr
{ program 11.6
  find zero of a function by bisection }
program bisect(input, output);

type Tf = function (const v: Extended): Extended;
const eps = 1E-14;
var x, y: Extended;

function zero(f: Tf; a, b: Extended): Extended;
var x, z: Extended; s: boolean;
begin
  s := f(a) < 0;
  repeat
    x := (a + b) / 2.0; z := f(x);
    if (z < 0) = s then a := x else b := x;
  until abs(a - b) < eps;
  result := x;
end; { zero }

begin
  read(x, y); writeln(x, y, zero(sin, x, y));
  read(x, y); writeln(x, y, zero(cos, x, y));
end. { main }

このプログラムを実行して次のように入力すると、

-1 1 1 2〔Enter〕 

このような結果が得られます。

-1.00000000000000E+0000  1.00000000000000E+0000  -7.10542735760100E-0015
 1.00000000000000E+0000  2.00000000000000E+0000   1.57079632679490E+0000

See also:

11.2.2. 副作用 (Side effect)

『J&W』に掲載されていた、手続きや関数内からグローバル変数を操作すると思わぬ結果をもたらす、というコードのサンプルです。結果を予想するのがとても難しいです。

SideEffect.pas
program SideEffect(Output);

   { プログラム 11.10 - 関数の副作用の例 }

  var
    A, Z: Integer;

  function Sneaky(X: Integer): Integer;
  begin
    Z := Z - X { Z への副作用 };
    Sneaky := Sqr(X);
  end; { Sneaky }
  
begin
  Z := 10; A := Sneaky(Z);
  Writeln(Output, A, Z);
  Z := 10; A := Sneaky(10); A := A * Sneaky(Z);
  Writeln(Output, A, Z);
  Z := 10; A := Sneaky(Z); A := A * Sneaky(10);
  Writeln(Output, A, Z);
end. { SideEffect }

上記コードの実行結果は次の通りです。

        100          0
          0          0
      10000        -10

Pascal においての 副作用 とは、手続き / 関数内で定義されていない変数または変数パラメータへの代入の事を指します。

11.3. 前方宣言 (Forward declaration)

手続きや関数は前方宣言があれば、その手続きや関数の宣言よりも前で使用できます。

  procedure A(v: Integer);
  begin
    B(1); { 呼び出せない }
  end;

  procedure B(v: Integer);
  begin

  end;

上記の問題は forward 指令を使って前方宣言する事によって回避できます。

  procedure B(v: Integer); forward;

  procedure A(v: Integer);
  begin
    B(1); { 呼び出せる }
  end;

  procedure B; { 仮パラメータと結果型はここに書かない }
  begin

  end;

Delphi の場合、前方宣言はプログラムファイル (*.dpr) 内だけで使えます。

forward 指令は、ユニットの interface セクションでは無効です。interface セクション内の手続きと関数のヘッダーは、forward 宣言と同様の動作をするので、implementation セクションで定義宣言をする必要があります。

Unit1.pas
unit Unit1;

interface

// 関数のヘッダー宣言
procedure A(v: Integer);
procedure B(v: Integer);

implementation

procedure A(v: Integer);
begin
  B(1); { 呼び出せる }
end;

procedure B(v: Integer);
begin

end;

end.

See also:

(11.4.) 手続き型 (Procedural Types)

手続き型は手続きまたは関数 (ルーチン) を代入できる型です。パラメータと結果の型が同じルーチンを代入できます。他言語では関数ポインタ (Function Pointer) とも呼ばれる機能です。

例えば、System.SysUtils では汎用的な手続き型 TProcedure が宣言されています。

System.SysUtils.pas
{ Generic procedure pointer }

  TProcedure = procedure;

TProcedure 型の変数にはパラメータのない手続きを代入できます。

ProcTest.dpr
program Project1;

{$APPTYPE CONSOLE}

procedure A;
begin
  Write('A');
end;

procedure B;
begin
  Write('B');
end;

procedure C;
begin
  Write('C');
end;

type
  TProcedure = procedure; // パラメータのない手続き

var
  i: Integer;
  Procs: array [0..2] of TProcedure;
begin
  Procs[0] := A;
  Procs[1] := B;
  Procs[2] := C;
  for i:=Low(Procs) to High(Procs) do
    Procs[i];
  Writeln;
end.

Delphi には手続き型に類似する型として

  • メソッドポインタ型 (of object)
  • メソッド参照型 (reference to)

がありますが、どちらもクラスに関連するものなので割愛します。

See also:

(11.4.1.) 手続き型定数とグローバル手続き型変数の初期化

Delphi で手続き型定数を宣言するには、定数の宣言される手続き型と互換性のあるルーチンの名前を指定します。

ProceduralConstantsTest1.pas
program ProceduralConstantsTest1;

{$APPTYPE CONSOLE}

// 手続き型の定義
type
  TFunction = function(X, Y: Integer): Integer;

var
  i: Integer;

  function Calc(X, Y: Integer): Integer;
  begin

  end; { Calc }


// グローバル手続き型定数
const
  MyFunction1: TFunction = Calc;

// グローバル手続き型変数と初期化
var
  MyFunction2: TFunction = Calc;

  procedure Sub;
//var
//  ローカル手続き型変数は初期化できない
//  MyFunction3: TFunction = Calc;
  begin

  end; { Sub }

begin
  i := MyFunction1(5, 7);
  i := MyFunction2(5, 7);
end.

手続き型を定義せずに直接宣言する事もできます。

ProceduralConstantsTest2.pas
program ProceduralConstantsTest2;

{$APPTYPE CONSOLE}

var
  i: Integer;

  function Calc(X, Y: Integer): Integer;
  begin

  end; { Calc }

const
  MyFunction1: function(X, Y: Integer): Integer = Calc;

// グローバル手続き変数と初期化
var
  MyFunction2: function(X, Y: Integer): Integer = Calc;

  procedure Sub;
//var
//  ローカル手続き変数は初期化できない
//  MyFunction3: function(X, Y: Integer): Integer = Calc;
  begin

  end; { Sub }

begin
  i := MyFunction1(5, 7);
  i := MyFunction2(5, 7);
end.

手続き型変数を使うと次のような事ができます。

ProceduralConstantsTest2
program ProceduralTypesFizzBuzz;

{$APPTYPE CONSOLE}

  procedure FizzBuzz(X: Integer);
  begin
    Writeln('FizzBuzz');
  end; { FizzBuzz }

  procedure Fizz(X: Integer);
  begin
    Writeln('Fizz');
  end; { Fizz }

  procedure Buzz(X: Integer);
  begin
    Writeln('Buzz');
  end; { Buzz }

  procedure Num(X: Integer);
  begin
    Writeln(X);
  end; { Num }

var
  i: Integer;
  FB: procedure(X: Integer);
begin
  for i:=1 to 100 do
    begin
      if ((i mod 3) + (i mod 5)) = 0 then
        FB := FizzBuzz
      else if (i mod 3) = 0 then
        FB := Fizz
      else if (i mod 5) = 0 then
        FB := Buzz
      else
        FB := Num;
      FB(i);
    end;
end.

See also:

索引

[ ← 10. ポインタ型 ] [ ↑ 目次へ ] [ → 12. テキストファイルの入出力 ] :sushi:

  1. Pascal が手続きと関数を明確に区別しているので可能となっています。 2

  2. パラメータの型とサイズに応じて値渡しまたは参照渡しになります。複雑なのでヘルプで確認してください。基本的には型のサイズが SizeOf(Pointer) よりも大きければ参照渡しとなります。 2

  3. このコード例は『J&W』第 3 版以降、別のものに差し替えられています。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5