初めて Perl でコードを書く時に知っておきたかったこと

今は全然 Perl を使わなくなりましたが、Perl の会社に内定をもらってから新卒1年目までは時々 Perl を使っていました。
業務で初めて書いた Pig 関連のスクリプトが今でも現役で活躍しているとしたらゾッとします(汚すぎて)。
この辺の知識があればもっとスムーズにもうちょっとはまともなコードが書けたのかなぁと思ってまとめてみました。
アンカーが設定できなくて残念ですがアジェンダとしてはこんな感じです。

  • 環境構築
    • perlbrew の導入
    • cpanm によるモジュール管理
  • REPL
  • ドキュメントの参照方法
  • デバッグ
  • コーディングスタイル
  • モジュールの開発
    • private method について
    • 例外処理について
  • ハマりポイント

環境構築

perlbrew の導入

そこそこちゃんと Perl を使う人は perlbrew を使うんじゃないかなと思います。複数バージョンの Perl を管理するツールで、Python の virtualenv、Ruby の rbenv に相当します。

$ \curl -L http://install.perlbrew.pl | bash
$ echo 'source ~/perl5/perlbrew/etc/bashrc' >> ~/.bashrc
$ source ~/perl5/perlbrew/etc/bashrc
$ perlbrew available  # インストール可能な Perl
  perl-5.19.1
  perl-5.18.0
  perl-5.16.3
  perl-5.14.4
  perl-5.12.5
  perl-5.10.1
  perl-5.8.9
  perl-5.6.2
  perl5.005_04
  perl5.004_05
  perl5.003_07
$ perlbrew install perl-5.19.1
$ perlbrew switch perl-5.19.1
$ which perl
/Users/arabiki/perl5/perlbrew/perls/perl-5.19.1/bin/perl

上記の例では 5.19.1 をインストールしてますが、マイナーバージョン(この場合は 19)が奇数だと開発版 (unstable) らしいです!
現在の最新安定版は Perl 5.18 みたいですね。

はてブのコメントでもいただきましたが、最近は plenv に移行している人もいるみたいです。今年の YAPC::NA (北アメリカの Perl の祭典) でも紹介しているので、Ruby の rvm と rbenv の関係のように、plenv の方がメジャーになる日も来るかもしれません。今から導入するのであれば plenv を検討してみるといいかもです。

cpanm によるモジュール管理

初めて Perl を触った時はよくわからず cpan を使ってモジュールをインストールしましたが cpanm を使う方がメジャーのようです。今から Perl を使うのであれば何も考えず cpanm を使えばいいんじゃないでしょうか。
cpanm について詳しく知りたければ次のページあたりが参考になるかと思います。

perlbrew 環境で cpanm をインストールするには次のコマンドを実行します。

$ perlbrew install-cpanm
$ which cpanm
/Users/arabiki/perl5/perlbrew/bin/cpanm

利用方法については cpanm -h で確認してください。たいていの人は Examples の使い方で事足りると思います。
古いバージョンのモジュールをインストールしたい場合はここから古いバージョンのアーカイブを取得すると良いみたいです。作者の ID から辿る必要がありますが・・・。1
cf. The CPAN Frequently Asked Questions - www.cpan.org

業務で Perl を利用する場合、モジュールをインストールする権限がないけど試しに使ってみたいという場合があると思います。そんな時はホームディレクトリ以下に cpanm をインストールすると良いです。

$ cd ~/local/bin
$ curl -LO http://xrl.us/cpanm
$ chmod +x cpanm

モジュールのインストール先もホームディレクトリ以下にして、パスも通しておきます。次の例では ~/extlib をインストール先にしています。

$ echo 'export PERL_CPANM_OPT="-l $HOME/extlib"' >> ~/.bashrc
$ echo 'export PATH=$HOME/extlib/bin:$PATH' >> ~/.bashrc
$ echo 'export PERL5LIB=$HOME/extlib/lib/perl5:$PERL5LIB' >> ~/.bashrc

REPL

ちょっとした動作確認をしたい時に REPL は大変便利ですよね。最初は perlsh を使っていましたが、perl -de 0 か tidyrepl を使うようになりました。
cf. Perl5 で irb 相当のことをする方法、すなわち REPL をする方法

tidyrepl を使い出した理由はコードをコピペする際に my を削除しないといけないのが面倒だからだったと思います。
ターミナル上で使うといろんなキーが効かないんで、rlwrap 経由で使うと良いと思います。2

$ wget http://utopia.knoware.nl/~hlub/rlwrap/rlwrap-0.37.tar.gz
$ tar xf rlwrap-0.37.tar.gz
$ cd rlwrap-0.37
$ ./configure --prefix=$HOME/local  # prefix は好みに応じて調整
$ make
$ make check
$ make install
$ cpanm Eval::WithLexicals
$ rlwrap tinyrepl

はてブのコメントもいただきましたが、今だと Reply が良いみたいです。my を付けても付けなくても変数が保存されるみたいで(あと他にもいろんな機能があるみたいで)たしかに便利そうです。

ドキュメントの参照方法

ドキュメントを参照するには perldoc を使います。ネット上の情報は古い場合があるので、使い方の知らない関数やモジュールを使う場合はとりあえず perldoc で確認すると良いです。
特に特定の演算子、正規表現、特殊変数に関する情報(記号)はググるのが厳しいので、perldoc でドキュメントを開いて全文検索するのがオススメです。

$ perldoc perldoc         # perldoc の使い方
$ perldoc perlop          # Perl の演算子に関するドキュメント
$ perldoc perlre          # Perl の正規表現に関するドキュメント
$ perldoc perlvar         # Perl の特殊変数に関するドキュメント
$ perldoc Test::More      # Test::More モジュールのドキュメント
$ perldoc -f push         # 組み込み関数 push のドキュメント
$ perldoc -f -X           # -f, -d, -e などの関数のドキュメント
$ perldoc -q yesterday    # Perl で昨日の日付を取得する方法 (FAQ)
$ perldoc -m Test::More   # Test::More のソースコードを表示
$ perldoc -ml Test::More  # Test::More のソースコードのパスを表示

perlop, perlre, perlvar などは PageName ですが、PageName に何があるかは perldoc perl で確認できます。

デバッグ

デバッグオプション (-d) を付けて実行します。例えば次のコードをデバッグすることを考えます。オプションに指定した数値を全て足し合わせるスクリプトです。

package Math;
use strict;
use warnings;

our $VERSION = '0.00';

sub sum {
    my ($class, $values) = @_;
    my $val = 0;
    $val += $_ for @$values;
    return $val;
}

1;

このモジュールを利用するスクリプト sum.pl を次のように作成します。

#!/usr/bin/env perl
use strict;
use warnings;
use Math;

sub main {
    print Math->sum(@_), "\n";
}

main(@ARGV);

実行してみるとエラーになります。

$ perl sum.pl 1 2 3 4 5
Can't use string ("1") as an ARRAY ref while "strict refs" in use at Math.pm line 10.

デバッグオプション付きで実行してみます。

$ perl -d sum.pl 1 2 3 4 5

Loading DB routines from perl5db.pl version 1.40
Editor support available.

Enter h or 'h h' for help, or 'man perldebug' for more help.

main::(sum.pl:10):	main(@ARGV);
  DB<1>

デバッガの使い方を確認します。

  DB<1> h
List/search source lines:               Control script execution:
  l [ln|sub]  List source code            T           Stack trace
  - or .      List previous/current line  s [expr]    Single step [in expr]
  v [line]    View around line            n [expr]    Next, steps over subs
  f filename  View source in file         <CR/Enter>  Repeat last n or s
  /pattern/ ?patt?   Search forw/backw    r           Return from subroutine
  M           Show module versions        c [ln|sub]  Continue until position
Debugger controls:                        L           List break/watch/actions
  o [...]     Set debugger options        t [n] [expr] Toggle trace [max depth] ][trace expr]
  <[<]|{[{]|>[>] [cmd] Do pre/post-prompt b [ln|event|sub] [cnd] Set breakpoint
  ! [N|pat]   Redo a previous command     B ln|*      Delete a/all breakpoints
  H [-num]    Display last num commands   a [ln] cmd  Do cmd before line
  = [a val]   Define/list an alias        A ln|*      Delete a/all actions
  h [db_cmd]  Get help on command         w expr      Add a watch expression
  h h         Complete help page          W expr|*    Delete a/all watch exprs
  |[|]db_cmd  Send output to pager        ![!] syscmd Run cmd in a subprocess
  q or ^D     Quit                        R           Attempt a restart
Data Examination:     expr     Execute perl code, also see: s,n,t expr
  x|m expr       Evals expr in list context, dumps the result or lists methods.
  p expr         Print expression (uses script's current package).
  S [[!]pat]     List subroutine names [not] matching pattern
  V [Pk [Vars]]  List Variables in Package.  Vars can be ~pattern or !pattern.
  X [Vars]       Same as "V current_package [Vars]".  i class inheritance tree.
  y [n [Vars]]   List lexicals in higher scope <n>.  Vars same as V.
  e     Display thread id     E Display all thread ids.
For more help, type h cmd_letter, or run man perldebug for all docs.
  DB<1>

Math::sum にブレークポイントを設定してブレークポイントまで進みます。

  DB<1> b Math::sum
  DB<2> c
Math::sum(Math.pm:8):	    my ($class, $values) = @_;
  DB<2> l
8==>b	    my ($class, $values) = @_;
9:	    my $val = 0;
10:	    $val += $_ for @$values;
11:	    return $val;
12 	}
13
14:	1;
  DB<2>

ステップ実行(ステップオーバー)して、$values に何が入っているか確認してみます。

  DB<2> n
Math::sum(Math.pm:9):	    my $val = 0;
  DB<2> x $values
0  1
  DB<3>

$values は配列のリファレンスであることが前提ですが、$values は数値のスカラーになっています。
というわけで、配列のリファレンスにして処理を継続してみます。

  DB<3> x @_
0  'Math'
1  1
2  2
3  3
4  4
5  5
  DB<4> $values = [@_[1..5]]

  DB<5> x $values
0  ARRAY(0x100cc3778)
   0  1
   1  2
   2  3
   3  4
   4  5
  DB<6> c
15
Debugged program terminated.  Use q to quit or R to restart,
use o inhibit_exit to avoid stopping after program termination,
h q, h R or h o to get additional info.
  DB<6>

正常に 15 が出力されましたね。というわけで、sum.pl を次のように変更すれば良いということがわかります。3

#!/usr/bin/env perl
use strict;
use warnings;
use Math;

sub main {
    print Math->sum(\@_), "\n";
}

main(@ARGV);

再度実行してみます。

$ perl sum.pl 1 2 3 4 5
15

直りましたね。
スクリプト内に直接ブレークポイントを埋め込むこともできます。

#!/usr/bin/env perl
use strict;
use warnings;
use Math;

sub main {
    $DB::single = 1;  # ここにブレークポイントを設定
    print Math->sum(\@_), "\n";
}

main(@ARGV);

実行してみます。

$ perl -d sum.pl 1 2 3 4 5

Loading DB routines from perl5db.pl version 1.40
Editor support available.

Enter h or 'h h' for help, or 'man perldebug' for more help.

main::(sum.pl:11):	main(@ARGV);
  DB<1> c
main::main(sum.pl:8):	    print Math->sum(\@_), "\n";
  DB<1> l
8==>	    print Math->sum(\@_), "\n";
9 	}
10
11:	main(@ARGV);
  DB<1>

ちゃんと $DB::single = 1 のところで止まりましたね。

デバッガの使い方については perldoc perldebtut でチュートリアルも見てみると良いかもしれません。

コーディングスタイル

とりあえず perldoc perlstyle を参照するのがいいんじゃないかと思います。snake_case か camelCase かで言えば、後者は自分の狭い観測範囲では見かけたことがない気がします。
モジュールの命名規則(というより注意点?)はこの辺でしょうか。
PAUSE: pause_namingmodules

モジュール名は PascalCase です。全部小文字は pragma と見做されるので不可という内容をどこかで見かけた気がします。

モジュールの開発

CPAN にアップロードするか否かに関係なく CPAN 形式で開発すると良いと思います。
CPAN 形式での開発を支援するモジュールはいくつかありますが、私は id:gfx 氏に勧められた(gfx 氏作の)Dist::Maker を使っています。自分のレベルだとたぶん何を使っても一緒ですが。
客観的な意見として、(はてブコメントにあるように)最近だと Minilla や Milla が良いみたいです。

$ cpanm Dist::Maker
$ dim --help
Usage:
        # set your name and email address first
        dim config user.name 'foo bar'
        dim config user.email 'foo at example.com'
        # or import user info from $HOME/.gitconfig
        dim config --import-from-gitconfig

        # then make distributions
        dim init Foo::Bar             # creates a distribution
        dim init Foo::Bar Default     # ditto
        dim init Foo::Bar::XS XS      # using D::M::Template::XS
        dim --verbose=4 init Foo::Bar # with verbosity
        dim init --force Foo::Bar     # do `rm -rf Foo-Bar` before init
        dim init --dry-run Foo::Bar   # don't actially init the dist

        # cd and do something important
        cd Foo-Bar/

        # add new files
        dim new Baz.pm               # creates lib/Foo/Bar/Baz.pm
        dim new t/001-basic.t        # creates a test file
        dim new xs/Foo-Bar-Baz.xs XS # creates a file with D::M::Template::XS

$ dim config user.name abicky
$ dim config user.email 'abicky@example.com'

Perl 5.19 だと Data::MessagePack のテストにコケてインストールできないので、とりあえず Data::MessagePack をテストなしでインストールして再度インストールを試みると良いです。

$ # cpanm Data::MessagePack --force も可
$ cd ~/.cpanm/latest-build/Data-MessagePack-0.47/
$ make install
$ cpanm Dist::Maker

バッチ処理を行うようなスクリプトを作成するにしても、テストのことを考えるとモジュールを作成し、スクリプトからはそのモジュールを呼び出すだけにするのがオススメとのことです。(by gfx)
一番の理由はスクリプト内にテストコードも含まれると見通しが悪くなるからです。
スクリプトのテンプレートとしては次のような感じでしょうか。

#!/usr/bin/env perl
use strict;
use warnings;
use ModuleName;

eval {
    if (!ModuleName->run(@ARGV)) {
        print STDERR ModuleName->error(), "\n";
        exit 1;
    }
};
if ($@) {
    print STDERR "$@\n";
    exit 1;
}

ちなみに dim コマンドもスクリプト自体は物凄くシンプルです。

$ head $(which dim)
#!/Users/arabiki/perl5/perlbrew/perls/perl-5.19.1/bin/perl5.19.1 -w

eval 'exec /Users/arabiki/perl5/perlbrew/perls/perl-5.19.1/bin/perl5.19.1 -w -S $0 ${1+"$@"}'
    if 0; # not running under some shell
use strict;
use Dist::Maker;
Dist::Maker->run(@ARGV);
__END__

=head1 NAME

適当に Acme::Abicky というモジュールを作ってみることにします。

$ dim init Acme::Abicky
$ cd Acme-Abicky/
$ tree
.
├── Changes
├── MANIFEST
├── MANIFEST.SKIP
├── MANIFEST.SKIP.bak
├── META.yml
├── MYMETA.json
├── MYMETA.yml
├── Makefile
├── Makefile.PL
├── README
├── author
│   └── requires.cpanm
├── inc
│   └── Module
│       ├── Install
│       │   ├── AuthorTests.pm
│       │   ├── Base.pm
│       │   ├── Makefile.pm
│       │   ├── Metadata.pm
│       │   ├── Repository.pm
│       │   └── WriteAll.pm
│       └── Install.pm
├── lib
│   └── Acme
│       └── Abicky.pm
├── t
│   ├── 000_load.t
│   └── 001_basic.t
└── xt
    ├── perlcritic.t
    ├── pod.t
    ├── podcoverage.t
    ├── podspell.t
    └── podsynopsis.t

8 directories, 26 files

いろんなファイルがありますが、とりあえず Makefile.PL が Makefile などを作成するためのスクリプト、lib 以下がこのパッケージのソースコード、t 以下がテストスクリプトということだけ覚えておけばいいんじゃないかと思います。

$ perl Makefile.PL
include /path/to/Acme-Abicky/inc/Module/Install.pm
include inc/Module/Install/Metadata.pm
include inc/Module/Install/Base.pm
include inc/Module/Install/Makefile.pm
include inc/Module/Install/Repository.pm
Cannot determine repository URL
include inc/Module/Install/AuthorTests.pm
include inc/Module/Install/WriteAll.pm
Writing Makefile for Acme::Abicky
Writing MYMETA.yml and MYMETA.json
Writing META.yml
$ make test  # テストを実行(テストを追加していないのでパスする)
cp lib/Acme/Abicky.pm blib/lib/Acme/Abicky.pm
PERL_DL_NONLAZY=1 /Users/arabiki/perl5/perlbrew/perls/perl-5.19.1/bin/perl5.19.1 "-MExtUtils::Command::MM" "-e" "test_harness(0, 'inc', 'blib/lib', 'blib/arch')" t/*.t xt/*.t
t/000_load.t ...... 1/1 # Testing Acme::Abicky/0.01
t/000_load.t ...... ok
t/001_basic.t ..... ok
xt/perlcritic.t ... ok
xt/pod.t .......... skipped: Test::Pod 1.14 required for testing POD
xt/podcoverage.t .. ok
xt/podspell.t ..... skipped: Test::Spelling is not available.
xt/podsynopsis.t .. skipped: Test::Synopsis required for testing SYNOPSIS
All tests successful.
Files=7, Tests=4,  1 wallclock secs ( 0.06 usr  0.02 sys +  1.09 cusr  0.13 csys =  1.30 CPU)
Result: PASS
$ prove -Ilib t  # lib 以下のファイルに対して t 以下のテストを実行するだけならこれも可
t/000_load.t ... 1/1 # Testing Acme::Abicky/0.01
t/000_load.t ... ok
t/001_basic.t .. ok
All tests successful.
Files=2, Tests=2,  0 wallclock secs ( 0.04 usr  0.01 sys +  0.06 cusr  0.01 csys =  0.12 CPU)
Result: PASS

テストに関してはこの辺の連載が参考になりそうです。
第1回 Perlにおけるテストの概要/TAPとは?:Happy Testing Perl|gihyo.jp … 技術評論社

テストも含めて適当にモジュールを書こうかと思ったんですが、CPAN にアップロードされてるモジュールの中身を覗く方がよっぽどためになるんで、いくつかモジュールを作ってみて失敗したなぁと思った点を2つ程。

private method について

Moose(起動に時間がかかる等の理由から、最近は Mouse や Moo が良く使われているそうです)を使うとキレイに解決できるのかもしれませんが、Perl で private method を実現しようと思うと次のようにローカル変数にコードリファレンスを代入することで可能というブログエントリーをちらほら見かけます。
が、個人的にはオススメしません。

package Acme::Abicky;
use strict;
use warnings;

our $VERSION = '0.01';

sub new {
    my $class = shift;
    my $self = {};
    bless $self, $class;
    # $self に対してごにょごにょ
    # ...
    return $self;
}

my $_private_method = sub {
    print "_private_method\n";
};

sub public_method {
    my $self = shift;
    print "public_method\n";
    $self->$_private_method();
}

1;
$ perl -Ilib -MAcme::Abicky -e 'Acme::Abicky->new->public_method()'
public_method
_private_method

っで、こうして定義した private method のテストはどうするの?って感じですよね。というわけで、今モジュールを作成する場合は命名規則で public と private を分けています。

sub _private_method {
    print "_private_method\n";
}

sub public_method {
    my $self = shift;
    print "public_method\n";
    $self->_private_method();
}

例外処理について

当初はなんでもかんでも Carp::croak を使っていました。
でも croak はメソッドの使い方が間違っているとかのレベルの誤りを(俺は悪くない!お前の使い方が悪い!と)指摘するもので、ファイルが存在しない、接続に失敗したといったコードを書いている人に罪のないエラーは undef を返して何らかの形でエラーメッセージを取得できるようにするのが Perl の作法なのかなと思います。

イメージとしては以下のような感じでしょうか。

my $err_msg;

sub connect {
    my ($class, $host, $port) = @_;

    if (!$class->_is_valid($host, $port)) {
        croak('invalid arguments');
    }
    my $con = $class->_connect($host, $port);
    if (!$con) {
        $err_msg = "can't connect to $host:$port";
        return;
    }
    return $con;
}

sub error {
    return $err_msg;
}

上記の例では想定内の例外しか扱えてないですが、想定外の例外も扱う場合は eval で括ってエラーメッセージの有無を確認します。(Perl には try-catch がないので)

eval {
    # 何らかの処理
};
if ($@) {
    $err_msg = $@;
    return;
}

例外処理を記述するためのモジュールに関してはこちらの資料が非常に詳しいそうです。
ついに顕在化しはじめた Exception リスク

ハマりどころ

以下、自分が今までハマった内容です。他にもある気がしますが・・・。
とりあえず括弧はあまり省略しない方が良いと思います。

$ tinyrepl
re.pl$ use Time::Piece; use Time::Seconds;
re.pl$ localtime->date;  # 今日の日付
"2013-07-15"
re.pl$ (localtime - ONE_DAY)->date;  # 昨日の日付のつもりが・・・
"1969-12-31"
re.pl$ (localtime() - ONE_DAY)->date;  # localtime(-ONE_DAY)->date になっていた
"2013-07-14"
re.pl$
re.pl$
re.pl$ print (1 * 0) || 1;  # print((1 * 0) || 1) のつもりが・・・
01
re.pl$ print ((1 * 0) || 1); # (print((1 * 0)) || 1 になっていた
11
re.pl$
re.pl$
re.pl$ my $undef;
undef
re.pl$ print "foo" if not defined $undef || 1;  # ((not defined $undef) || 1) のつもりが・・・
""
re.pl$ print "foo" if (not defined $undef) || 1;  # (not (defined $undef || 1)) になっていた
foo1
re.pl$ print "foo" if ! defined $undef || 1;  # and,or,not と &&,||,! は混ぜるな危険
foo1
re.pl$
re.pl$
re.pl$ my $obj;
undef
re.pl$ $obj->{a};
undef
re.pl$ $obj  # hash reference になった!(Autovivification)
{}
re.pl$
re.pl$
re.pl$ my @arr = 1..5;
1
2
3
4
5
re.pl$ $_ *= $_ foreach (@arr);
""
re.pl$ @arr  # 元の配列の内容が変わった!
1
4
9
16
25

こちらも参考にどうぞ。
バグだらけのPerlサンプルコード - あらびき日記

こんなところでしょうか。
構文とかの内容は誰でも必ず勉強すると思うんで、リャマ本やアルパカ本で基礎を勉強するといいと思います。

id:moznion さんには、はてブコメントとコメントにてたくさんのご指摘をいただきました。ありがとうございます!

  1. http://www.cpan.org/authors/id/ との違いがよくわかりませんが、こちらだと作者が削除したものでも取得できるんでしょうか? 

  2. 私は普段 Emacs の shell 上から使うんで問題ないですが 

  3. Math::sum が配列を受け取るように修正しても良いですが