| Top Page | プログラミング | Perl 目次 | prev | next | 索引 |

16. サブルーチン:長いプログラムを読みやすく,管理しやすく

サブルーチンのご利益

プログラムを書き慣れてくると,だんたんいろんなことをさせたくなります. 最初は十数行からせいぜい数十行のプログラムを書いていたのが, いつのまにやら 200 行,300 行のプログラムを書くようになります.

手をかけたプログラムは,あとあと再利用することもあるでしょう. また,シミュレーション用のプログラムであればその動作を変えたり 新しいプロセスを付け加えたりしたくなるでしょうし, データ解析プログラムであれば解析のしかたを少し変えたくなることがよくあります.

けれども,長いプログラムはあとで読み返すのが大変だし,動作を変更しようと 思ってもどこをどう書きなおせばいいのかなかなか見えてこない, バグも見つけにくい,ということになりがちです.

長いプログラムを読みやすく・管理しやすくするのには, サブルーチンが欠かせません.サブルーチンは自分で作れる関数のようなものです. サブルーチンを使うと構造の見通しもよくなり,デバッグも改変もやりやすくなります. このページではサブルーチンの使い方の基本を説明します. 使いやすいサブルーチンを書くうえで重要な「変数の局所化」については 次のページで説明します.

サブルーチンの書き方

サブルーチンは, sub サブルーチン名 {...} という構文で定義します. {} で囲まれたブロックのなかに,そのサブルーチンにさせたい処理を書きます. それらの処理は,サブルーチンが呼ばれたときだけ実行されます.

# メインのプログラム

$fortune = &tell_fortune();        #  サブルーチンを呼んできょうの運勢を取得
print "Today's luck: $fortune\n";  #  運勢を表示する.

if ( ($fortune eq "凶") ||  ($fortune eq "大凶")) {  #  凶か大凶だったら
    $fortune = &tell_fortune();                      #  もういちど運勢を取得
    print "Today's luck (second trial): $fortune\n"; #  運勢を表示.
    if ( ($fortune eq "凶") ||  ($fortune eq "大凶")) {  #  また凶系だったら
        print "きょうはダメダメ\n";                  # ダメダメメッセージを表示
    }
}

# きょうの運勢を返すサブルーチンの定義.

sub tell_fortune
{
    $x = rand();
    if ($x < 0.2) {
        $return_val = "大吉";
    }
    elsif ($x < 0.7) {
        $return_val = "吉";
    }
    elsif ($x < 0.85) {
        $return_val = "凶";
    }
    else {
        $return_val = "大凶";
    }
    return $return_val;
}

上の例では,メインのプログラム(サブルーチンじゃないところをこう呼ぶ ことにしましょう)に続いてサブルーチンが書かれていますが, この順番に実行されるのではありません. サブルーチンは,呼ばれない限り実行されません (関数と同様に,サブルーチンは「呼ぶ」ものです).

サブルーチンを呼ぶときは,サブルーチン名の前に & をつけます (じつは & を省略してよい場合もありますが,とりあえずは かならずつけることにしておきましょう).

サブルーチンは,呼び出した側にスカラー値やリストを返すことができます. サブルーチン中で一番最後に評価された値が返されますが, return 文があると,return のうしろに書かれた値を返します. 値を返すサブルーチンはなるべくreturn 文を書くことにしておくと, あとで読みやすいでしょう. 上の例ではスカラー値ひとつを返しています.

サブルーチンを使うと,同じことをプログラムのあちこちに 何度も書かなくてよいというメリットがあります. 上の例では,おみくじ引きのサブルーチンをメインのプログラムの2ケ所から 呼んでますが,サブルーチンを使わないとすると,サブルーチン内の 10 数行分を 2回繰り返して書くことになります.これは,手間が増える,全体が長くなる, なにか変更したいときに2ケ所を書きなおす必要がある(で,たぶん ときに片一方をなおし忘れる)というデメリットがあります.

サブルーチンに引数をわたす

サブルーチンには,関数と同様に引数(ひきすう)を渡して処理を依頼することが できます.サブルーチン名のうしろのカッコの中に引数を並べます(これまた, じつはカッコを省略してもよい場合もありますが,とりあえずはかならず つけときましょう. 上の例では,引数がなにもないので,サブルーチンのうしろに中身のない () だけを書いてます).

呼び出し側が渡した引数は, サブルーチン中では @_ という特別な名前の配列にしまわれています. 配列の扱い方 はだいぶ前に解説しました.@_ もふつうの配列と同じに扱えます. たいてい,@_ の要素を意味のある名前の変数に一度代入してから使います. shift を使って要素をひとつづつ取り出しても, 変数のリストにいちどに代入してもかまいません. また,添え字を指定して個々の要素を参照することもできます. 配列変数の名前が @_ ですから,その最初の要素は @ を $ に変えて添え字を指定して,$_[0] のようにして参照できます (下の例のコメントを参照).

# メインのプログラム
&repeat_message("Hello, subroutine!", 5);
print "End of message\n";

# サブルーチンの定義.
# 渡された文字列を,2番めの引数の回数だけ繰り返して表示.

sub repeat_message
{
    $message = shift @_; 
    $rep = shift @_;
    # 上の2行をあわせて( $message, $rep) = @_; とも書ける.
    # また,$message = $_[0];  $rep = $_[1]; のように個々の要素を参照することも可.
    
    for ($i = 0; $i < $rep; ++$i) {
        print $message, "\n";
    }
}    

こんどはもう少し実用的な例です. 与えられたデータの平均と標準偏差を計算し,このふたつの値を リストとして返すサブルーチンを書いてみました. syntactic sugar のページ で解説した特殊変数 $_ も使ってます.

# コマンドラインで指定したファイルからデータを読み込む

while (<>) {           # 入力データは特殊変数 $_ に代入される.
    chomp;             # chomp の対象は $_ 
    push @dataset, $_; # データを配列に加える.
}

($mean, $sd) = &calc_mean_and_sd(@dataset);   # データの平均と標準偏差を求める
printf "mean, %.3f;  sd, %.3f\n", $mean, $sd; # 結果を表示.


#  受けとったリストの要素の平均と標準偏差を計算するサブルーチン

sub calc_mean_and_sd 
{
    $sum = 0;         # 積算用変数の初期化(書かなくてもよい)
    $sum_sq = 0;

    foreach $x (@_) {
        $sum += $x;           # 積算
        $sum_sq += $x ** 2;   # 二乗和
    }

    $n_data = @_;
    return (0, 0) if $n_data == 0;      #  データがひとつもない場合.
    return ($sum, 0) if $n_data == 1;   #  データがひとつの場合.

    $mean = $sum / $n_data;
    $sd = sqrt( ($sum_sq - $sum ** 2 / $n_data) / ($n_data - 1) );

    return ($mean, $sd);
}

この例では,平均と標準偏差というふたつの値をリストとして返しています.

サブルーチンのどこでも return 文があったらそこで処理を終えて 呼び出し側に戻ります. データ数がゼロや1の場合は,そのままあとの処理をするゼロで割り算をして しまいエラーが発生するので,特別扱いして return しています.


ふたつの配列を渡すには

引数がひとつの配列 @_ に詰め込まれて渡されるのだとすると, ふたつ配列を渡したかったらどうなるのか心配になります. たとえば,ふたつの配列の共通部分を探すなんてすぐ思いつく例です.

実は,引数のところにふたつの配列を書いても,@_ ではそれらがつながった ひとつの配列となってしまいます.どこがふたつの配列か,切れ目が分かりません. この問題のスマートな解決策は,名前だけ何度か出てきてたリファレンスを使う ことなんですが,これはもうちょっと待ってください.

これまでの知識でできる解決は,配列の要素をつなげた文字列を作って渡し, サブルーチン中で split してから使うという方法です. ハッシュの活用:柔軟なデータ集計(その2) で,似たような例がありましたね.2つの配列それぞれをひとつの文字列にして 渡せば,サブルーチン内でちゃんと区別して扱えます. 配列の要素を一度に連結してしまうには,join という関数が便利です. たとえば $x = join (":", @a); と書くと,配列 @a の全要素を, あいだにコロン ':' をはさみながら連結した文字列が $x に代入されます. これをまた分割するには,当然,split (/:/, $x); です.

@group_a = ("a", "b", "c", "d");
@group_b = ("a", "c", "e", "f");

$group_a = join (":", @group_a);    #  配列の要素を連結.
$group_b = join (":", @group_b);

@common = &find_common($group_a, $group_b);  # 共通要素を探す.

foreach (@common) {  # 共通要素を表示する.
    print $_, "\t";
}

#  ':' で区切られた多数の要素を含む文字列2つを受け取り,
#  共通要素のリストを返すサブルーチン

sub find_common 
{
    ($a, $b) = @_;

    @a = split /:/, $a;  #  文字列を分割して配列に.
    @b = split /:/, $b;
    @c = ();             #  共通要素を入れる配列.

    foreach $a_element (@a) {      #  @a の各要素を…
        foreach $b_element (@b) {  #   @b の各要素と比較.
            if ($a_element eq $b_element) {  # 同じだったら,
                push @c, $a_element;         # 共通要素として配列にしまう.
                last;              # これ以上 @b の要素を調べる必要なし.
             }                     #   → @a の次の要素に進む.
         }
    }
    return @c;   #  共通要素の配列を返す.
}

では,ハッシュをサブルーチンに渡したかったら?これも, リファレンスを使ったスマートな方法と,それ以外の便法がいくつかあります. どうも,リファレンスを先送りするばかりで徐々にフラストレーションを貯めてる 感がありますね.便法の解説をするよりも, もう少し先のリファレンスの解説を待つことにしましょう. リファレンスを使えば 配列でもハッシュでもファイルハンドルでもサブルーチンに渡せます. また,サブルーチンにスカラー値やリストだけでなくハッシュを返させる こともリファレンスを使うと可能です. なんだか,どんどんリファレンスへの期待が高まるようです…


よい名前をつけてあげよう

プログラムをサブルーチンに分割すると, 全体の処理の流れを把握しやすくなるというメリットもあります. あまり長くないメインの部分と,そこから呼び出されるいくつかのサブルーチン という構造のプログラムなら,まずメインの部分を読んで全体の流れを把握し, 必要に応じてサブルーチンの内部を調べる,という読み方ができます.

ただし,メイン部分だけ読んで流れを追えるためには処理の内容が分かるような サブルーチン名をつけないといけません.上の,平均と標準偏差を 求めるサブルーチンの名前を例に考えてみます.まず,とても悪い例から.

#  サブルーチン

sub subroutine01
{
    #  中身は上の例と同じ.
}

サブルーチン名を見ただけでは何も分かりませんね.その上の 「# サブルーチン」というコメントも何も語っていません. 中身を読んではじめて算術平均と標準偏差を計算していることが分かります.

下はましな例です.

#  平均と標準偏差を計算する

sub stats  
{
    #  中身は上と同じ.
}

コメントで何をするサブルーチンか書いてありますし, サブルーチン名も統計計算らしさをうかがわせます. でも,どうせならもっと具体的な名前のほうがよいでしょう. 最初に書いた例を繰り返します.

#  受けとったリストの要素の平均と標準偏差を計算する

sub calc_mean_and_sd 
{
    #  中身は上と同じ.
}

サブルーチン名は処理そのもの,コメントにはどんな引数を渡すか まで書いてあって,これなら読みやすいし使いやすいですね.

ところで,具体的に何をするサブルーチンか分かるような名前をつけるには, サブルーチンそのものが,これは何をするサブルーチンだ, と簡単にはっきり言えるように設計されてないといけません. 呼ばれた時刻にあわせて挨拶を表示し(おはよう!とかこんばんわ!とか), 与えられたデータの平均と標準偏差を計算し, それをファイルに保存したあと,きょうの運勢を占ってくれるサブルーチンには 名前の付けようがありません. サブルーチンの命名に困ったら,サブルーチンの設計自体がこれでよいのか 考え直してみましょう.

下請け、孫請けに出して構造の整理

サブルーチンの中からほかのサブルーチンを呼ぶことができます. さらにそのサブルーチンからほかのサブルーチンを呼んでもよい, サブルーチンの呼び出しに制限はありません.

※サブルーチンが自分自身を呼ぶことも可能です.再帰呼び出しと言って, 問題によってはこれを使うととてもスマートなプログラムが書けます.

サブルーチンから他のサブルーチンを呼んでいる擬似コード(そのままじゃ 動かない)を書いてみます.

# メイン部分

$days = 100; 
$production = &calc_total_production($days); #  100 日間の光合成生産を求める.
print $production;

#  引数で受けとった期間の光合成生産量を求めるサブルーチン

sub calc_total_production  
{
    $grow_days = shift @_;  # 生育期間
    $total_prod = 0;
    
    for ($i = 0; $i < $grow_days; ++$i) {
        $light_amount = &todays_light($i);
        $total_prod += &calc_daily_production($light_amount);
    }
    return $total_prod;
}

#  引数で受けとった日光量に応じた光合成生産量を求めるサブルーチン

sub calc_daily_production
{
    $light = shift @_;
    # ...  $light に応じて日光合成生産量を計算して返す.
}

#  指定された日の積算光量を返すサブルーチン

sub todays_light
{
    # ...  今日の光を計算して返す.
}

メイン部分を見るだけで、100日間の光合成生産量を計算していること、実際の計算は calc_total_production というサブルーチンに下請けに出していることが分かります。 計算手法をもう少し詳しく知りたいなら、サブルーチン calc_total_production の中を覗きます。 すると、一日ごとに光量を計算し、それにもとづいてその日の生産を計算していることが分かります。 さらに光の計算方法を知りたいなら孫請けサブルーチン todays_light を読んでみる、 光量からどのように生産を計算しているかを知りたければ別の孫請けサブルーチン calc_daily_production を 読んでみる、というように見ていきます。

このようにプログラムが階層的に整理されていると、全体の見通しがとてもよくなります。


プログラムをサブルーチンに分割して書くことのメリットを説明してきました. サブルーチンをきちんと書いて,あとは内部の詳細は気にせずに呼び出して使うだけ, となるとプログラムは書きやすくなります.でも,ほんとに 「内部の詳細は気にせずに」使うには,「変数の局在化」が必要になります. その解説は次のページへ.



| Top Page | プログラミング | Perl 目次 | prev | next |