┌──────────────────┐
│Perl入門 〜構造化プログラミング編〜 │
│(6)エラー処理            │
│2008/05/25             │
└──────────────────┘

[▲Perl Home▲]

●例外処理(1):dieとevalを使う

 ◆エラー処理の必要性

  ・ツールの有用性が高くなると、自分だけでなく多くの人が、様々な用途で使用す
   るようになってきます。すると当然想定外の要素も多くなり、それ故のエラーも
   増えてきます。

  ・エラー処理とは、想定外の事柄...つまり「例外」をどう処理するかというもの
   です。そこで深刻なエラーの処理について話をしたいと思います。


 ◆evalによる例外補足

  ・典型的なシステムエラー、例えば「0で割ってしまった」というケースを考えて
   みましょう。下記のようなコードでは当然システムエラーが起きます。

  ┌───────────────────────────────────┐
   #!/usr/local/bin/perl
   use strict;
   
   {
     my $op_a;
     my $op_b;
     my $ans;
     
     $op_a=20, $op_b=0; # 分母が0!!
     $ans = CalDiv($op_a, $op_b);
     print "$op_a / $op_b = $ans\n";
   }
   
   sub CalDiv {
     my ($a, $b) = @_;
     my $c;
     
     $c = $a / $b;
   
     return($c);
   }
  └───────────────────────────────────┘
  ┌───────────────────────────────────┐
   C:>test_err.pl
   Illegal division by zero at test_err.pl line 18.
  └───────────────────────────────────┘

  ・このままだとプログラムがエラー終了するので、例えば
    + division by zero が発生したら、STDERRにメッセージを表示
    + 割り算の解には ERROR を入れておく
   のようにしたいこともあるでしょう。こんなときには「eval」を使用します。

  ・evalは下記の機能を持ちます。
    + 実行時のエラーを補足
    + 特殊変数「$@」にエラーメッセージを入れる
     → エラー無い場合は、$@は空文字列になる

  ・この機能を実装したコードを示します。division by zeroで強制終了せずにプロ
   グラムが続行されていることがわかると思います。

  ・特殊変数「$@」でエラー処理をしている部分で、関数warnを使用しています。こ
   れはユーザ定義の文字列に加えて、該当部のファイル名と行番号も付加したうえ
   でSTDERRに表示します。

  ┌───────────────────────────────────┐
   #!/usr/local/bin/perl
   use strict;
   
   {
     my $op_a;
     my $op_b;
     my $ans;
     
     $op_a=20, $op_b=0; # 分母が0!!
     $ans = CalDiv($op_a, $op_b);
     print "$op_a / $op_b = $ans\n" if ($ans ne 'ERROR');
   
     $op_a=20, $op_b=10; # 普通に計算
     $ans = CalDiv($op_a, $op_b);
     print "$op_a / $op_b = $ans\n" if ($ans ne 'ERROR');
   }
   
   sub CalDiv {
     my ($a, $b) = @_;
     my $c;
     
     eval { $c = $a / $b; }; # 最後「;」に注意
     if ($@) { # division by zeroの処理
       $c = 'ERROR';
       warn "$a / $b = ERROR";
     }
   
     return($c);
   }
  └───────────────────────────────────┘
  ┌───────────────────────────────────┐
   C:>test_err.pl
   20 / 0 = ERROR at test_err.pl line 25. # これは STDERR
   20 / 10 = 2              # これは STDOUT
  └───────────────────────────────────┘


 ◆die関数とeval

  ・しかし上記のエラーメッセージを見ると、25行目に問題あるように見えますが、
   本当は「0で割算」命令を発行した「10行目」が悪者です。つまり、本来であれ
   ばエラーを起こした起点に情報を伝播させる必要があります。

  ・このときは、die関数 と evalを組み合わせて使用します。die関数はwarn関数と
   同様にSTDERRにメッセージを表示して、プログラムを停止させます。

  ・die関数のよく見る形としては
     open(FH, "< read_file.txt") or die;
   のような使い方をしていることが多いと思います。上記は「or演算」の左辺が
   undefで終わった場合に、右辺実行でdieによるプログラム終了です。

  ・すると「あれ? エラー伝播(再投入)したいのに、終了関数die()を使うってどう
   いうこと?」と感じるでしょう。

  ・そうです。ここがポイントです。今回の目的「エラーを起こした起点に情報を伝
   播させる」では「evalブロックの中でdie関数を使う」のです。die関数の機能は
    + プログラム終了させる
    + 例外を投入する
   ですが、このうち前者の「プログラム終了」をevalによって止める作戦です。

  ・evalブロックの中でdie関数を使うと、エラーメッセージを特殊変数$@に入れた
   後も、dieによるプログラム終了が発生しません(eval自身で補足される)。その
   まま呼び出し側に投入して、再度エラー補足させた際に「特殊変数の$@の中身を
   使わせる」という形になります。

  ・ちょっとややこしいので、とにかく例を見ることにしましょう

  ┌───────────────────────────────────┐
   #!/usr/local/bin/perl
   use strict;
   
   {
     my $op_a;
     my $op_b;
     my $ans;
     
     $op_a=20, $op_b=10; # 普通に計算
     my $ans = CalDiv($op_a, $op_b) or die;
   
     $op_a=20, $op_b=0; # 分母が0!!
     my $ans = CalDiv($op_a, $op_b) or die; # このdieで再補足
   }
   
   sub CalDiv {
     my ($a, $b) = @_;
     my $c;
     
     eval {
      ($c = $a / $b) or die; # eval内では、dieでも終了しない!!
     }; # 最後「;」に注意
   
     if ($@) {
       warn "$a / $b = ERROR\n"; # \n終了で付加メッセージ無し
     } else {
       print "$a / $b = $c\n";
     }
   
     return($c); # ERROR時はundef戻し
   }
  └───────────────────────────────────┘
  ┌───────────────────────────────────┐
   20 / 10 = 2
   20 / 0 = ERROR
   Illegal division by zero at test_err.pl line 21.
       ...propagated at test_err.pl line 13.
  └───────────────────────────────────┘

  ・この例を見るとわかるように、21行目で発生したシステムエラーはevalブロック
   内なので、die関数が実行されても終了しません。ただし、$@へのエラーメッセ
   ージはセットされます。

  ・サブルーチン CalDiv は undef を戻すので、13行目で左辺不成立により再度die
   関数が実行されています。このとき特殊変数$@の中身が空文字列ではないので、
   die関数は
     \t...propagated at
   をメッセージとして付加します。

  ・この結果生成されたエラーメッセージを見ると、21行目でdivision by zeroが発
   生し、それは13行目から伝播したものであることがわかります。


 ◆実はtry〜catchで書くことも

  ・今回 eval/dieで記述した例外処理。C++等では、try/catchで書きますが、実は
   Perlでもtry/catch記述を使用することができます。それにはCPANの
     http://search.cpan.org/~uarun/Error-0.15/Error.pm
   を導入する必要があります。

  ・上記moduleは、まだ「Perlをインストールすれば黙って入ってくる」ものではな
   いので、今回は説明しませんが、C++での記述に慣れている方には良いかもしれ
   ません。


●例外処理(2):Carpモジュールのcroak/carpを使う

 ◆トレース情報を得るならCarpを使うのが楽

  ・ここまでエラー情報のトレースを行わせるために、evalとdieを使いました。し
   かし常にトレース情報を得たい場合、eval/dieよりも便利な方法があります。そ
   れは
    Carpモジュール
   を使うことです。

  ・このPerl解説のコンテンツでは「構造化プログラミング」を意識していますが、
   構造化コーディングすなわちサブルーチンを多用した場合や、コードがモジュー
   ルとして利用されている場合、先ほどのようにeval/dieを使った方法では
    + 例外catch/throwの仕組みが可能なことはわかったが
    + 再投入/再補足がちょっと面倒
     - eval {}クロージャ内のスコープを意識するとかも
   が少し引っかかっていると思います。

  ・そこでエラートレースをするためであればCarpモジュールを使います。むしろ
   Perl5.8以上ではCarpモジュールを使うべきだと思います。

  ・Carpモジュールでは、下記の関数が使用できます。(*1)
    carp / cluck  : warn関数相当
    croak / confess : die関数相当

  ・warn/dieとの違いは、呼び出し元の情報:スタックのバックトレース情報を表示
   する点にあります。

  ・これもサンプルコードを見てみましょう。下記のサンプルコードは引数のファイ
   ルを全て読み込み/表示するものですが、引数指定が無い場合と、一部のファイ
   ルが無い場合について実行してみました。

  ┌───────────────────────────────────┐
   #!/usr/local/bin/perl
   use Carp;
   use strict;
   
   {
     my $src = ReadFiles();
     PrintDatas($src);
   }
   
   #====================================================
   sub ReadFiles {
   
     ArgCheck() or croak;
     my $src = AccumulateDatas();
   
     return($src);
   }
   
   #====================================================
   sub PrintDatas {
     my ($src) = @_;
   
     for (my $i=0; $i<@{$src}; $i++) {
       print "==== FileName:",$src->[$i]->{name}," ====\n";
       my $buf = $src->[$i]->{list};
       for (my $j=0; $j<@{$buf}; $j++) {
         print $buf->[$j];
       }
     }
   }
   
   #====================================================
   sub AccumulateDatas {
     my @src;
   
     for (my $i=0; $i<@ARGV; $i++) {
       open(FH, "< $ARGV[$i]")
        or carp "WARN:$ARGV[$i]が無い";
       my @sub_src = <FH>;
       push(@src, {
           'name' => $ARGV[$i],
           'list' => \@sub_src } );
       close(FH);
     }
   
     return(\@src);
   }
   
   #====================================================
   sub ArgCheck {
     ($#ARGV >=0) or croak "ERROR:引数指定無い";
   
     return($#ARGV);
   }
  └───────────────────────────────────┘
  ┌───────────────────────────────────┐
   >test_err.pl # 引数無し実行
   ERROR:引数指定無い at test_err.pl line 51
       main::ArgCheck() called at test_err.pl line 13
       main::ReadFiles() called at test_err.pl line 6
  └───────────────────────────────────┘
  ┌───────────────────────────────────┐
   >test_err.pl data0.txt data1.txt # data1.txt存在しない
   WARN:data1.txtが無い at test_err.pl line 37
       main::AccumulateDatas() called at test_err.pl line 14
       main::ReadFiles() called at test_err.pl line 6
   ==== FileName:data0.txt ====
   data0 00 matsudo
   data0 01 kamihongo
   data0 02 matsudoshinden
   data0 03 minoridai
   data0 04 yabashira
   data0 05 tokiwadaira
   data0 06 goko
   data0 07 motoyama
   data0 08 kunugiyama
   data0 09 kitahatsutomi
   ==== FileName:data1.txt ====
  └───────────────────────────────────┘

   ・実行結果を見ると、引数無しの場合はcroak関数が効いて実行止まりますが、そ
    の際に呼び出し元へのトレース情報も出ていることがわかります。

  ・ファイル無しの場合は、carp関数が効いて実行は止まりませんが、このときも呼
   び出し元へのトレース情報が出ています。


●まとめ

  ・これでPerl構造化コーディングに関する基礎的な説明は大体できました。

  ・今後はいろいろなModuleを使用/作成する本格的なコーディング又は、オブジェ
   クト指向へ移ることになると思いますが、このレポートが一助になることを願っ
   ております。

                                    Monpe

(*1)今回のサンプルコードではcarp/cluck と croak/confessの表示に違いは出ません。

[▲Perl Home▲]

Copyright(c)2008 Monpe
All Rights Reserved