Hachioji.pm 日めくりテックトーク

「Perl入学式 第3回」の復習問題, vote.plを解いてみよう!

皆様こんにちは.
「Perl入学式 in 東京 #3 補講」の待ち時間に, プロジェクターに映しながら艦これで3-2-1へ1回出撃したら, 那珂ちゃんがドロップして超爆笑したりしたpapixです.

...というわけで, 突如始まった「Hachioji.pm 日めくりテックトーク」の第一陣を務めさせて頂きます.
このテックトークは, Hachioji.pmらしく(?), 割と"何でもアリ"な感じでゆるふわにやって行きたいと思っています.

多分, 今後songmuさんがRijiの話をしたり(このブログもRijiで作られています!), ytnobodyさんがNephiaの話をしたり, mackee_wさんがヘリの話をしたり, moznionくんがXMLモジュールの話をしたり, xtetsujiさんがmod_perlの話をしたり, boolfoolがVimの話をしたり, あとはHachioji.pmは「Hachioji PHP Monster」の略なので, 総帥のuzullさんがPHPの話をしたりすると思います.

今日のネタ「vote.pl」

そんな「日めくりテックトーク」の記念すべき第1回ですが, 最初からガチなネタを投下してしまうと身構えられてしまうかもしれないので, 今回はゆるふわに, 以前開催した「Perl入学式 #3」の復習問題の「vote.pl」をネタとして取り上げてみたいと思います.

さて, 「Perl入学式 #3」では, ハッシュやリファレンスといった, Perlのキモとも言うべき部分にチャレンジしました.
復習問題の「vote.pl」も, それらの要素がゴロゴロ入ったいい問題に仕上がっています.

  1. 「自分の名前 (name)」と「好きな食べ物の配列のリファレンス (favorite_foods)」を持ったハッシュリファレンスを作成しましょう (つまり, 好きな食べ物は複数個書いてください)
  2. 同様のハッシュリファレンスを2, 3個作ってみましょう (周りの人のリアルデータを使ってハッシュリファレンスを作ると良いかもしれません)
  3. 作成した複数のハッシュリファレンスを1つの配列に格納しましょう (配列を操作する関数を使っても良いですし, 直で代入しても良いです)
  4. どんな方法でも良いので, 好きな食べ物のランキングを作って表示してみて下さい

解説

...さあ, コードを書いていきましょう!
都合のいいことに問題が4つに分かれているので, 1つずつ消化して行くことにします.

1. ハッシュリファレンスを作る

まずは, 「名前」と「好きな食べ物」というデータを持つ, ハッシュリファレンスを作る所からですね.

my $papix = {
    name           => 'papix',
    favorite_foods => [qw/ sushi niku ramen onigiri /],
};

...こんな感じのデータ構造になるはずです.

まず, my $papixでスカラ変数「papix」を宣言して, そこに{ ... }という形で, ハッシュのリファレンスを作り出して代入しています.

そのハッシュには, 「name」というキーに「papix」という要素が, 「favorite_foods」というキーに, [ ... ]という形で生成した, 好きな食べ物が格納されている配列のリファレンスが格納されています.
配列のリファレンス内の要素は, qwショートカットを使って, シンプルに表現しています.

...さて, このコードは, 次のコードの省略形と言うことができます.

my @papix_favorite_food = qw/ sushi niku ramen orinigi /;
my %papix_data = (
    name           => 'papix',
    favorite_foods => \@papix_favorite_food,
); 
my $papix = \%papix_data;

それぞれ, { ... }[ ... ]でハッシュ/配列のリファレンスを直接生成するのではなく, 一度%papix_data@papix_favorite_foodというハッシュ/配列を作っています.
ここからリファレンスを取る事で, 先ほどの例の$papixと同じデータ構造を生成しているわけです.

ただ, %papix_data@papix_favorite_foodは, $papixを生成してしまえば, それらのデータは$papixから%{$papix}だったり@{$papix->{favorite_foods}}だったりという形で参照できるので, 邪魔になってきますよね?

その為, { ... }[ ... ]といった記法で, 直接ハッシュや配列のリファレンスを生成する機能が必要になってくる訳ですね.

2. ハッシュリファレンスを複数個作る

my $macopy = {
    name => 'macopy',
    favorite_foods => [qw/ sushi niku /],
};

my $moznion = {
    name => 'moznion',
    favorite_foods => [qw/ sushi ramen /],
};

$papixと同様に, $macopy$moznionを用意しました.

3. ハッシュリファレンスを配列に格納する

ここまでに作ってきた, $papix, $macopy, $moznionという3つのハッシュリファレンスを, 配列に格納しましょう.
それぞれ個人のデータが格納されているので, 配列の名前は@peopleとします.

my @people = ($papix, $macopy, $moznion);

リファレンスはスカラなので, このようにシンプルに, 配列に格納することができます. もちろん,

my @people;
push @people, $papix;
push @people, $macopy;
push @people, $moznion;

のように書いても問題はありませんが, ちょっとタイプ数が多くて辛くなりますね.

4. 好きな食べ物のランキングを作る

...の前に, ここまで書いてきたコードを確認してみましょう.

use strict;
use warnings;

my $papix = {
    name           => 'papix',
    favorite_foods => [qw/ sushi niku ramen onigiri /],
};

my $macopy = {
    name => 'macopy',
    favorite_foods => [qw/ sushi niku /],
};

my $moznion = {
    name => 'moznion',
    favorite_foods => [qw/ sushi ramen /],
};

my @people = ($papix, $macopy, $moznion);

3人それぞれの個人情報が格納されたハッシュリファレンスを作り, それを@peopleという配列に格納しました.
ここから, この@peopleを使って, 好きな食べ物のランキングを作っていきます.

方針としては... そうですね, まず%rankingというハッシュを作って, そこにキーを「食べ物の名前」, 要素を「その食べ物が好きな人の数」という形で格納していきましょう.
これで, ある食べ物を好きな人は何人いるか? というのが一目でわかるようになるはずです.

というわけで, まずは3人の好きな食べ物が格納された配列を取り出しましょう.
ついでに, %rankingの宣言も忘れずに.

my %ranking;
for my $person (@people) {
    my $favorite_foods = $person->{favorite_foods};
}

@peopleに格納されているハッシュリファレンスが$personに格納されながらループが回っていきます.
その為, $favorite_foodsには, $personというハッシュリファレンスの「favorite_foods」というキーが格納している, 好きな食べ物が格納された配列のリファレンスが代入されるはずです.

確認の為, Data::Dumperモジュールでダンプしてみましょう.

use Data::Dumper;

my %ranking;
for my $person (@people) {
    my $favorite_foods = $person->{favorite_foods};
    print Dumper $favorite_foods;
}

恐らく, $papix, $macopy, $moznionの順番で, それぞれに格納されている「好きな食べ物」の配列の中身が出力されるはずです.

$VAR1 = [
          'sushi',
          'niku',
          'ramen',
          'onigiri'
        ];
$VAR1 = [
          'sushi',
          'niku'
        ];
$VAR1 = [
          'sushi',
          'ramen'
        ];

あとは, $favorite_foodsから1つずつ食べ物の名前を取り出して, %rankingに格納していけばOKです.
こんな感じでしょうか.

for my $person (@people) {
    my $favorite_foods = $person->{favorite_foods};
    for my $food (@{$favorite_foods}) {
        $ranking{$food}++;
    } 
}

%rankingprint Dumper \%rakingといった感じでダンプしてみると,

$VAR1 = {
          'onigiri' => 1,
          'sushi' => 3,
          'ramen' => 2,
          'niku' => 2
        };

こんな感じになるはずです.
ちゃんと, 食べ物の名前がキー, その食べ物を好きな人の数が要素になったハッシュが生成されていますね!

ところで...

さっきあっさりと流したのですが, こう思った人はいないでしょうか? 「あれ, $ranking{$food}++ってやってるけど, 最初は$ranking{$food} = 0で初期化したり, 初期化と1票を追加する処理を合わせて$ranking{$food} = 1とか書かなきゃ駄目じゃないの?」...と.

まず, ハッシュの任意のキーに対する要素(中身)は, 初期状態だとすべてundefになっています.
これは, 次のようなワンライナーで確認できます(ワンライナーについては, 近日中に@songmuさんがこのブログで執筆してくれるそうです!!!).

$ perl -MData::Dumper -le 'my %hash; print Dumper($hash{hoge});'
$VAR1 = undef;

で, undefはPerlでは「未定義の値」を意味するのですが, これに1を足そうとすると, なんと「0」として扱われるんですね.

$ perl -le 'my $hoge = undef; $hoge++; print $hoge'
1

なので, 先ほどのように, いきなり$ranking{$food}++と書いても, うまくいく! という訳です.

リファクタリング

for my $person (@people) {
    my $favorite_foods = $person->{favorite_foods};
    for my $food (@{$favorite_foods}) {
        $ranking{$food}++;
    } 
}

筋のいい方はお気づきかもしれませんが, このコードでは$favorite_foodsを省略することができます.

for my $person (@people) {
    for my $food (@{$person->{favorite_foods}}) {
        $ranking{$food}++;
    } 
}

個人的にはこちらの方が好みですが, どちらが書きやすいか/読みやすいかは個人の感性なので, 好きな方を選ぶと良いでしょう.

更にチャレンジ!

さて, これで3人の好きな食べ物についての票数を集計することができました.
でも, どうせなら人気順(票数順)に食べ物を表示してみたいですよね.

というわけで, もう少し頑張ってみましょう.
方針としては, %votesというハッシュを用意して, 「票数」をキーとして, そのキーに対応する要素として, 「その票数を獲得した食べ物」を配列のリファレンスとして格納していく... という感じにします.

my %votes;
for my $food (keys %ranking) {
    push @{$votes{$ranking{$food}}}, $food;
}

%voteをダンプしてみると, こうなるはずです.

$VAR1 = {
          '1' => [
                   'onigiri'
                 ],
          '3' => [
                   'sushi'
                 ],
          '2' => [
                   'ramen',
                   'niku'
                 ]
        };

あとは, この%votesのキーを数値としてソートして表示すれば...

for my $vote (sort { $b <=> $a } keys %votes) {
    print "$vote:\n";
    for my $food (@{$sort->{$vote}}) {
        print "  $food\n";
    }
} 

このように, 獲得した票数順に食べ物の名前が出てくるはずです!

3:
  sushi
2:
  ramen
  niku
1:
  onigiri

まとめ

...それにしてもvote.pl, なかなか手応えのある問題ですね!
ハッシュやリファレンスという道具は, Perlでコードを書く時に必ず必要になるモノなので, 2回, 3回と反復して解いて, 是非自分ものにして下さい.

「どれだけ考えても解けない...」という人は, 一歩ずつ解いていくことを意識すると, 解けるようになるかもしれません.
極端な話, コードを1行書くごとに実行して, 構文(シンタックス)に問題がないか, 出力が正しいかどうかを確かめていけば, 時間はかかりますが, いつか確実に完成するはずです.
一気に40行, 50行とコードを書くのは楽しいですが, そこでバグが出てきた場合, それまでに書いてきた50行を虱潰しにチェックして, どこにバグがあるかを探さなくてはなりません.

なので最初は, バグを取る為に調べる必要がある行数を減らす為に, 1行1行(...は, さすがに細かいので, 5行とか10行くらいとかの単位で...)確認しながらコーディングを進めていくことをおすすめします. ある程度慣れてくると, エラーの出力から問題のあるコードが見えてくるようになるので, そうなれば一気に20行, 30行と書いていっても, 多少困らなくなると思います.

Perlという言語は, #2や#3でもお話した通り, 同じ問題を解いたとしても, ある程度いろいろな解き方が出てきます.
実行結果が同じであっても, やはり自分や他人が理解しやすいコードの方が後々助かるので, 自分の解答と他人の解答を見比べて, いいところを吸収して行くといいと思います.

「papixのコード, よくないと思う. 自分のコードが最高だ!」という方は, 是非gistやブログに掲載して頂けると嬉しいです. こちらの記事でも紹介させて頂きます.

次回予告

というわけで, 「Perl入学式 #3」の復習問題, vote.plの解答とその解説をお送りしました.
2013年のPerl入学式はあと3回, 大阪と東京で開催しますので, 皆さんと一緒にPerlを楽しめればいいな, と思っています(Perl入学式 in YAPC::Asiaもよろしくおねがいします!).

明日は, 「はてなインターン2013 第六天魔王将軍 ツールチェインギャング(反社会的ではない)見習い」こと, moznionくんに担当して頂く予定です.
お楽しみに!!!

created by
papix
created at
last modified at
2013-09-16 09:19
comments powered by Disqus