8. 副プログラム

8.1. 副プログラムとは

手続き型(命令形)言語において、文が集まって一つの実行単位となったものを副プログラム(subprogram)と言う。ほとんどの場合、副プログラムは次のような性質を持つ。

  • 開始点が一つだけある。

  • 副プログラムを呼び出すと、呼び出した側の実行は停止する。

  • 副プログラムの実行が終了すると、呼び出した側の実行が再開する。

  • 副プログラムの呼び出しが式として値を持つ(副プログラムが結果を返す)場合と持たない場合がある。

結果を返す副プログラムを関数(function)、返さない副プログラムを手続き(procedure または subroutine)と区別する言語もあるが、両方まとめて関数と呼ぶ言語も多い。また、オブジェクト指向言語では両方ともメソッド(method)と呼ぶのが普通である。

注釈

関数型言語では関数定義が副プログラムに相当するが、そもそも関数が基本的な実行単位なので、あえて副プログラムと言わないことが多い。手続き型言語の関数と共通する点が多いが、代入や副作用がないので、概念的にはよりシンプルである。

8.2. 実引数と仮引数

引数(parameter)を使うことによって、一つの副プログラムを複数のデータに対して適用することができる。

実引数(actual parameter)

呼び出すときに指定されるデータ。

仮引数(formal parameter)

副プログラム内で、実引数として指定されたデータを扱うために使用される変数。

  • 多くの言語では、実引数が複数ある時は、その順序によって仮引数との対応が決まる(positional parameter)。これは、引数の個数が多いと間違えやすい。それに対して、仮引数の名前と実引数の組を書く方式もある(keyword parameter)。

  • 普通は仮引数に対応する実引数が無いとエラーになるが、実引数が無い場合のデフォールト値を宣言できる言語もある。

  • 複数の実引数をまとめて1個の配列として仮引数に対応させたり、その逆ができる言語もある。

Adaの実引数と仮引数

Ada で次のような手続き宣言があるとする。

procedure Draw_Rect( X , Y : Integer;
                     Width : Integer := 10;
                     Height : Integer := 10;
                     Color : Integer := 0 )

次のような呼び出し方が可能である。

Draw_Rect( Width => 20, X => 100, Y => 50, Color => 1 )
Draw_Rect( 100, 50, Width => 20, Color => 1 )

Rubyの実引数と仮引数

実引数が配列で、仮引数がカッコで囲んだ変数の場合、要素が1個ずつ仮引数に代入される。余った要素は捨てられる。

def foo((a,b))
  p a  # 1が表示される
  p b  # 2が表示される
end

foo([1,2,3])

実引数が複数あり、仮引数がアスタリスク付きの変数の場合、実引数が1個の配列にまとめられて仮引数に代入される。

def bar(*a)
  p a  # [1, 2, 3]が表示される
end

bar(1, 2, 3)

8.3. 引数の方向

引数によってデータが渡される方向には次の二方向があり得る。

入力

呼び出す側から、呼び出される副プログラムへ

出力

呼び出された副プログラムから、呼び出した側へ

出力の場合は、実引数は代入可能でなくてはならない(典型的には変数)。

Adaにおける引数の方向

Ada では、仮引数に対してデータが渡される方向を宣言する。

procedure Foo(A : in Integer;
              B : in out Integer;
              C : out Integer) is
begin
  B := B + 1;
  C := A + B;
end Foo;

8.4. 引数の評価

実引数をどのように評価するかについて、次の三種類の方式のどれかを使うことが多い。

8.4.1. 値渡し(pass by value)

実引数を評価した値を、仮引数にコピーする。

  • 通常は入力のみに使われる。入力と出力の両方に使う場合は pass by copy-restore と呼ばれることもある。

8.4.2. 参照渡し(pass by reference)

実引数に対する参照を渡し、仮引数の変数名は、その参照が指している実体に束 縛される。つまり、仮引数は実引数の別名(3.3.2 章)になる。

  • 左辺値(4.3 章)がない式は実引数として書けない。

  • 必然的に入力と出力の両方向になる。

Pascalの値渡しと参照渡し

Pascal では、仮引数宣言に var を付けると参照渡しによる入出力、付けないと値渡しによる入力になる。次の手続きは、呼び出しても何も起こらない。

procedure swap(a, b : integer)
  temp : integer;
  begin
    temp := a;
    a := b;
    b := temp
  end;

1行目を次のように変更すると、引数の値を交換する手続きになる。

procedure swap(var a, b : integer)

8.4.3. 名前渡し(pass by name)

呼び出した時点では実引数は評価されない。仮引数が評価されると、対応する実引数が評価され、その値が仮引数の値として用いられる。なお、実引数の評価は呼び出し元の環境で行う。

  • 実引数が代入可能であれば、対応する仮引数に代入することによって出力も可能。

  • 仮引数が複数回評価される場合、毎回実引数を評価する方式と、最初の評価の結果を覚えておいて、2回目以降は評価せずに済ませる方式(前の方式と区別する時は「必要渡し(pass by need)」と呼ぶ)がある。副作用が無ければどちらでも同じ結果であるが、副作用があると結果が違ってくる可能性がある。

Scalaの名前渡し

var x = 0
def f(a: => Int) = {
    print(a)  //1が表示される
    x = 2
    print(a)  //3が表示される
}
f(x+1)

課題15

次のような(C風の構文で書いた)プログラムがある。

void main() {
  int value = 2, list[5] = {1, 3, 5, 7, 9};
  swap(value, list[0]);
  swap(list[0], list[1]);
  swap(value, list[value]);
}

void swap(int a, int b) {
  int temp;
  temp = a;
  a = b;
  b = temp;
}

次の引数受け渡しの方式を使用するとき、実行後の変数 value, list の値はそれぞれどうなるか答えよ。

  1. 値渡し(入力のみ)

  2. 参照渡し

  3. 名前渡し(毎回評価)

【解答例】

8.5. クロージャ

関数が first-class object の言語では、関数を変数に代入しておいて、後で呼び出すことができる。変数の束縛が静的スコープ(3.4 章)であれば、外側のブロックで宣言されたローカル変数の生存期間(4.5 章)を延ばし、その束縛を覚えておく必要がある。この機能をクロージャ(closure)と言う。

JavaScriptのclosure

function hoge() {
    var x = 1;
    return function() { return x += 1; };
}

var foo = hoge();
console.log(foo());  //2が表示される
console.log(foo());  //3が表示される
var bar = hoge();
console.log(bar());  //2が表示される
console.log(foo());  //4が表示される

課題16

ウェブページに10個のボタンがある時、クリックすると1〜10の番号をそれぞれ表示したいとする。しかし、JavaScriptで次のように書いてもうまくいかない。うまくいかない理由を説明せよ。

// b という配列には、ボタンのオブジェクトが10個入っているとする
for (i = 1; i <= 10; i++) {
    b[i-1].onclick = function(){ alert(i); };
}

なお、上のfor文を for (let i = 1; i <= 10; i++) とすると、期待したように動く。

【解答例】

8.6. コルーチン

普通の副プログラムは呼び出す側と呼び出される側に主従関係があるが、複数の副プログラムが対等な関係でお互いに制御を渡す(自分の実行を一時中断して、他の副プログラムの中断した箇所から再開する)場合にはコルーチン(coroutine)と呼ばれる。

Rubyの Fiber

yieldresume によって制御を受け渡す。

fib = Fiber.new {
        a,b = 1,1
        loop {
          Fiber.yield a
          a,b = b,a+b
        }
      }

5.times {
  puts fib.resume  # 1,1,2,3,5が表示される
}

注釈

コルーチンは制御の流れという点で見ると、プログラムで指定した点でしか切り替わらない(non-preemptive)並行プログラムと言ってもよい。詳しくは並行プログラミング言語のところで。