Perl

(作成:2005/01)

オブジェクト指向が叫ばれ出して久しい此頃。Perlでもオブジェクト指向が推奨されているのか、例えばCPANのモジュールなんかで大活躍なのがPerl-OOと呼ばれるオブジェクト指向Perlスクリプト。
まぁ何の事はなくPerlでオブジェクト指向スクリプトを書けるってだけだが、これがPHPなんかと比較にならない程しっかりした作りをしていて正直感動。

ウチもそこそこPerl愛用しとるので、折角だしその一部を紹介しつつ、ここで少しだけまとめてみようと思う。

Perl-OO オブジェクト指向事始め

オブジェクト指向といえばやっぱりクラス化。必要な機能ごと分割させて、順々に作っていくのが真っ当ですな。ってそれは構造化プログラミングだけれど。
さて、そいではウチが研究用に作った簡単なログ整理用スクリプトなぞを書いてみる。

ReadTextクラス ReadText.pm(長いので折り畳み)
### ReadTextクラス ReadText.pm
package ReadText;

########## Don't touch! ##########
use constant TRUE => 1;   ## Flags
use constant FALSE => 0;
##################################


# コンストラクタ
#
sub new {
    my $this = shift;
    ## ハッシュリファレンス生成
    my $hash = bless { "fh" => shift,
                       "fname" => shift,
                       "eof_flag" => FALSE,
                     }, $this;

    $this->openText($hash->{fh}, $hash->{fname});

    return $hash;
}


# テキストファイルのオープン
#
# @param  fname ファイル名
sub openText {
    my $this = shift;
    local ($fh, $fname) = @_;

    ## ファイル存在チェック
    if(!-e $fname || !-f $fname) {
        die "File '$fname' not found or illegal file.";
    }
    ## ファイルオープンチェック
    if(!open($fh, $fname)) {
        die "File open error.";
    }
}


# EOFチェック
#
# @return TRUE/FALSE
sub eofText {
    my $this = shift;

    return $this->{eof_flag};
}


# テキストファイルのクローズ
#
sub closeText {
    my $this = shift;

    close($this->{fh});
}


# 文字読込
#
# @return 文字
sub readChar {
    my $this = shift;

    my $char = "";

    ## 例外
    if($this->{eof_flag} == TRUE) { die "File handle is over EOF."; }

    ## データ取得
    if(($char = getc($this->{fh})) eq "") {
        $this->{eof_flag} = TRUE;
    }

    return $char;
}


# 文字列読込
#
# @param  length    文字列長(ASCII)
#
# @return 文字列
sub readString {
    my $this = shift;
    local ($length) = @_;

    my $str = "";

    eval {
        ## 文字列長分の文字を取得
        for(my $i = 0;$i < $length;$i++) {
            $str .= $this->readChar();
        }
    };
    if($@) {
        return $str;
    }

    return $str;
}


# 単語読込
#
# @return 単語
sub readWord {
    my $this = shift;

    my $ch;
    my $word = "";

    eval {
        ## デリミタを読み飛ばす
        do {
            $ch = $this->readChar();
        } while($ch eq " " || $ch eq "\t" || $ch eq "\n");

        ## 次のデリミタまで文字を取得
        while(($ch ne " ") && ($ch ne "\t") && ($ch ne "\n")) {
            $word .= $ch;
            $ch = $this->readChar();
        }
    };
    if($@) {
        return "";
    }

    return $word;
}


# 1行読込
#
# @param  length    文字列長(ASCII)
#
# @return 文字列
sub readLine {
    my $this = shift;

    my $ch;
    my $line = "";

    eval {
        ## 改行まで文字を取得
        while(($ch = $this->readChar()) ne "\n") {
            $line .= $ch;
        }
    };
    if($@) {
        return "";
    }

    return $line;
}


1;

これが所謂Perlモジュールと言われるモノの内容。普通の.plファイルと違うのは、頭の #! /usr/bin/perl が無いことと、最後に 1; で終了してること。
前者は、これ単体で何か実行するものではないため、実行スクリプトを指定する必要が無いから。だから勿論、.pmファイルのパーミッションに実行権限を与える必要は無い。
後者は、何故かは判らんけれど、モジュールとして読み込んだ際にエラーになるのを防ぐ為。1=TRUE を返す必要があるらしい。userequire なんかにTRUEを返す必要があるのだろうな。うん。

ついでにこれの検証用スクリプト。

Testクラス test.pl(長いので折り畳み)
#! /usr/bin/perl

## Testクラス test.pl
package Test;

########## Don't touch! ##########
use constant TRUE => 1;                 ## Flags
use constant FALSE => 0;
##################################

## モジュール定義
##################################################
use lib "$ENV{HOME}/PerlModule/";

use ReadText;
##################################################


# コンストラクタ
#
sub new {
    my $this = shift;
    my (@foo) = @_;

    ## ハッシュリファレンス生成
    my $hash = bless { "fname" => shift,
                     }, $this;

    return $hash;
}

# メイン関数
#
# @param    
sub main {
    my $this = shift;
    my (@array) = @_;

    my $fp;

    eval {
        ## ファイル読込
        $fp = ReadText->new('DAT', $this->{fname});

        my $str = "";
        ## ファイル終端まで読込
        while(!$fp->eofText()) {
            $str = $fp->readChar();
            #$str = $fp->readString(20);
            #$str = $fp->readLine();
            #$str = $fp->readWord();
            if($str ne "") {
                print $str;
            }
        }

        ## ファイルクローズ
        $fp->closeText();
    };
    ## 例外処理
    if($@) {
        print $@."\n";
        exit -1;
    }


    return 0;
}


## エラーチェック
if ($ARGV[0] eq "") {
    die "Trial number is empty.\n";
}

$class = Test->new($ARGV[0]);
$class->main();
exit 0;

さて、いきなり長めではあるけれど、そんなに難しいもんじゃないと思う。取り敢えず何か適当なテキストファイルに対して実行してみるよろし。

$ ./test.pl hoge.txt

以降、順を追って解説をば。

クラスの作り方

順を追って解説をば。
まずはクラス定義から。

### ReadTextクラス
package ReadText;

ここでは ReadText という名前のクラスを定義した。
Perlでは「パッケージ」と呼ぶのが通例らしいけど、つまるところ package という宣言でクラス名を定義できる、と考えて差し支え無いかと。

続いて目にするのは new という関数。

# コンストラクタ
#
sub new {

これが所謂コンストラクタ関数になり、他の関数群はメンバ関数となってる。とはいえ、Perlの場合は他の関数と明確に区別されている訳でなく、コンストラクタの役割になる関数は、それなりの約束事さえ守ってさえいれば良い。
なので、 new という関数名を他の名前に変えてもOK。

まず先にコンストラクタのお約束をば。

    ## ハッシュリファレンス生成
    my $hash = bless { "fh" => shift,
                       "fname" => shift,
                       "eof_flag" => FALSE,
                     }, $this;
    ...
    return $hash;

他の関数と見比べて判るのは、コンストラクタでは bless てのを使ってオブジェクトを作ってる。 new ではこれを返すことで、クラスのオブジェクトを生成することが出来、クラス呼出し側ではこのオブジェクトを用いてクラスの各機能にアクセスする事が出来る。

また、 bless に渡すデータは「連想配列+クラス名」の形式になってるけれど、連想配列については必要に応じてで良いんじゃないかな。
この連想配列は、クラス名と一緒にオブジェクト化されるので、クラス内ではメンバ変数のように扱う事が出来る。
値を shift とすればクラス呼出し側で渡したコンストラクタ引数の順に取得出来るし、undef とすれば空のメンバ変数にも出来るし、適当な値で初期化する事も出来る。何とかと鋏は使いようさね。

さて、今度は他の関数についてもちょこっと。Perl-OOを意識しない書き方では見慣れないところも幾つかあり、まずは関数の最初に

 my $this = shift;

という部分があるのについて。
Perlの関数は、呼出し時に自身の所属する「パッケージ名」を暗黙に渡すのだそうで、それを取得することにより $this を自身のクラスへのハンドルにする事ができる。
自身のクラス内の要素へのアクセスはこれを用いて間接的に弄くれば良いというわけで。丁度C++の this と同じ働きね。
アクセス方法は、C/C++と同じくアロー演算子 -> でOK。
これまた別に区別された変数ではないので、$this 以外の名前でもOK。

クラス? 名前空間?

ここでちょっと、スコープについて。

C/C++のサンプルなんかでよく見掛ける、1ファイルに複数クラスを記述したい場合は以下のような書き方が出来たりする。

## Test1クラス test.pm
{
    package Test1;

    sub new {
        ## なんたらかんたら
    }

    sub method1 {
        ## ほげほげふがふが
    }
}

## Test2クラス
{
    package Test2;

    sub new {
        ## うんたらほにゃらら
    }

    sub method2 {
        ## ぴよぴよふぅばぁ
    }
}

...

1;

例えば、ReadText がサポートする範囲は ReadText.pm ファイル内がスコープ範囲。直上の Test1 がサポートする範囲は、それを囲うカモメ確固内がスコープ範囲。
このスコープ内で宣言された定数/変数/その他は、それぞれのクラス内で共有することができる。

逆に言えば、これらスコープ外は各クラスのサポート範囲外となるため、外で定数等を宣言しても、それを共有することができない。そういう意味では、クラス宣言というよりは名前空間の宣言と言う方が正しい。

まずここで、C/C++しか知らない人には困惑するかも知れない。C/C++でいう #define 宣言や main 関数を記述するようなグローバル領域は、Perlには無い。
かといって、Javaのような呆れる程に融通の利かないオブジェクト指向とも、ちょっと違う。
例えば上記 test.pm での各クラスを包括するカモメ括弧外の場所は、グローバル領域のように見えてそうでない。この領域は、いわゆる無名空間という扱いにでもなる、のかな?

そこで、例えばこんなスクリプトを組んでみる。

#! /usr/bin/perl

## test2.pl

use constant HOGE => "メイン定数ほげ";
my $hoge = "メインほげ";

{
    package MyClass;

    use constant HOGE => "MyClass定数ほげ";
    my $hoge = "MyClassほげ";

    $fuga = "やっぱりMyClassだけふが";

    print HOGE.' '.$hoge.' '.$fuga."\n";
}

print HOGE.' '.$hoge.' '.$fuga."\n";

# でもこうすると見えちゃう
print $MyClass::fuga."\n";
# こっちは見えない
print $MyClass::hoge."\n";

exit 0;

オブジェクト指向もへったくれも無いけれど、さておき。出力は以下の通り。

$ ./test2.pl
MyClass定数ほげ MyClassほげ やっぱりMyClassだけふが
メイン定数ほげ メインほげ 
やっぱりMyClassだけふが

この通り、 my 宣言してなくとも名前空間が違う為に出力されない。スコープ解決演算子 :: を用いれば、グローバル宣言された変数なんかは見えるけど、 my 宣言した変数はやっぱり見れない。

こういう或る程度に融通が利いて、かつ確りしたスコープ解決は本当に感服するね。スゴイねPerl。