読者です 読者をやめる 読者になる 読者になる

Perl入学式in東京第3回復習問題vote.pl(再チャレンジ編)

数日ウルトラ多忙で一旦更新を休みましたが、粘って継続してみたいと思います。
#本当は昨日(12/19)に更新したかったのですが、ちょっと予定が狂って一日延びました・・

前回までにもちらちら書いていましたように、
Perl入学式in東京第3回復習問題score.pl(再チャレンジ編・その5) - draft

今日はPerl入学式2013 in東京 第3回の復習問題より、「vote.pl」という問題について再チャレンジしたいと思います。

お題はこちら。
workshop-2013-03/practice.md at master · perl-entrance-org/workshop-2013-03 · GitHub

vote.pl

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

はい。んで、1と2についてはこんな感じにしてみました。

my $me = {
    name => 'Hiroaki',
    food => [qw/egg rice potato beef/],
};
my $bob = {
    name => 'Bob',
    food => [qw/vegi egg suiton beef/],
};
my $alice = {
    name => 'Alice',
    food => [qw/vegi egg tomato rice/],
};

この辺はまあ、とくに問題ないと思うんですが。
んで、3についてはこんな。

my @people = ($me, $bob, $alice);

はい。じつはこれが最初は結構ハマって、たとえばこんな風にしてみたり、

my @people = qw/$me $bob $alice/;

こんな風にしてみたりしたんですが、

my @people = ("$me", "$bob", "$alice");

これらだと「ハッシュリファレンスとして配列に格納する」ってことができない感じっぽくて、それで上記のようになるまでだいぶ時間がかかったというか・・一見なんでもないような1行で書かれた配列なんだけど、難しかったな・・。

んで、しかし本番はここからというか。
とりあえず、まず結論から言うとこんな感じになりました。

はい。なんだけど、もう全然自力ではこれはダメで。できなくて。
やったことの順番としては、まずpapixさんの模範解答がこちらにあるのですが、
「Perl入学式 第3回」の復習問題, vote.plを解いてみよう! | Hachioji.pm 日めくりテックトーク

これがまず「なるほど・・わからない」という感じで、それについては一度記事にも書いてますが、
Perl入学式復習:第3回の復習問題「vote.pl」より - draft

とにかくハードル高くて・・んで、じゃあどうしたかと言うと、そのpapixさんが書いてくださったものをひな形に、ひとつひとつ、変数名などを自分の理解しやすいようにちょっとずつ変えつつ、とにかくなぞって1行ずつ納得していくと。そういうことをしていきました。
んで、結果なんとか、ふむ、そういうことか・・と、まだ少しだけど、今ようやく理解できたかな・・という感じ。

以下、そのひとつひとつ、というのを簡単に自分用に解説してみますが。

まず、18行目の

my @people = ($me, $bob, $alice);

までの流れで、ネタになるデータは全部配列 @people に入れる、と。

次に、この後の作業で扱いやすいよう、とりあえず食べ物の名前(値)を全部取り出して、票数をカウントするためのハッシュとして %count_foods を作成。
#ここのところで、papixさんは「%ranking」というハッシュにされてました。

my %count_foods;

ちなみに、ここで僕はついこないだまで、このハッシュをリファレンス化しなきゃいけない、とずっと思い込んでいたんだけど、後述の理由でそれは不要なんだ、とつい最近分かりました。

次に、一旦その20行目はエポケー(判断停止)して、こんなの作る。

for my $people (@people) {
    my $person_foods = $people->{food};

このfor文によって、 $person_foods に食べ物の名前が列記された配列のリファレンスが3人分(3タイプ)入る、ということなはず。
ここまでは一応理解できてるつもりで、実際見ないでも書けた。
#ちなみに、この $person_foods の部分はpapixさんの模範解答だと $favorite_foods になってました。

んで、それに続ける形で以下2行。

    for my $name_food (@$person_foods) {
        $count_foods{$name_food}++;

まず、上の行(24行目)では $name_food の中に、「誰々の(好きな食べ物)」という属性を外してどんどん個別の食べ物の名前を放り込んでいく。

んで、そこまでは分かったんだけど、次のインクリメント(++)の行がかなりつらくて、というのは教えてもらった中には無い概念な感じがしたので(インクリメントとかは教えてもらったけどこの組み合わせが、という意味)もうこういう構文として覚えることに。
ようは、これをやれば、ハッシュのキー(ここでは $name_food の中身)に対応する値が数字になって、それが加算されていくのだ、と。そういうやり方なのだと。

ちなみに、インクリメントの使用に先んじてここでは初期化が不要、という話がpapixさんの記事で解説されていて、最初はよく分からなかったけど今は少し分かったつもりです。

それから、その同じ行の「$count_foods{$name_food}」という部分について、最初は、どうも見慣れない記法なのでこれ「$count_foods->{$name_food}」の略記か? と思っていたんですが(最初は、というよりしばらくずっと)、これって単にハッシュの中の値にアクセスするための基礎的な構文だったんですね。教えてもらった時に地味に扱われていたか、あるいは僕がたんにボーっとしていたかで全然認識していなかっただけで。
なので別に「->」を略してるわけではなさそう、というのが昨日・一昨日ぐらいにようやくわかりました。(これが上述の、20行目について語ったところの話とつながる)

さて次に、こんな感じでハッシュとハッシュリファレンスを作る。

my %ranking;
my $ranking = \%ranking;

ちょっと煩雑な話になりますが、ここで僕が「ranking」とした変数名はpapixさんの方では「votes」になっています。
僕の印象ではここからの作業でようやくランキングっていう概念が出てくるかなあ、と思ったので・・

んで、32〜35行目がこんな風に続く。

for my $name_food2 (keys %count_foods) {
    my $voted_num = $count_foods{$name_food2};
    push @{$ranking{$voted_num}}, $name_food2;
}

$name_food2 としたのは、papixさんの方だと一つ上の $name_food としたところもこれも、どっちも $food になってるんですが、僕の場合は「どちらも同じ食べ物の名前だけど、後のほうは宣言し直してるので別の名前の方が様子が理解しやすい」という気がしてそうしてます。

んで、続く33, 34行目はpapixさんの方では1行にまとめているんですが、僕の方ではちょっとそれだと進捗早すぎな感じもあって亀の歩みでわざわざ間にひとつ変数($voted_num)を設けて、それに代入して進めたりしています。

まあ、ようはこの33, 34行目で何が起こっているかというと、ひとつ前のfor文で作ったハッシュ、 %count_foods におけるキーと値の関係を左右逆にしてる、ってことなんでしょうね。
その辺の様子が把握できるまで時間かかったなあ・・

ということで、ここまでの内容を一回Data::Dumperでプリントするとこんな感じです。

$VAR1 = {
          '1' => [
                   'tomato',
                   'suiton',
                   'potato'
                 ],
          '3' => [
                   'egg'
                 ],
          '2' => [
                   'rice',
                   'vegi',
                   'beef'
                 ]
        };

ハッシュは順番が関係なくなるそうなのでやや分かりづらいですが、1票と2票が3つずつ、eggが3票を獲得しています。

で、あとはこれを成形してプリントするための、3つめのfor文をつくるのみです。

ちなみに、その前に、ここでやっていることの大きな流れをまとめると、

  • 最初のfor文で、人物ごとにまとめていた食べ物を一旦すべて抽出して、それぞれの票数をカウント
  • 2つめのfor文で、票数ごとに食べ物をグループ分け。
  • 3つめの(この後に作る)for文で、票数の多いものから少ないものへ並べて表示。

みたいな感じでしょうかね。

最後のfor文は、こんな感じになってます。(37〜42行目)

for my $sorted_ranking (sort {$b <=> $a} keys %ranking) {
    print "$sorted_ranking: \n";
    for my $sorted_foods (@{$ranking->{$sorted_ranking}}) {
        print "  $sorted_foods\n";
    }
}

最初の行でsort関数を使って、%rankingのキーである票数を、大きい順に抽出していく。
このとき、ぼくは $sorted_ranking という変数にしましたが、papixさんの方では $vote にされていますね。

んで、次の行(38行目)でとりあえずその票数(大きい順)をプリント。

んで、次の行(39行目)で、その中身となる食べ物の名前を抽出&新たな変数に代入していくわけですが、まず食べ物の名前はハッシュリファレンス $ranking の値に「->」でアクセスしてから「@」で配列にデリファレンスしたものを、 $sorted_foods に格納。
#このときpapixさんの方では変数を3度めの $food にしてますね。

んで、それをプリントしていく、と。これで終わり。

最終的には、こんな実行結果になっています。

3:
  egg
2:
  rice
  vegi
  beef
1:
  tomato
  suiton
  potato

はい。まあ、「全く意味不明です!!」とか思っていたつい数日前までに比べたら、けっこう腑に落ちています。
たぶんこの内容、今後も何度も見直すことになるんじゃないかな・・

さて、とはいえ、これでようやく本当に第3回の復習問題は一旦さらい終えたかと思いますので、この後はそうだな、ちょっと飛んで第5回のこの問題にしたいと思います。
workshop-2013-05/slide.md at master · perl-entrance-org/workshop-2013-05 · GitHub

これは12/8のPerl入学式in東京、僕は体調崩して参加できなかったんですが、その時に家で自習がてらトライしてかな〜〜り、ハマってようやくできたんですよね・・ようは文字列と数値の違いを扱うのに相当苦労したというか。
結果的にそれで合っていたのかもわからないですが、結構時間が経って、もう何にどうハマっていたのかも忘れつつあるので、まあちょうどいいタイミングかなと。

感覚的には今けっこう、「いくらやってもすぐ忘れる!」て感じでつらめですが、たぶんここ乗り切れば、と思ってます。ひき続きよろしくお願いします。>誰にともなく