他言語からの訪問 【第 3 回】 Perl


書いた人:たなべすなお (@sunaot)

はじめに

Ruby 以外のプログラミング言語を Rubyist へむけて紹介する他言語からの訪問、第 2 回目はまさかの Perl です。

この記事は「Perl ね。Ruby の前は使ってたな。なつかしい。」という人よりも、「LL は Ruby で入りました。便利!」と思ってる人へ向けて書いていきます。

「Perl なんて過去の言語。今さら学んでも意味がない、おもしろくない。」なんて思ってませんか?

この記事では、つい二年ほど前までそう思っていた (!) 筆者が Perl の会社へ転職したことをきっかけに覗いた Perl の魅力を紹介します。

ようこそ、Perl のある生活へ

Perl を学び始めたときに真っ先に思ったのが、Perl 版の「初めての Ruby」がほしいということでした。「初めての Ruby」は「他の言語でプログラミングを経験したことがあるプログラマを対象としています」と書かれているとおり、プログラミングについて他言語で一通りの理解があるが、Ruby はよく知らないという人向けに書かれた本です。わずか 200 ページほどの薄さにも関わらず、Ruby を始めるには十分な情報が載っているという Ruby 入門の名著です。

Ruby を日常的に、そして仕事では PHP を使っていた筆者にとって、一から Perl の書籍を読むのはムダが多く感じられ、「初めての Ruby」相当の本がほしいなとたびたび思いながらリャマ本を読んだものでした。

そこで、この記事では Rubyist のための「初めての Perl」を目指して Perl のオブジェクト指向プログラミングを解説していきます。

なお、これから記事の中で説明していくオブジェクト指向プログラミング (以降、OOP) とはクラスベースのそれを指します。

TMTOWTDI の Perl らしく、ここに書くやり方以外にもクラスベースの OOP を実装する手段はありますが、一つの実装例として読んでください。

Perl のオブジェクト指向プログラミング

説明を始める前に。

記事を読むにあたって必要になりそうなプリミティブ型の紹介をしておきます。

文字列と数値。

{
    my $number = 10;
    my $name = 'Tarou';
    my $single_quoted_string = 'シングルクォーテーションの String は $name としても変数の値を展開しません';
    my $double_quoted_string = "ダブルクォーテーションは $name や ${name} とすることで変数の値を展開します";
}

リスト。そして配列とハッシュ。

{
    # (1, 2, 3) や qw( dog cat ) といったリテラルでリストを生成します。
    # リストを @ のシジルをつけた変数へ入れると配列となり、
    # % のシジルをつけた変数へ入れるとハッシュとなります。
    # $array[0] や $hash{key} で要素へアクセスします。
    my @array = ('l', 'i', 's', 't');
    my %hash = ( key => 'value', another_key => 'value' );
}

配列やハッシュへのリファレンス。

{
    # [ ] や { } のリテラルでは無名の配列やハッシュをつくり、
    # $ をつけた変数でその参照を保持します。
    # $array_reference->[0] や $hash_reference->{key} で要素へアクセスします。
    my $array_reference = ['l', 'i', 's', 't'];
    my $hash_reference = { key => 'value', another_key => 'value' };
}

リファレンスと参照渡し。サブルーチンでの引数の受取り。

{
    # リファレンスを渡すことで参照渡しができます
    call_function( $array_reference );  # 変数の値がリファレンスであればそのまま渡すだけです
    call_function( \@array );  # \ をつけることでリファレンスをつくります

    # サブルーチンの引数は @_ へ配列としてセットされます。
    # shift で前から一つずつ受け取るか @_ で配列として受けるのが通例です。
    sub method_name {
        my (@args) = @_;
        print Data::Dumper::dump(@args);
    }
}

Rubyist には馴染みのある見た目ですね。見た目が同じでも意味合いが違うものがあるので注意が必要です。

まずはコードから。

それでは Perl の OOP の紹介を始めます。実装されたコードに勝るサンプルはないということで、お手本を読むところから始めましょう。なにかを学びたいと思ったときに、一流の実装を読むというのはなにより一人での学習を助けてくれます。

Perl できれいに実装された OOP のコードを見るなら Plack がおすすめです。まずは全体のディレクトリ構成や名前付けを眺め、トリッキーなコードの少ない Plack::Request へと読み進めるのがよいでしょう。

クラス宣言

まずはクラス宣言をしなければ始まりません。クラスの宣言は package を使って行います。

package Plack::Request;
use strict;
use warnings;

use strictuse warnings は Perl モジュールのおまじないなので、初めてみる方は別途調べてください。

結果としてクラス宣言として働きますが、これは単なるモジュール化の手段としての package 文です。package として扱われるスコープなども通常の package 文の利用と変わりありません。

コンストラクタ

ただの package 文がクラスとして働くためには、オブジェクト生成のサブルーチンが必要です。Perl でのコンストラクタは new という名前のサブルーチンを作成します。

sub new {
    my($class, $env) = @_;
    Carp::croak(q{$env is required})
        unless defined $env && ref($env) eq 'HASH';

    bless { env => $env }, $class;
}

パッケージ名に対して Package::Name->method(@args) でメソッド呼出しをすると、Package::Name::method($class_name, @args) という関数呼出しとして解釈され、my($class, $env) = @_; のように、第一引数としてクラス名 (package 名) を受け取ります。

オブジェクト生成のトリックは bless { env => $env}, $class; にあります。

{ env => $env } というハッシュリファレンスを $class を引数に bless することで、$class パッケージのサブルーチンを呼び出すことができるリファレンスができあがります。

こうしてできたハッシュリファレンスはこのように使えます。

my $req = Plack::Request->new();
print $req->remote_host();

$req は Plack::Request のインスタンスとして振る舞いますが、実際は new サブルーチンの結果として返る bless されたハッシュリファレンスです。new を使うのは慣例であり、Perl のシンタクスではありません。

ここでは、Package::Name->sub_name(@args) 1という呼び出しをするとパッケージ名が暗黙の第一引数としてわたされる、というのが Perl のシンタクスとしての機能です (Package::Name::sub_name(@args) でも静的なサブルーチンとしての呼び出しは可能です。ただし、この場合は継承木はたどらず、静的なパッケージ関数の呼出しとして解釈され sub_name() の引数もパッケージ名はわたされません)。

コンストラクタの中でインスタンス自身を操作したいときは、このようにも書けます。便宜上コンストラクタと呼びましたが、実際はたんなる Factory Method なので、最後に bless されたハッシュリファレンスを return してやればいいだけです。

sub new {
    my $class = shift;
    my $self = {};
    bless $self, $class;
    $self->do_something();
    return $self;
}

インスタンスメソッド

メソッドはパッケージのサブルーチンとして定義します。インスタンス (bless されたハッシュリファレンス) から $object->method() の形式で呼び出されるとインスタンスメソッドとして振る舞います。

sub headers {
    my $self = shift;
    if (!defined $self->{headers}) {
        my $env = $self->env;
        $self->{headers} = HTTP::Headers->new(
            map {
                (my $field = $_) =~ s/^HTTPS?_//;
                ( $field => $env->{$_} );
            }
                grep { /^(?:HTTP|CONTENT)/i } keys %$env
            );
    }
    $self->{headers};
}

インスタンスから -> により呼び出されたサブルーチン (インスタンスメソッド) は第一引数に自身を表すリファレンスを受け取ります。慣例として $self と名付けて受け取ります。

private メソッド

private なメソッドには、sub _private_method というような命名をします。言語の機能としてアクセシビリティの制御をするわけではなく、紳士協定として外向けの API として公開しませんよという意思表示をする慣例です。

不安に思うかもしれませんが、Ruby の private メソッドもシンタクスはありますが迂遠な呼び方をすれば呼ぶことはでき、実際のところは「外から呼び出しにくくする」という設計なのでその意味で Perl の private メソッドも同じような理解でいいでしょう。

public なメソッドは sub public_method というように先頭の _ をつけません。

インスタンス変数

インスタンス変数は bless されたハッシュリファレンスの要素を使います。インスタンスメソッドで受けとるインスタンス $self は実際には bless されたハッシュリファレンスなので、ハッシュリファレンスとしてのアクセスもできます。結果として、下記のようにハッシュの要素をインスタンス変数のように扱うことができます。

$self->{middlewares} はインスタンス変数のためのシンタクスではなく、$self ハッシュリファレンスに対する要素の操作です。

Plack::Builder の例。

sub new {
    my $class = shift;
    bless { middlewares => [ ] }, $class;
}

sub add_middleware {
    my($self, $mw, @args) = @_;

    if (ref $mw ne 'CODE') {
        my $mw_class = Plack::Util::load_class($mw, 'Plack::Middleware');
        $mw = sub { $mw_class->wrap($_[0], @args) };
    }

    push @{$self->{middlewares}}, $mw;
}

クラス変数

クラス変数は Perl の package 変数です。public なものは our を使って定義します。private なものは my を使って定義します (package はブロックスコープをつくらないので一つのファイルに複数 package を定義しているようなときは注意)。

クラスメソッド

クラスメソッドは package のサブルーチンとして定義し、{PackageName}->{sub}() で呼び出します。このときサブルーチンの第一引数としてパッケージ名が渡されます。つまり、コンストラクタと呼んでいた new はただのクラスメソッドです。

Plack::MIME での例です。

package Plack::MIME;
use strict;

...

sub mime_type {
    my($class, $file) = @_;
    $file =~ /(\.[a-zA-Z0-9]+)$/ or return;
    $MIME_TYPES->{lc $1} || $fallback->(lc $1);
}

...

=head1 SYNOPSIS

  use Plack::MIME;

  my $mime = Plack::MIME->mime_type(".png"); # image/png

定数

OOP 以前に Perl としても諸説あり、これが正解というのはないようです。

個人的には constant プラグマを使っています。

継承

use parent を使います。Perl の継承は多重継承です。

Amon2::Web::Request での例。

package Amon2::Web::Request;
use strict;
use warnings;
use parent qw/Plack::Request/;

親クラスのメソッドを呼び出すときは SUPER:: 経由で呼び出します。

クラスメソッドの場合。

sub new {
    my ($class, $env, $context_class) = @_;
    my $self = $class->SUPER::new($env);
    if (@_==3) {
        $self->{_web_pkg} = $context_class;
    }
    return $self;
}

インスタンスメソッドの場合。

sub body_parameters {
    my ($self) = @_;
    $self->{'amon2.body_parameters'} ||= $self->_decode_parameters($self->SUPER::body_parameters());
}

use base というプラグマもありましたが、“Don’t use base.pm, use parent.pm instead!” とのことなので use parent を使いましょう。

クラス定義のサンプル

ここまで紹介した内容を元にクラス定義のサンプルを書いてみました。

{
    package Class::Name;
    use strict;
    use warnings;

    use parent qw(Parent::Class::Name);
    use constant CONST_BLANK => ' ';
    our $public_class_variable;
    my $private_class_variable;

    sub new {
        my ($class, @args) = @_;
        bless {
            a_instance_variable => 'hello',
     	     another_instance_variable => 'world',
        }, $class;
    }

    sub public_method {
        my ($self, @args) = @_;
        $self->_private_method();
    }

    sub _private_method {
        my ($self, @args) = @_;
        print $self->{a_instance_variable};
        print CONST_BLANK;
        print $self->{another_instance_variable};
    }
}

my $obj = Class::Name->new();
$obj->public_method();

言語内 DSL の実装方法

OOP とは話題がずれますが、Perl の機能のおもしろさを理解するために言語内 DSL の実装方法について見てみましょう。

ここまで見てきたように、Perl には Ruby でいうコンテキストのようなものがありません。常に $self や $class のような形で明示的にレシーバが渡されます。

Ruby では instance_eval などでコンテキストを差し替えることで、言語内 DSL を自然な形で実装できます。前提の違う Perl ではこのような言語内 DSL はどのように実現するのでしょうか?

Plack::Builder が参考になります。

まずは使う側のコードから見てみましょう。

  # in .psgi
  use Plack::Builder;

  my $app = sub { ... };

  builder {
      enable "Deflater";
      enable "Session", store => "File";
      enable "Debug", panels => [ qw(DBITrace Memory Timer) ];
      enable "+My::Plack::Middleware";
      $app;
  };

非常に自然に言語内 DSL が実現されているのがわかると思います。これだけ見ると、あたかも builder ブロックの中はコンテキストが差し替わったかのようです。さて、実装を見てみましょう。

package Plack::Builder;
use strict;
use parent qw( Exporter );
our @EXPORT = qw( builder add enable enable_if mount );

...

# DSL goes here
our $_add = our $_add_if = our $_mount = sub {
    Carp::croak("enable/mount should be called inside builder {} block");
};

sub enable         { $_add->(@_) }
sub enable_if(&$@) { $_add_if->(@_) }

sub mount {
    my $self = shift;
    if (Scalar::Util::blessed($self)) {
        $self->_mount(@_);
    }else{
        $_mount->($self, @_);
    }
}

sub builder(&) {
    my $block = shift;

    my $self = __PACKAGE__->new;

    my $mount_is_called;
    my $urlmap = Plack::App::URLMap->new;
    local $_mount = sub {
        $mount_is_called++;
        $urlmap->map(@_);
        $urlmap;
    };
    local $_add = sub {
        $self->add_middleware(@_);
    };
    local $_add_if = sub {
        $self->add_middleware_if(@_);
    };

    my $app = $block->();

    if ($mount_is_called) {
        if ($app ne $urlmap) {
            Carp::carp("WARNING: You used mount() in a builder block, but the last line (app) isn't using mount().\n" .
                       "WARNING: This causes all mount() mappings to be ignored.\n");
        } else {
            $app = $app->to_app;
        }
    }

    $app = $app->to_app if $app and Scalar::Util::blessed($app) and $app->can('to_app');

    $self->to_app($app);
}

...

=head1 SYNOPSIS

  # in .psgi
  use Plack::Builder;

  my $app = sub { ... };

  builder {
      enable "Deflater";
      enable "Session", store => "File";
      enable "Debug", panels => [ qw(DBITrace Memory Timer) ];
      enable "+My::Plack::Middleware";
      $app;
  };

  # use URLMap

  builder {
      mount "/foo" => builder {
          enable "Foo";
          $app;
      };

      mount "/bar" => $app2;
      mount "http://example.com/" => builder { $app3 };
  };

サブルーチンの宣言が、sub builder(&) となっています。これはプロトタイプと呼ばれる Perl の機能で、引数のブロックをサブルーチンリファレンスとして受け取るように構文解析器へのヒントを出しています (参考: perl5 でファンクションプロトタイプをつかっちゃいけない理由と使われる理由。)。

our $_add = our $_add_if = our $_mount = sub {
    Carp::croak("enable/mount should be called inside builder {} block");
};

sub enable         { $_add->(@_) }
sub enable_if(&$@) { $_add_if->(@_) }

$add->(@) というのは、$add 変数に入れられたコードリファレンス (今回の場合はクロージャ) へ引数 (@) をすべて委譲して呼出しています。builder ブロックの外では Carp::croak により例外が発生します。

    local $_mount = sub {
        $mount_is_called++;
        $urlmap->map(@_);
        $urlmap;
    };
    local $_add = sub {
        $self->add_middleware(@_);
    };
    local $_add_if = sub {
        $self->add_middleware_if(@_);
    };

この部分がコンテキストの切替えに相当するトリックを実現するポイントです。local は動的スコープで変数の値を一時的に書き換えます。レキシカルスコープの変数をつくる my と違い、動的スコープで変数の値を書き換える local は宣言されたブロックの中とそこから呼び出される先の処理で変数の値を書き換えます。

builder サブルーチンの中では、$_mount や $_add や $_add_if へクロージャ sub { … } のコードリファレンスが値として代入されています。これによって、サブルーチン mount、enable、enable_if が呼ばれたときの動作が一時的に変わります。

sub builder(&) {
    my $block = shift;

    ...

    my $app = $block->();

そして、$block->() として builder へ渡されたブロックが実行されます。このブロックが実行される間は動的スコープで $_mount などの変数の値が書き換わっているため、 mount や enable や enable_if の挙動が変わることになります。

言語内 DSL の実装に見る「たのしい Perl」

最後に少し Ruby と Perl のプログラミングのたのしさについて触れてみます。筆者が Perl を始めたときに感じたのは、Perl のプログラミングのたのしさでした。そして、それが身近に知っている感覚に近いという感想でした。Ruby が教えてくれた「自分が表現したいことを邪魔されない気持ちよさ」です23

今回は触れられませんでしたが、Perl のリフレクションやメタプログラミングとなると、シンボルテーブルを直接書き換えるような、さらに自由度、万能感が高く、その一方で自らの足を打ち抜き放題のものとなってきます。また、さらに深く潜ればソースフィルタのような機能も提供されています。なにかを実現しようと思ったときに、手段が多岐にわたり用意されており、さらにそれらをよりよく使うための支援モジュールも CPAN に山のように登録されています。

もう一つ、Ruby と比較したときに感じたのはその Ruby のオープンクラス以上のノーガードぶりです。筆者が Ruby を好きな大きな理由のひとつとして、「気分良く書ける魔法が掛かっていること」があります。

気分良くかけることについては、そうだろう。Rubyは「プログラマという人種のあるサブセットに共有されているある思想や習慣」に合わせて最適化されているっていうもんなー。だからそのサブセットに含まれる人に取ってRubyは最高に使い易い。finalizerあたりに象徴的だけれども、Rubyの仕様策定の方針として

  • 良く使う、望ましいプログラミングスタイルを強力にサポートする
  • あまり使わない、望ましくないプログラミングスタイルは不可能では無いけれども使いにくくする finalizerは故意に使いにくくしたそうだものね。

この点は Perl は思想の根本が違っているように感じます。”TMTOWTDI” であることが優先され、その more than one way たちの中での優劣は感じさせません。結果として、内臓が剥き出しで手段が提供され、CPAN モジュールの生存競争のなかでデファクトスタンダードとなるラッパー API が決まってきているような面があります。言語側がまず可能性を提示するので、「あまり使わない、望ましくないプログラミングスタイルは不可能では無いけれども使いにくくする」というのは、その後の生存競争後の活動によってなされている様子が見られます4

このような違いを学ぶことは、自分のプログラミング習慣や設計するライブラリの API 設計などへの学びともなり、慣れ親しんだ Ruby についても理解が深まるよいきっかけとなりました。そういった学びの機会という意味でも、Perl は楽しい教材です。

おわりに

Perl の OOP を紹介してきました。最後には言語内 DSL の Perl での実装の仕方を紹介しました。最初からオブジェクト指向プログラミング言語として作られた Ruby と違い、Perl では歴史的な経緯のなかで既存の機能をうまく活かしつつ、最小限の工夫で OOP が実現されています。言語内 DSL の実現でも見られるとおり、言語の機能を上手に使ってモダンなプログラミングを実現するパズル的なおもしろさと、やりたいことがあればなにかしらの手段で実現できる言語の強力さがあります。この記事が少しでも Perl の魅力を伝えられたのならうれしいです。

著者について

たなべすなお (Sunao Tanabe / @sunaot) 。日本 Ruby の会、Asakusa.rb 所属 (と言いつつ最近参加できてないなあ)。Rubyist Magazine 編集者。記事公開時点では Perl の会社で Perl や Ruby の仕事をしているプログラマ。sunaot@github sunaot@twitter

  1. sub_name Package::Name(@args) とも書けます。たとえば、my $req = Plack::Request->new(@args) は my $req = new Plack::Request(@args) とも書けるということで、これは共に Plack::Request::new(“Plack::Request”, @args) を呼び出しているのと同義です。 

  2. もちろん本来の時系列は逆で Perl の持っていた強力で際限のない自由を Ruby がバランスのよい API で取り入れたのでしょう。筆者の主観で Ruby -> Perl とたどったので、この感想になっています。 

  3. では Perl は夢の言語かというと、足を引っ張られているのを感じる言語仕様も当然ながらあります。一例を挙げると、スカラーコンテキスト、リストコンテキストによる挙動の違いなどは不要だと感じた言語仕様です。 

  4. 気になる方は Moo や Moose や Mouse やをキーワードに検索して、その歴史を眺めてみるとおもしろいかもしれません