January 28, 2004

Perl の変数に関するちょっとした誤解と、動的な性質について

[ Java , Perl ]

ここ最近、Perl や Java の話を何人かの友達や先輩と話す機会がなぜか数回あったのですが、飲み会の席とかですと周囲の非エンジニアの人たちが引いてしまうので、あまり突っ込んだ話もできません。そこで、いい機会だしエントリとしてまとめてみることにしました。

他のサイトでもときどき見かけたり、聞いたりするのですが、「Perlは(CやJavaと違って)変数宣言がなくて typo に気付かなかったりどこで変数が初期化されたかわからなくなるのが欠点」という話。これはちょっとした誤解です。

なんか頑張って書いたら長文になってしまいました。

まず、なぜ変数宣言がないと困るのかというと、(かなりわざとらしいですが) 以下のコードが好例です。

#!/usr/local/bin/perl
$hello = "Hello, World\n";
print $hell;

変数に代入された「Hello, World!!」を出力させたいところなのですが、3行目で $hello を typo して $hell になってしまっています。このスクリプトを実行しても、

[naoya@mary panimal]$ perl no_strict.pl
[naoya@mary panimal]$

と、エラーも何も出力されません。

変数宣言が必要な言語、例えば Java の場合はコンパイラが typo な変数は宣言されていないとしてエラーに気付かせてくれますが、Perl の場合それがないためにこういった間違いは注意深くコードとにらめっこしないと、発見できないと思われがちです。

しかし、さきにも述べたとおりこれはちょっとした誤解で、Perl の標準機能であるstrictプラグマを使えば、変数を局所化することなしに使用することに制限をかけることができ、この問題を解消することができます。

#!/usr/local/bin/perl
use strict;
$hello = "Hello, World\n";
print $hell;

と、スクリプトの先頭付近で「use strict」の一行を宣言しておきます。この状態で Perl のシンタックスチェックをかけてみましょう。

[naoya@mary panimal]$ perl -cw no_strict.pl
Global symbol "$hello" requires explicit package name at no_strict.pl line 3.
Global symbol "$hell" requires explicit package name at no_strict.pl line 4.
no_strict.pl had compilation errors.

きっちりエラーを返してくれます。このスクリプトがエラーを吐かないようにするためには、

#!/usr/local/bin/perl
use strict;
my $hello = "Hello, World\n";
print $hello;

と my により必ず変数を局所化するようにします。strictプラグマは、局所化されていない変数の使用/シンボリックリファレンスの使用/bareword(見つからないサブルーチンやファイルハンドル名)の使用に制限を加えてくれます。一定の規律が与えられるため、健全なコードの記述の助けになります。加えて warningsプラグマ を併用すると、未使用の変数があった場合の警告なども出力されるようになります。これらのプラグマの作用によるエラーメッセージが出ないよう、コーディングをするといいというわけです。

use strict は特別な行為ではなく、どんなに短いスクリプトを記述する場合でも、それを宣言することは Perl プログラミングにおいて常識です。ネットで配布されている掲示板プログラムなどでは宣言されていない場合も多いですが、あれはプロバイダが提供する Perl インタプリタがモジュールをサポートしていなかったり、(相当)古いバージョンとの下位互換性確保のためであるとか、そういう理由があるんだと思います。(普通に知らなかっただけというスクリプトもあるかとは思いますが)

ということで、変数宣言が無いから typo が云々という話は誤解であるということで一つ納得していただきたい。

型付きの言語と比べたときに、Perl が変数に型を持たないことが大きく影響するのは、宣言が云々という部分ではなくて、主にメソッド呼び出しの際です。すなわち、Javaなどの静的な型付けを持った言語に対してPerlは動的という点です。これは、オブジェクト指向プログラミングにおけるポリモフィズムの簡単な実装が分かりやすい例になります。

animal_uml.gif

インタフェース Animal を実装した Dog と Tiger を用意するという簡単な設計で例示してみます。(これまた至極わざとらしい例ですね。)

まずは、Java の実装を見てます。手始めにインタフェースとなる Animal.java です。

public interface Animal {
    void bark();
}

次に、そのインタフェースを実装した Dog.java。

public class Dog implements Animal {
    public void bark() {
	System.out.println("わんわん");
    }
}

続いて Tiger.java。

public class Tiger implements Animal {
    public void bark () {
	System.out.println("がおお");
    }
}

これで、役者はそろいました。Main.java にて、ポリモフィズムを使って犬や虎を鳴かせてみましょう。

import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;
 
public class Main {
    public static void main (String[] args) {
	List zoo = new ArrayList();
	
	zoo.add(new Dog());
	zoo.add(new Dog());
	zoo.add(new Tiger());
	zoo.add(new Dog());
	zoo.add(new Tiger());
 
	Iterator it = zoo.iterator();
	while (it.hasNext()) {
	    Animal animal = (Animal)it.next();
	    animal.bark();
	}
    }
}

実行結果は以下です。

[naoya@mary Java_and_Perl]$ java Main
わんわん
わんわん
がおー
わんわん
がおー

Dog インスタンス、Tiger インスタンスはそれぞれ異なるクラスから生成されたオブジェクトですが、インタフェース Animal を実装しているので、生成したインスタンスは Animal 型の変数で受けることができます。Animal 型変数に代入されたオブジェクト対して bark() メソッドを呼び出したとき、そのオブジェクトが Dog、 Tiger、どちらのインスタンスかによって、振る舞いが変わっています。

さて、このときプログラマが typo して bark() メソッドの呼び出しを誤って bank() としてしまったとします。

while (it.hasNext()) {
    Animal animal = (Animal)it.next();
    animal.bank();
}

この状態でコンパイルすると、どうなるでしょうか。

[naoya@mary Java_and_Perl]$ javac *.java
Main.java:18: シンボルを解決できません。
シンボル: メソッド bank ()
場所    : Animal の クラス
            animal.bank();
                  ^
エラー 1 個

当然のようにコンパイラがシンタックスエラーを検知して、それを教えてくれます。とても当たり前のように思われますが、ここで敢えて、コンパイラが animal インスタンスには bank() メソッドが実装されていないことを検知できたのか考えてみましょう。それはもちろん、Java は変数に型があるからです。

Java ではインスタンスを変数で受ける場合、

Dog dog = new Dog();

として Dog インスタンスを Dog 型の変数に代入することを明示する必要があります。これにより、(プログラムを実行することなしにソースコードの解析結果から)コンパイラが dog 変数には Dog クラスのオブジェクトが入っていることを理解することができるようになります。従って、

dog.noImplementedMethods();

などとして、存在しないメソッドを起動した場合でも、コンパイラはそれが存在しないことを検知してエラーを出力してくれます。また、ポリモフィズムの実現に際しては、Dog クラスのインスタンスを Dog 型ではなく Animal 型で受けます。

Animal animal = new Dog;
animal.bark();

インタフェース Animal には bark() メソッドが(抽象メソッドとして)用意されているので、コンパイラはそれを理解しコンパイルを通します。このとき、仮にインタフェース Animal を用意せずに単に Dog、Tiger でそれぞれ bark() を実装しただけですと、それらのクラスを共通して受けることができて、且つ bark() メソッドを起動してもコンパイラがエラーを吐かない変数型は用意できないため、ポリモフィズムの実現は不可能です。

逆に言うと、interface や abstract class によって下位のクラスに対してそのインタフェースを共通化させることができ且つそれをコンパイラが検知できるのが Java のような静的な型付けを持つ言語の強みです。Java が大規模開発に向いていると言われる主な理由の一つはここにあります。(参考: '大規模開発では PHP や Perl よりも Java、という話について')

さて、次に Perl による実装を見てみましょう。インタフェースの Animal.pm

package Animal;
use strict;
sub bark { die }
1;

Perl にはシンタックスとしてのインタフェースや抽象クラスという概念がありませんが、このようにメソッド呼び出しに対して有無を言わさず例外を投げるようにするのが、その実装方法の一つです。

次々行きます。Dog.pm。use strict を忘れずに。

package Dog;
use strict;
use warnings;
sub new {
    bless {}, shift;
}
 
sub bark {
    my $self = shift;
    print "わんわん\n";
}
 
1;

次は Tiger.pm。

package Tiger;
use strict;
use warnings;
sub new {
    bless {}, shift;
}
 
sub bark {
    my $self = shift;
    print "がおお\n";
}
 
1;

作ったクラスを利用してポリモフィズムを実現するための main.pl です。

#!/usr/local/bin/perl
use strict;
use warnings;
use Dog;
use Tiger;
 
my @zoo = ( Dog->new, Dog->new, Tiger->new, Dog->new, Tiger->new );
 
for my $animal (@zoo) {
    $animal->bark;
}

シンタックスチェックと、実行結果は以下のようになります。

[naoya@mary panimal]$ perl -cw main.pl
main.pl syntax OK
[naoya@mary panimal]$ perl main.pl
わんわん
わんわん
がおお
わんわん
がおお

問題なく、Java 版と同じ実行結果が得られています。

では、Java のときに同じく bark メソッド呼び出しを typo して bank を呼び出してみましょう。

for my $animal (@zoo) {
    $animal->bank;
}

まずは、シンタックスチェックです。

[naoya@mary panimal]$ perl -cw main.pl
main.pl syntax OK

Java ではコンパイラがこのタイミングでエラーを検知するのですが、Perl では予想に反して? 文法エラーにはなりませんでした。実行すると...

[naoya@mary panimal]$ perl main.pl
Can't locate object method "bank" via package "Dog" at main.pl line 11.

とエラーが出ます。

この結果が意味するのは、Perl はメソッド呼び出しが動的であるということです。つまり、(Perl は事前にコードをコンパイルしますが)メソッド呼び出しは実行時になってはじめて解釈されるということです。

動的であるということは、コンパイラがコンパイル時にはインタンスに結び付けられたメソッドのチェックは行っていないということでもありますが、裏を返すとチェックができないということでもあります。それは、Perl の変数には明示的な型が存在しないからです。

先の main.pl を見ると一目瞭然なのですが、

for my $animal (@zoo) {
    $animal->bark;
}

このポリモフィズムの実現に際して、変数型という概念が一切出てきませんね。また、Animal インタフェースを作ったけれども、このコードには Animal インタフェースは必要ないのです。変数に型はありませんから、コンパイラはその時点で変数に入っているインタンスがどのクラスに属しているのかを、(実行前には)判定することができません。そのインスタンスに対して所望のメソッドは実装されていなかったよという実行事例外で初めてその事実を知ることになります。

従って、Java のように下位のクラスの実装をスーパークラスから強制するメカニズムがないため、いわゆる「スーパークラスやインタフェースでプログラミングする」ことが、意味を成さないときが多々あります。(実装上は意味のないインタフェースや抽象クラスを敢えて用意することで、プログラマの意図をはっきりさせるという利点はもちろんあります。)


ちなみに、Perl 5.005 から導入されている型指定されたレキシカル変数

my Classname $variable_name;

という形式を使うとコンパイル時チェックが効きますが、これをプログラマに強制する手段が用意されておらず、性善説の上にしか成り立ちません。 この形式は fields プラグマと組み合わせた際のインスタンスフィールドに対してしか効果がありません。誤解しておりました。(thanks to 宮川さん)

ここまで読むと、Perl のメソッド呼び出しが動的であることは欠点のようにしか思えませんが、無論そんなことはなく、動的であるが故にもたらされる様々な利点ががあります。(且つ、その利点が欠点を大きく上回っているようにも思います。)

例えば Perl には AUTOLOAD というメカニズムが用意されています。AUTOLOAD は 動的にメソッドを呼び出してみて、そのパッケージ/クラスに該当のサブルーチン/メソッドが存在しなかった場合、AUTOLOAD() メソッドが呼び出される、という仕組みです。オブジェクト指向プログラミングにおいては、これを使うと簡単にメソッドの delegate が可能になります。

先の Animal/Dog/Tiger のクラス構成に、ExtraDog というクラスを加えてみましょう。ExtraDog は Dog に同じく犬なのですが、「自分は巷の犬とは違った高尚な犬なのだ」と自惚れつつも「吠えろ」と命令されるとパブロフの犬がごとく、その辺の犬同様に吠えてしまうという犬です。AUTOLOAD を利用して、何かしら命令されるたびに「僕はその辺の犬とは違うんだ」と思いながらも巷の犬同様に振舞ってしまう犬として実装してみます。

package ExtraDog;
 
sub new {
    my $class = shift;
    $self = bless {}, $class;
    $self->{_dog} = shift;
    $self;
}
 
sub AUTOLOAD {
    my $self = shift;
    (my $method = $AUTOLOAD) =~ s/.*://g;
    return if ($method eq 'DESTROY');
 
    print "(僕はその辺の犬とは違うんだ...) ";
    return $self->{_dog}->$method;
}
 
1;

コンストラクタで Dog インタンスを引数に受け取り、メソッドはすべて AUTOLOAD でその Dog インスタンス delegate。delegate の直前にちょっとぼやきが入ります。このクラスは、

my $ext_dog = ExtraDog->new(Dog->new);

として使います。いわゆる Decorator パターンです。

さて、先の main.pl にこのExtraDog も追加してみましょう。

#!/usr/local/bin/perl
use strict;
use warnings;
use Dog;
use Tiger;
use ExtraDog;
 
my @zoo = ( Dog->new, Dog->new, Tiger->new, Dog->new, Tiger->new );
my $ex_dog = ExtraDog->new(Dog->new);
push @zoo, $ex_dog;
 
for my $animal (@zoo) {
    $animal->bark;
}

以下、実行結果です。

[naoya@mary panimal]$ perl main.pl
わんわん
わんわん
がおお
わんわん
がおお
(僕はその辺の犬とは違うんだ...) わんわん

なんだか一匹、自惚れた犬が混ざっていますね。

Perl の動的メソッド呼び出しはうまく使うと非常に強力で、getter/setter などのアクセサをいちいちそれぞれに用意することなくまとめて作ってしまったりといったことも可能になります。Movable Type の MT::Object は、RDBMS のレコードをオブジェクトにマッピングすることで、SQL 要らずでデータベースを透過的に操作することを可能とするクラス(POOP/Perl Object-Oriented Persistence)ですが、これも AUTOLOAD を使って実装されているようです。

Class::DBI なども MT::Object に同じく POOP を可能とする非常に強力なクラス(というよりもはやフレームワーク?)ですが、これも Perl のメソッドが動的呼び出しであることをふんだんに利用している実装の代表例かと思います。

静的な言語、動的な言語はそれぞれ利点、欠点がありますが、要は適材適所で、最終的には実装がどうあれほとんど差はないというのが実感です。○○言語だからどうこう、と決め付けてしまわずにそのメカニズムをちょっと詳しく覗いてみると、色々新たな発見や先入観の払拭ができたりして、面白いかもしれません。

ところで、気になるオブジェクト指向実装における Perl スクリプトの実行速度ですが、Damian Conway 氏による『オブジェクト指向Perlマスターコース―オブジェクト指向の概念とPerlによる実装方法』の序章にこうあります。

一般には、オブジェクト指向Perlによるシステムの実装は、それと等価の非オブジェクト指向実装よりも高速になることはなく、実際には比較して通常20〜50パーセントほど低速になる。
この数字はオブジェクト指向Perlから多くのユーザを遠ざけるほど十分に大きいかもしれないが、オブジェクト指向の設計面および実装面のそれを補うさまざまなり点を見逃すのは悲劇的である。
(中略)
残念ながら多くの人は、「20〜50パーセントの低速化」という数字に惑わされ、過去6か月間でプロセッサ速度が2倍になったにもかかわらず、何を意味しているか忘れがちになる。
オブジェクト指向Perlマスターコース―オブジェクト指向の概念とPerlによる実装方法

また、他言語との比較データが欲しい方には川合さんの 'JavaはPerlよりも比較にならないほど速い?' あたりが参考になるかと思います。

僕が今持っている知識をまとめた程度のものなので、間違いも多々あるかもありません。気付いたかたは指摘していただければ幸いです。:)

Posted by naoya at January 28, 2004 03:19 AM | トラックバック (5)  b_entry.gif
トラックバック [5件]
TrackBack URL: http://mt.bloghackers.net/mt/suck-tbspams.cgi/838
Perl OOP
Excerpt: NDO::Weblog: Perl の変数に関するちょっとした誤解と、動的な性質について ここ最近、Perl や Java の話を何人かの友達や先輩と話す機会がなぜか数回あったのですが、飲み会の席とかですと周囲の非エンジニアの人たちが引いてしまうので、あまり突っ込んだ話もできません...
Weblog: blog.bulknews.net
Tracked: January 28, 2004 06:58 AM
Perlの誤解
Excerpt: NDO::Weblog: Perl の変数に関するちょっとした誤解と、動的な性質について 他のサイトでもときどき見かけたり、聞いたりするのですが、「Perlは(CやJavaと違って)変数宣言がなくて typo に気付かなかったりどこで変数が初期化されたかわからなくなるのが欠点」という話。...
Weblog: p0t
Tracked: January 28, 2004 10:40 AM
Perlの変数と動的な性質について
Excerpt: Perl の変数に関するちょっとした誤解と、動的な性質について [NDO::Weblog] ここ最近、Perl や Java の話を何人かの友達や先輩と話す機会がなぜか数回あったのですが、飲み会の席とかですと周囲の非エンジニアの人たちが引いてしまうので、あまり突っ込んだ話もできませ...
Weblog: Orbium
Tracked: January 28, 2004 11:17 PM
ということは
Excerpt: これはJavaの仕様に、同じ名前のメソッドとして作る必要がないよね?と言われているわけだ。つまり、昨日の話で言うならば、NodeListはList型として抽象化...
Weblog: As An Engineer, Like A Programmer...(Written By Y.Sawa)
Tracked: January 16, 2005 02:47 AM
関数呼び出しとクラス継承のベンチマーク
Excerpt: フレームワークを考えるにあたって、気になる部分のベンチマークを取ってみた。 ポイントは次の3点。 関数の呼び出し方法: Class::func() と Clas...
Weblog: Daio Today
Tracked: July 8, 2005 11:53 PM
コメント [0件]