# Perl基礎 - ゴール: Perlの基礎をおさらいしつつ, ハマりがちなところを学ぶ - 参考: https://github.com/hatena/Hatena-Textbook/blob/master/foundation-of-programming-perl.md ## 参考資料 - Perlを基礎から振り返りたい - [Perl入学式](http://www.perl-entrance.org/)の講義資料 - [第2回](https://github.com/perl-entrance-org/workshop-2015-02/blob/master/slide.md) - [第3回](https://github.com/perl-entrance-org/workshop-2015-03/blob/master/slide.md) - [第4回](https://github.com/perl-entrance-org/workshop-2015-04/blob/master/slide.md) - [MetaCPAN](https://metacpan.org/) - モジュールの検索, ドキュメントの閲覧 - [Perldoc.jp](http://perldoc.jp/) - Perlのドキュメントの日本語訳 ## `use strict`と`use warnings` ```perl use strict; use warnings; ``` コードの冒頭に必ず書くようにしましょう. 次のような効果があります. - `use srtict`は好ましくない構文をエラーにしてくれます - `use warnings`は好ましくない構文に対して警告を行ってくれます(コードは実行される) なお, これ以降, いくつかのPerlコードの例が登場しますが, これらは基本的に`use strict`及び`use warnings`が記述されているものとして扱って下さい. ## 変数の宣言 〜`my`と`our`〜 ```perl use strict; $var = 1; print "$var\n"; ``` `use strict`が有効な場合, 上記のようなコードを実行すると次のようなエラーになります. ``` Global symbol "$var" requires explicit package name at strict.pl line 3. Global symbol "$var" requires explicit package name at strict.pl line 4. Execution of strict.pl aborted due to compilation errors. ``` `use strict`が有効な場合, 次の変数宣言時に`my`ないし`our`を付与する必要があります. ```perl use strict; my $var = 1; print "$var\n"; ``` `my`で宣言された変数はレキシカル変数とも呼び, その変数を宣言したスコープのみでしか利用することができません(スコープについては後述します). `our`で宣言された変数はパッケージ変数とも呼び, どこからでも(スコープ外からでも)呼び出すことができます. いわゆる"グローバル変数"のようなものです. どうしてもパッケージ変数として宣言しなければならない場合を除いて, `my`を使って変数を宣言するようにしましょう. ## 「数値の比較」と「文字列の比較」 ## 文字コードと`use utf8` Perlでは, 全ての文字列は「Perl内部表現」と「バイト列」のどちらかで表現されます. 結論から述べると, Perlにおいて全ての文字列は次のように扱うべきです: - 「バイト列」を受け取ったら, 「Perl内部表現」に変更してから処理を行う - `print`などで文字列を出力する場合, 「Perl内部表現」を「バイト列」にしてから行う ### 文字列を「バイト列」で扱った場合の不都合 文字列に対して, `length`を適用するとその文字列の長さを返します. 次のコードをUTF8で保存して実行すると, どうなるでしょうか? ```perl my $str = 'わいわい'; print length $str; ``` 実行すると, 予想に反して「12」が出力されるはずです. これは, 「わいわい」という文字列がUTF8による「バイト列」となっており, UTF8において「わいわい」という文字列は12バイトになるので, この「12」が`length`の返り値になっているからです(「12」になるのは, 文字コードがUTF8になっているからなので, 他の文字コードでファイルを保存した場合, 結果は異なります). ### `use utf8`による「Perl内部表現」への変換 そこで, この「わいわい」という文字列を「Perl内部表現」に変換するようにしましょう. 1つの方法が, `use utf8`です. use utf8`を記述すると, Perlはコードにあるマルチバイト文字をUTF8で書かれた文字列と解釈して, 自動的に「Perl内部表現」に変換してくれます(この機能が存在することもあり, PerlのコードはUTF8で記述することが一般的です). なお, `use utf8`は, UTF8以外の文字コードでファイルを保存した場合であっても, 無理矢理UTF8で解釈します. `use utf8`する場合, 文字コードは必ずUTF8で保存するようにしましょう. ```perl use utf8; my $str = 'わいわい'; print length $str; ``` このコードを実行すると, 正しい「わいわい」という文字列の長さ, つまり「4」が出力されます. ### 「Perl内部表現」と「バイト文字列」の変換 「Perl内部表現」と「バイト文字列」を変換するためのモジュールとして, Encode.pmが提供されています. このモジュールは, Perl 5.8以降からコアモジュールなので, インストールすることなく利用することができます. #### 「Perl内部表現」から「バイト文字列」 ```perl use utf8; my $str = 'わいわい'; print $str; ``` 上記のような, `use utf8`によって「Perl内部表現」に変換されている文字列をそのまま`print`などで出力すると, 次のようなエラーが出力されます. ``` Wide character in print at strict.pl line 4. わいわい ``` これは, 「Perl内部表現」の文字列を`print`などで出力する場合, 「バイト文字列」に変換しなければならないからです. この場合, Encode.pmの`encode`を利用します. 特に, 「Perl内部表現」をUTF8のバイト文字列として出力する場合は, 次のように`encode_utf8`を利用します. ``` use utf8; use Encode; my $str = 'わいわい'; print encode_utf8($str); ``` `encode_utf8`によって, 「Perl内部表現」の文字列が「バイト文字列」に変換されて出力されるので, 警告が出力されなくなります. #### 「バイト文字列」から「Perl内部表現」 引数からバイト文字列を受け取ったり, `<STDIN>`で標準入力からバイト文字列を受け取った場合, 「バイト文字列」から「Perl内部表現」への変換が必要になります. この場合, Encode.pmも`decode`を利用します. 特に, UTF8のバイト文字列を「Perl内部表現」に変換する場合, 次のように`decode_utf8`を利用します. ``` use Encode; chomp(my $str = <STDIN>); print length decode_utf8($str); ``` `decode_utf8`によって, 「Perl内部表現」の文字列を取得することができますので, 例えば標準入力から「わいわい」と入力した場合, その文字列の長さとして「4」が出力されます. ## データ型 Perlの代表的なデータ型, 「スカラ」, 「配列」, 「ハッシュ(連想配列)」について解説します. ### スカラ スカラは, 単一のデータを扱う型です. スカラの変数は, 変数名にprefixとして`$`が付きます(`$calar`と覚えるといいでしょう). スカラの変数は, 次のように任意の1つのデータを受け取ることができます. ```perl my $scalar1 = 12345; my $scalar2 = 'hello!'; ``` ### 配列 配列は, 複数のデータを格納できる型です. 配列の変数は, 変数名にprefixとして`@`が付きます(`@rray`と覚えるといいでしょう). スカラの変数は, 次のように複数のデータを格納することができます. ```perl my @array = (12345, 'hello!'); ``` 配列の任意の要素を取得したり, 変更したりするには, 次のようにします. ```perl my @array = (12345, 'hello!'); print $array[1]; # => hello! $array[2] = 'good bye!'; # @array = (12345, 'hello!', 'good bye!') ``` 配列の任意の要素は「単一のデータ」なので, `@array[1]`ではなく`$array[1]`でアクセスすることができます. なお, 配列の添字は「0」からのスタートとなります. #### 配列の操作 ##### 配列の長さ, 最後の添字, スライス `@array`という配列について, - 配列の長さは, `scalar(@array)`で取得できます - 配列の最後の添字は, `$#array`で取得できます - 配列の一部の要素(例えば, 添字が1から3までの要素)については, `@array[1, 2, 3]`で抜き出すことができます(スライス) ```perl my @array = (1, 2, 3); print scalar(@array); # => 3 print $#array; # => 2 my @slice = @array[1, 2]; @slice = (2, 3) ``` ##### `for`ループ 次のように`for`を使うことで, 配列の各要素について繰り返し処理を行うことができます. ```perl my @array = (1, 2, 3); my $sum = 0; for my $i (@array) { $sum = $sum + $i; } print $sum; # => 6 ``` ##### 要素の追加 `push`で配列の末尾に, `unshift`で配列の先頭に, それぞれ要素を追加することができます. ```perl my @array = (2, 3, 4); push @array, 5; # => @array = (2, 3, 4, 5) unshift @array, 1; # => @array = (1, 2, 3, 4, 5) ``` ##### 要素の取り出し `pop`で配列の末尾から, `shift`で配列の先頭から, それぞれ要素を抜き出すことができます. ```perl my @array = (1, 2, 3); my $pop = pop @array; # => $pop = 1, @array = (2, 3) my $shift = shift @array; # => $shift = 3, @array = (2) ``` 詳しくは「サブルーチン」の項で説明しますが, `pop`や`shift`はサブルーチンへ渡された引数を取り出す際に活用することが多いです. ##### `map`/`grep` `map`は, 次のように配列の全要素について, ある一定の処理を適用することができます. ```perl my @array = (1, 2, 3); my @array2 = map { $_ + 1 } @array; # => @array2 = (2, 3, 4) ``` 上記のコードでは, `@array`の全要素について, `$_ + 1`を適用したものを`@array2`へ代入しています. また, `grep`は配列の全要素について, 指定した条件を満たすものだけ抜き出すことができます. ```perl my @array = (1, 2, 3); my @array2 = grep { $_ > 1 } @array; # => @array2 = (2, 3) ``` 上記のコードでは, `@array`の全要素について, `$_ > 1`を満たすものだけを`@array2`へ代入しています. ##### `join`/`sort` `join`は, 配列の全要素を指定した文字列で連結することができます. ```perl my @array = (1, 2, 3); print join('-', @array); # => 1-2-3 ``` また, `sort`は配列の全要素を指定した条件でソートすることができます. ```perl my @array = (2, 1, 3); my @sorted = sort { $a <=> $b } @array; # @sorted => (1, 2, 3) ``` なお, `sort`において`$a`及び`$b`という変数名を使うので, 混乱を防ぐため, コードの中では`$a`及び`$b`という変数は使わないようにしましょう. ##### 配列を操作するモジュール 配列の操作を支援するモジュールとして, 以下のモジュールが提供されています. - [List::Util](https://metacpan.org/pod/List::Util) (コアモジュール) - [List::MoreUtils](https://metacpan.org/pod/List::MoreUtils) - [List::UtilsBy](https://metacpan.org/pod/List::UtilsBy) ### ハッシュ(連想配列) ハッシュは, 配列と同じく複数のデータを格納できる型です. 連想配列とも呼びます. 配列との違いは, 配列はデータに添字でアクセスしましたが, 連想配列では任意のキーでアクセスできる点です. ハッシュの変数は, 変数名にprefixとして`%`が付きます(強引ですが, `%ash`と覚えるといいでしょう. 目を細めて`%`を見ると`H`に見えてくるでしょう...?). ```perl my %hash = ( tokyo => 'tokyo', kanagawa => 'yokohama', ); ``` なお, `=>`はファットカンマと呼びます. 単なるカンマ(`,`)と同じですが, ファットカンマの左側は文字列を`'`で囲う必要がありません. ファットカンマを利用することで, ハッシュの宣言をする際, キーと値の関係(`kanagawa => 'yokohama'`におけるキー`kanagawa`と値`yokohama`など)の関係をわかりやすく示すことができます. ハッシュの任意の要素を取得したり, 変更したりするには, 次のようにします. ```perl my %hash = ( tokyo => 'tokyo', kanagawa => 'yokohama', ); print $hash{kanagawa}; # => yokohama $array{chiba} = 'chiba'; # %hash = (tokyo => 'tokyo', kanagawa => 'yokohama', chiba => 'chiba') ``` ハッシュの任意の要素は「単一のデータ」なので, 配列と同じく`%hash{kanagawa}`ではなく`$hash{kanagawa}`でアクセスすることができます. #### ハッシュ(連想配列)の操作 ##### `keys` `keys`で, ハッシュに含まれる全てのキーを取得することができます. 注意点として, ハッシュのデータ構造の都合上, `keys`で取得できるキーの順序は順不同になっているので, 必要であれば`sort`などで並べ替える必要があります. `keys`を利用すると, ハッシュに格納された各要素について繰り返し処理を行うことができます. ```perl my %hash = ( tokyo => 'tokyo', kanagawa => 'yokohama', ); for my $pref (keys %hash) { print $hash{$pref}; } ``` ##### `values` `values`は, `keys`の逆で, ハッシュに含まれる全ての値を取得することができます. ##### `delete`/`exists` `exists`は, ハッシュに任意のキーに対応する値が存在するか確認することができます. また, `delete`はハッシュから任意のキーとそれに対応する値を削除することができます. ```perl my %hash = ( tokyo => 'tokyo', kanagawa => 'yokohama', ); print exists $hash{tokyo} ? 'exists' : 'not exists'; # => exists delete $hash{tokyo}; print exists $hash{tokyo} ? 'exists' : 'not exists'; # => not exists ``` ## コンテキスト Perlには「リストコンテキスト」と「スカラコンテキスト」が存在し, このコンテキストによって結果が変わることがあります. コンテキストの違いによって, 結果が変わる例を見ていきましょう. ```perl my @array = (1, 2, 3); my $scalar_context = @array; # スカラコンテキスト my ($list_context) = @array; # リストコンテキスト ``` 上記のコードは, 配列`@array`をそれぞれ「スカラコンテキスト」と「リストコンテキスト」で代入しているコードです. スカラコンテキストで配列を代入しようとすると, 予想に反して「配列の要素数(長さ)」が代入されます. そのため, `$scalar_context`には「3」が代入されることになります. 配列の要素をスカラ変数に代入したい場合, リストコンテキストで代入する必要があります. 上記のコードでは, `$list_context`には`@array`の1つ目の要素である「1」が代入されます. これを利用して, 配列に格納されたデータをそれぞれ別のスカラ変数に代入することができます. ```perl my @array = (1, 2, 3); my ($first, $second, $third) = @array; # $first = 1, $second = 2, $third = 3 ``` ## リファレンスとデリファレンス 「リファレンス」は, これまで見てきたスカラ, 配列, ハッシュといったデータへの「参照」です. Perlで複雑なデータ構造を作る場合, 「リファレンス」をうまく扱っていく必要があります. 「リファレンス」に対して, その参照先のデータを取得することを「デリファレンス」と呼びます. ```perl # スカラとリファレンス my $scalar = '123'; my $scalar_ref = \$scalar; # スカラのリファレンスの取得 print ${ $scalar_ref }; # スカラのリファレンスのデリファレンス # 配列とリファレンス my @array = (1, 2, 3); my $array_ref1 = \@array; my $array_ref2 = [ 1, 2, 3 ]; print $array_ref1->[0]; # 配列のリファレンスを使った要素へのアクセス my @dereferenced_array = @{ $array_ref1 }; # 配列のリファレンスのデリファレンス # ハッシュとリファレンス my %hash = (hoge => 'fuga'); my $hash_ref1 = \%hash; my $hash_ref2 = { hoge => 'fuga' }; print $hash_ref1->{hoge}; # ハッシュのリファレンスを使った要素へのアクセス my %dereferenced_hash = %{ $hash_ref1 }; # ハッシュのリファレンスのデリファレンス ``` ### リファレンスを使った複雑なデータ構造とその操作 リファレンスを使うことで, 例えば次のような複雑なデータ構造も表現することができます. ```perl my $shinagawa = { operated_by => { jr_east => { line => ['yamanote line', 'keihin-tohoku line', 'tokaido main line', 'ueno-tokyo line', 'yokosuka line'], }, jr_central => { line => ['tokaido shinkansen'], }, keikyu => { line => ['keikyu main line'], station_code => 'KK01', }, }, location => '3 Takanawa, Minato, Tokyo Japan', opened => 1872, }; ``` また, このデータ構造は次のように操作することができます. 配列やハッシュのリファレンスは, 適切にデリファレンスすることで, 配列やハッシュを操作する関数を適用することができます. ```perl # 任意の要素へのアクセス print $shinagawa->{operated_by}->{keikyu}->{station_code}; # => KK01 # 配列のリファレンスをデリファレンスして`for`ループで処理 for my $line (@{ $shinagawa->{operated_by}->{jr_east}->{line} }) { print $line; } ``` ## サブルーチン コードを適切にサブルーチンに切り分けることで, コードの見通しを良くすることができます. サブルーチンは次のように定義して, 利用することができます. ```perl sub subroutine { print "hello!"; } subroutine(); subroutine; # 定義後であれば, 括弧を省略することもできる ``` ### サブルーチンと引数の受け渡し サブルーチンに渡された引数については, サブルーチン内では`@_`という配列に格納されており, これを使って渡された引数にアクセスすることができます. サブルーチンにおいて, 引数の受け取りは次のように行うことが多いです. ```perl sub subroutine { my ($arg1, $arg2, $arg3) = @_; print $arg1 + $arg2 + $arg3; } subroutine(1, 2, 3); ``` 他にも, `shift`を利用するパターン, 配列やハッシュで受けるパターン, そしてそれらの複合パターンもあります. ```perl # shiftを利用するパターン sub subroutine { # サブルーチンの中で操作対象を指定せず`shift`した場合, `@_`に対して操作が行われる my $arg1 = shift; my $arg2 = shift; my $arg3 = shift; print $arg1 + $arg2 + $arg3; } subroutine(1, 2, 3); ``` ```perl # 配列やハッシュで受けるパターン sub subroutine { my @args = @_; print $args[0] + $args[1] + $args[2]; } subroutine(1, 2, 3); ``` ```perl # 複合パターン sub subroutine { my $arg1 = shift; my @args = @_; print $arg1 + $args[0] + $args[1]; } subroutine(1, 2, 3); ``` ### サブルーチンの返り値 サブルーチンは, `return`を使って値を返すことが可能です. サブルーチンの中で, 複数の`return`を記述することもできます. サブルーチンは処理が`return`に至ったタイミングで返り値を返して処理を終了します. ```perl sub div { my ($x, $y) = @_; if ($y == 0) { return 'nan'; # 0で割れない } else { return $x / $y; } } print div(0, 3); # => nan print div(3, 3); # => 1 ``` なお, `return`は省略することも可能です. 省略した場合, サブルーチンは最後に評価した内容を返り値とします. ```perl sub sum { my ($x, $y) = @_; $x + $y; # `return`がないので, 最後に評価した内容, つまり`$x + $y`の結果を返す } print sum(1, 3); # => 4 ``` サブルーチンは, 次のように複数の値を返すことも可能です. ```perl sub calc { my ($x, $y) = @_; return ($x + $y, $x - $y); } my ($sum, $sub) = calc(1, 2); ``` ### サブルーチンとリファレンス サブルーチンに配列などを渡す場合, リファレンスを使って渡す方が好ましいです. ```perl sub subroutine1 { my @array = @_; ... } sub subroutine2 { my $array_ref = shift; my @array = @{ $array_ref }; ... } subroutine1(1..100); subroutine2([1...100]); ``` `subroutine1`のように, 配列などを直接渡した場合, そのデータのコピーが発生します. 一方, `subroutine2`のようにリファレンスで渡す場合, リファレンスは単なるデータの「参照」なので, データのコピーは発生しません. 但し, リファレンスで渡した場合, 次のような参照先のデータの予想しない破壊が起こる場合があるので, 気をつけましょう. ```perl sub subroutine { my $array_ref = shift; ... $array_ref->[0] = $array_ref->[0] + 1; ... } my $array_ref = [1, 2, 3]; subroutine($array_ref); # => $array_ref = [2, 2, 3] ``` ## 無名サブルーチン Perlは, 名前を持たない「無名サブルーチン」をサポートしています. ```perl sub { ... } ``` 「無名サブルーチン」は, 名前を持たないサブルーチンを定義して, そのサブルーチンへのリファレンス(ここまでにスカラ, 配列, ハッシュのリファレンスについて説明してきましたが, サブルーチンのリファレンスを作ることもできるのです)を返します. ```perl my $sub_ref = sub { ... }; ``` このようにして生成した「無名サブルーチン」は, 次のようにして実行することができます. ```perl $sub_ref->(); ``` 普通のサブルーチンと同じく, 引数を渡すこともできます. ```perl $sub_ref->(1, 2, 3); ``` ## スコープ `{`と`}`で囲むことで, スコープを生成することができます. 前述の通り, `my`で変数を宣言した場合, 変数はそのスコープの中でのみ参照することができます. ``` my $hoge; sub subroutine { my $foo; # (1) } { my $fuga; # (2) if (...) { my $piyo; # (3) } } # (4) ``` 上記の(1)〜(4)の位置において, 参照できる変数/できない変数はどれでしょうか? - (1) - `$hoge`と`$foo`が参照できます - (2) - `$hoge`と`$fuga`が参照できます - (3) - `$hoge`, `$fuga`, `$piyo`が参照できます - (4) - `$hoge`のみ参照できます スコープを適切に指定することで, 変数を参照できる範囲を適切に設定することができます. ## CPANとモジュール PerlにはCPANというモジュール管理システムがあり, 全世界のPerlユーザが「CPANモジュール」という形でライブラリを提供してくれています. Perlを問わず, JavaScriptやRubyなど, 各種プログラミング言語で開発するのであれば, このような「パッケージ管理システム」と「ライブラリ」をうまく活用するとよいでしょう. CPANモジュールには, 「コアモジュール」とそうでないモジュールがあります. コアモジュールはPerlにデフォルトで付属しており, 特別にインストールせずに利用可能なモジュールです. そうでないCPANモジュールは別途インストールしなければなりません. CPANモジュールをインストールする方法としては, `cpanm`コマンドを利用することを推奨します. 昔は`cpan`コマンドを利用することが一般的ですが, 最近はこれよりも便利で軽量な`cpanm`コマンドが登場しています. `plenv`でPerlを管理している場合, `plenv install-cpanm`で`cpanm`をインストールすることができます. ### CPANモジュールのインストール 日本語を含む文字列の幅(例えば`あ`なら幅2, `A`は幅1, など)を求める, [Text::VisualWidth](https://metacpan.org/pod/Text::VisualWidth)というモジュールをインストールしてみましょう. ``` $ cpanm Text::VisualWidth ``` インストールに失敗した場合, `~/.cpanm/latest-build/build.log`にログが残るので, 参考にしてみると良いでしょう. ### CPANモジュールの利用 インストールしたCPANモジュールは, 次のように`use`を利用して呼び出すことができます. ```perl use Text::VisualWidth; ``` ### CPANモジュールのドキュメントの参照 モジュールの利用方法など調べるため, モジュールのドキュメントをターミナル上から参照したい場合, `perldoc`コマンドを使いましょう. ```perl $ perldoc Text::VisualWidth ``` ブラウザで, MetaCPANなどのサイトから参照するのも良いでしょう. ## 練習問題 (20分) 1から100までの数値について, FizzBuzzを表示しましょう. なお, 適宜モジュールをインストールして利用しても構いません. - FizzBuzzのルール - 数値が15で割れるなら: FizzBuzz - 数値が3で割れるなら: Fizz - 数値が5で割れるなら: Buzz - それ以外なら: 数値をそのまま表示 ## デバッグ 大抵のバグは, デバッガを使うまでもなく, いわゆる「printデバッグ」で解決できることが多いです. ここでは, 「printデバッグ」に有用なモジュールの紹介をします. ### Data::Dumper (コアモジュール) Data::Dumperは, 任意の変数の中身をダンプして表示してくれるモジュールです. 次のデータ構造を, Data::Dumperでダンプしてみます. ``` use Data::Dumper; my $shinagawa = { operated_by => { jr_east => { line => ['yamanote line', 'keihin-tohoku line', 'tokaido main line', 'ueno-tokyo line', 'yokosuka line'], }, jr_central => { line => ['tokaido shinkansen'], }, keikyu => { line => ['keikyu main line'], station_code => 'KK01', }, }, location => '3 Takanawa, Minato, Tokyo Japan', opened => 1872, }; print Dumper $shinagawa; ``` 実行結果は次のようになります. ``` $VAR1 = { 'location' => '3 Takanawa, Minato, Tokyo Japan', 'operated_by' => { 'keikyu' => { 'line' => [ 'keikyu main line' ], 'station_code' => 'KK01' }, 'jr_east' => { 'line' => [ 'yamanote line', 'keihin-tohoku line', 'tokaido main line', 'ueno-tokyo line', 'yokosuka line' ] }, 'jr_central' => { 'line' => [ 'tokaido shinkansen' ] } }, 'opened' => 1872 }; ``` 複数のリファレンスが入り組んだ複雑なデータの中身とその構造を, 簡単に確認することができます. ### Data::Printer Data::Dumperとは異なり, 別途インストールする必要がありますが, Data::Printerも「printデバッグ」に有用です. ``` use DDP; # Data::Printerのショートカット. 「p」が利用できるようになる my $shinagawa = { operated_by => { jr_east => { line => ['yamanote line', 'keihin-tohoku line', 'tokaido main line', 'ueno-tokyo line', 'yokosuka line'], }, jr_central => { line => ['tokaido shinkansen'], }, keikyu => { line => ['keikyu main line'], station_code => 'KK01', }, }, location => '3 Takanawa, Minato, Tokyo Japan', opened => 1872, }; p $shinagawa; ``` 実行すると, 次のような出力を得ることができます. ``` \ { location "3 Takanawa, Minato, Tokyo Japan", opened 1872, operated_by { jr_central { line [ [0] "tokaido shinkansen" ] }, jr_east { line [ [0] "yamanote line", [1] "keihin-tohoku line", [2] "tokaido main line", [3] "ueno-tokyo line", [4] "yokosuka line" ] }, keikyu { line [ [0] "keikyu main line" ], station_code "KK01" } } } ``` ## Perlのオブジェクト指向 -> https://github.com/perl-entrance-org/Perl-Entrance-Textbook/blob/master/oop.md ### 継承 任意のパッケージを継承したい場合, `use parent`を利用します. ```perl # 「Human」パッケージを継承して「Engineer」パッケージをつくる package Engineer; use parent 'Human'; ... ``` ### カプセル化 Perlのオブジェクト指向において, メソッドのprivate/publicは区別できない(全てpublic)ため, 命名規則でprivate/publicであることを示すことが多いです. 具体的には, privateなメソッドについては, prefixとして`_`を付与することが多いです. ``` sub public { ... } sub _private { ... } ``` ### クラスビルダー CPANには, オブジェクト指向プログラミングを支援するモジュールがいくつか公開されています. これらを利用することで, Perlのオブジェクト指向プログラミングで面倒な部分を, いくつか簡略化することができます. - [Class::Accessor::Lite](https://metacpan.org/pod/Class::Accessor::Lite) - [Moose](https://metacpan.org/pod/Moose) / [Moo](https://metacpan.org/pod/Moo) - [Mouse](https://metacpan.org/pod/Mouse) ## 練習問題 (2時間) ここまで長々と, Perlの基礎と詰まりどころを説明してきましたが, 退屈な講義(!?)はここまで. 後は思う存分コードを書いて下さい! ### Acme::NantokaMemoriesを作ろう! 今回は, Mobile Factoryが開発しているいわゆる"位置ゲー"である「ステーションメモリーズ!」, 通称「駅メモ」を題材に, Perlを使ったOOPに挑戦していきましょう. 今回は, **テストだけ用意している**ので, このテストに適合するようにコードを書いていきましょう. コードはGitHubで公開しています(https://github.com/papix/Acme-NantokaMemories). 手元にcloneして開発を進めましょう. ### 駅メモとは? ref: http://ekimemo.com/#sample 今回は, 「でんこ」, 「駅」, 「アイテム」という3つをPerlで表していきます. - 「でんこ」は, 思い出を集めるために, 「駅」にアクセスします - 「でんこ」は, 体力と攻撃力というステータスを持ちます - 「駅」は, 1人の「でんこ」とリンクすることができます - 「でんこ」が「駅」にアクセスしたとき, その「駅」が誰にもリンクされていない場合, アクセスした「でんこ」は自動的に「駅」とリンクします - 既に「駅」とリンクしている「でんこ」がいる場合, その「でんこ」とアクセスした「でんこ」でバトルをします - アクセスした「でんこ」がバトルに勝利した場合(既にリンクしている「でんこ」の体力を0にした場合), その「駅」とリンクする「でんこ」が入れ替わります - 「でんこ」は「アイテム」を保持することができます - 「アイテム」を使うことで, 例えば「でんこ」は体力を回復することができます ### (1) 「でんこ」オブジェクトを作れるようにしよう! - テスト: 01_denko.t - 確認方法: `prove t/01_denko.t` `Acme::NantokaMemories::Denko->new()`で, `Acme::NantokaMemories::Denko`のオブジェクトを生成できるようにしよう. ### (2) 「でんこ」オブジェクトにパラメータをもたせよう! - テスト: 02_denko_parameter.t - 確認方法: `prove t/02_denko_parameter.t` `Acme::NantokaMemories::Denko`のオブジェクトを作る際, 次のようにして初期パラメータを設定できるようにしよう. ```perl my $denko = Acme::NantokaMemories::Denko->new( name => 'しいな', hp => 84, # 体力 atk => 50, # 攻撃力 ); ``` また, オブジェクトから`name`, `hp`, `atk`のメソッドを使って, それぞれのパラメータにアクセスできるようにしよう. ### (3) 「駅」オブジェクトを作れるようにしよう! - テスト: 03_station.t - 確認方法: `prove t/03_station.t` `Acme::NantokaMemories::Station->new()`で, `Acme::NantokaMemories::Station`のオブジェクトを生成できるようにしよう. `Acme::NantokaMemories::Station`のオブジェクトは, `new`で設定可能な`name`と, 初期値が`undef`の`linked_by`というパラメータを持っているようにしよう. また, これらに`name`および`linked_by`メソッドでアクセスできるようにしよう. ### (4) 「でんこ」が「駅」にアクセスできるようにしよう! - テスト: 04_access.t - 確認方法: `prove t/04_access.t` `Acme::NantokaMemories::Station`に`access_by`メソッドを用意しよう. `access_by`メソッドは引数として, 1つの`Acme::NantokaMemories::Denko`のオブジェクトを受け取れるようにします. 「でんこ」が「駅」にアクセスした場合, その駅にリンクしているでんこ(`Acme::NantokaMemories::Station`の`linked_by`)が変わります. `linked_by`は, 今「駅」にリンクしている「でんこ」のリファレンスを持つこととします. ### (5) 「でんこ」が「駅」にアクセスした時に, バトルするようにしよう! - テスト: 05_battle.t - 確認方法: `prove t/05_battle.t` 「でんこ」が`access_by`でアクセスした際, 既にリンク済みの「でんこ」がいる場合, バトルするようにしよう. バトルのルールは次のようにします: リンク済みの「でんこ」は, アクセスした「でんこ」の攻撃力だけ, 体力が減ります 既にリンク済みの「でんこ」の体力が0以下になった場合のみ, アクセスした「でんこ」が新しくその「駅」とリンクします. また, 体力が0以下になった「でんこ」は, 体力の初期値に自動的に回復します(そのため, `Acme::NantokaMemories::Denko`のオブジェクトは, その「でんこ」の体力の最大値(初期値)を保持するようにしましょう). ### (6) おなじ「でんこ」が「駅」にアクセスした場合, バトルしないようにしよう! - テスト: 06_battle2.t - 確認方法: `prove t/06_battle2.t` (5)の実装では, ある「駅」にリンク済みの「でんこ」が, その駅に再びアクセスしようとした場合, 自分自身とバトルすることになってしまいます. そうならないように, 「駅」にリンク済みの「でんこ」とアクセスした「でんこ」が同じ場合, バトルの処理を行わないようにしましょう. ヒント: リファレンスの比較は, `==`で可能です. 2つのリファレンスが同じものを指しているのであれば真, そうでなければ偽になります. ### (7) 「でんこ」が体力を回復できるようにしよう - テスト: 07_recovery_hp.t - 確認方法: `prove t/07_recovery_hp.t` `Acme::NantokaMemories::Denko`のオブジェクトに`recovery_hp`メソッドをもたせましょう. このメソッドは, 正の整数を受け取って, その分「でんこ」の体力を回復させます. 但し, 回復後の体力が体力の初期値をオーバーした場合は, 体力の初期値になるようにします. ### (8) 「でんこ」が「アイテム」を持てるようにしよう - テスト: 8.t - 確認方法: `prove t/8.t` `Acme::NantokaMemories::Item->new`で「アイテム」のオブジェクトを作れるようにしよう. `Acme::NantokaMemories::Item`のオブジェクトは, 次のように`name`とそのアイテムを利用した時の効果`effect`を持つようにします. ```perl Acme::NantokaMemories::Item->new( name => 'バッテリー50', effect => sub { my ($denko) = @_; $denko->recovery_hp(50); }, ); ``` また, `Acme::NantokaMemories::Denko`に, `give_item`と`use_item`, `item`メソッドを持たせよう. `give_item`は引数として`Acme::NantokaMemories::Item`のオブジェクトを受け取って, 「でんこ」はそのアイテムを保持します(今回は, 簡単のために1人の「でんこ」は1つの「アイテム」だけを持てることとします. 複数回`give_item`を使った場合, 「でんこ」が持っている「アイテム」は, 常に最新のもので上書きされます. `use_item`で, 「でんこ」は「アイテム」を利用します. 使った「アイテム」は消費され, もう一度`use_item`しても使えなくなります. `item`で, でんこは現在持っている「アイテム」のオブジェクトを返します. アイテムを持っていない場合は`undef`を返すようにします. ### (9) オリジナルのアイデアで発展させていきましょう! テストを用意しているのはここまでです. 後はそれぞれのアイデアで拡張していきましょう! たとえば... - `Acme::NantokaMemories::Denko::Shiina->new()`だけで, しいなのパラメータをもったオブジェクトを生成できるようにする(`Acme::NantokaMemories::Denko`を継承して...) - 「でんこ」に, 「アイテム」を複数持たせられるようにする - アクセス時のバトルの計算式を変更する - 経験値やレベルの概念を追加する - リファクタリングしてみる などなど...