NaaN日記

やったこと、覚えたことを発信する場

初見でひっかかったPerlのmapとハッシュと配列

こんにちは。

最近業務でPerlを使い始めました、id:CNaanです。

Perl歴2ヶ月ほどとなりましたが、Perlをはじめて1ヶ月の頃に躓いた話をします。

これから話す内容は、先日Perl未経験の新卒エンジニア1名に解かせてみたところ、引っかかったので、初心者*1は引っかかりやすいポイントだと思います。

同じ引っかかりをするかな(したら共感があるな)、という気持ちを込めて、本題までゆっくりと話していきます。

やっていくぞ

まずは、こちらをご覧ください。

use strict;
use warnings;

sub get_english_to_japanese {
    return +{
        apple => 'りんご',
        phone => '電話',
    };
}

はじめにPerlが知らない人向けに言っておくと、strictとwarningsは、滅茶苦茶なソースコードの誕生を避けるために宣言すべきなものです。
これを宣言しないと、未定義の変数を突然利用できたりします。

get_english_to_japaneseは、英語と日本語の組のハッシュのリファレンスを取得するサブルーチン(関数)です。
( + は、ハッシュリファレンスであることを明示するためにつけている記号です)
大体他の言語をやっている人なら、「RubyのHashやPythonのDictみたいな感じね」という感じで雰囲気掴んでもらえるかと思います。
リファレンス とは、という話はここでは割愛します。
わからなくても問題ないですが、気になる人向けにperldoc.jpを置いておきます。
perldoc.jp
はてなキーワードもあるぞ。
リファレンスとは 一般の人気・最新記事を集めました - はてな


さて、実行すると、こんな感じで表示されます。

use DDP;
my $hashRef = &get_english_to_japanese; # &は無くても良い。サブルーチンだと分かりやすいかなと思って書いた
p $hashRef;

DDPを使うと、簡単に中身を見ることができて便利です。

% perl sample.pl
{
    apple   "りんご",
    phone   "電話"
}

printだと結果はこんな感じになります。

print $hashRef;
> HASH(0x15580aa68)

リファレンス(ハッシュ全体を参照するスカラ)になっていて、そのままでは読めませんね。
後置デリファレンスを使ってデリファレンスします。

print $hashRef->%*;    >    appleりんごphone電話

DDPを使った結果と比べると読みにくいですね。

sportsを足してみる

さて、ここでget_english_to_japaneseに他のペアも追加したいと思いました。
スポーツ系の単語を追加したいと思ったので、スポーツ系の単語をまとめたサブルーチン(sports)を作成し、get_english_to_japaneseからsportsを呼ぶことにしました。

use strict;
use warnings;

sub sports {
    return [
        {
            english  => 'soccer',
            japanese => 'サッカー',
        },
        {
            english  => 'baseball',
            japanese => '野球',
        },
    ];
}

sub get_english_to_japanese {
    my $sports = sports;

    return +{
        apple => 'りんご',
        phone => '電話',
        # ハッシュの配列を作成 ( soccer => 'サッカー', baseball => '野球' )
        map { $_->{english} => $_->{japanese} } $sports->@*,
    };
}

実行すると、このような結果が得られます。

% perl sample.pl
{
    apple      "りんご",
    baseball   "野球",
    phone      "電話",
    soccer     "サッカー"
}

やった!実装できましたね。

colorsを足してみる

この調子で、色のペアも追加していきます!

sub colors {
    return [
        {
            english  => 'red',
            japanese => '赤'
        },
        {
            english  => 'yellow',
            japanese => '黄',
        },
    ];
}
sub get_english_to_japanese {
    my $sports = sports;
    my $colors = colors;

    return +{
        apple => 'りんご',
        phone => '電話',
        map { $_->{english} => $_->{japanese} } $sports->@*,
        map { $_->{english} => $_->{japanese} } $colors->@*,
    };
}

さっきと同じノリで、sportsの後ろにcolorsを足して、実行します。

ERROR

% perl sample.pl
Can't use string ("red") as a HASH ref while "strict refs" in use at sample.pl line 37.

おや、怒られてしまいました(´・_・`)
37行目は`map { $_->{english} => $_->{japanese} } $sports->@*,`の行ですね。

mapが展開されて、↓のような結果で実行されると思ったのですが、何が問題なのでしょうか?
どうすれば意図した出力結果を得られるのでしょうか。

    return +{
        apple => 'りんご',
        phone => '電話',
        ( soccer => 'サッカー', baseball => '野球' ),
        ( red    => '赤',    yellow   => '黄' ),
    };

ところで、「おや?ハッシュの中に配列が入っている?」と違和感を感じている人向けに説明をすると、Perlでは、配列の中の配列は展開・連結される仕様となっています。

配列の中の配列?はい、PerlのHashは、文字列がキーになった配列です。

また、次の二つは同じです。
(さらに、1の書き方の場合は、キーの文字列をクォーテーションで囲まなくても大丈夫です)

1. ( apple => 'りんご', banana => 'ばなな' )
2. ( 'apple', 'りんご', 'banana', 'ばなな' )

ハッシュとして扱えばハッシュ、配列として扱えば配列になります。

{
    apple    "りんご",
    banana   "ばなな"
}
[
    [0] "apple",
    [1] "りんご",
    [2] "banana",
    [3] "ばなな"
]

ちなみに、 => のことをfat commmaと呼びます。
Perl fat comma」で調べると、有益な記事が多く出てきます。
このあたりについては、O'Reilly Japan - 初めてのPerl 第7版で、2の書き方→1の書き方の順番で丁寧に説明しています。

Deparse してみる

では、実際どうなのか?そのことを調べるために、Deparseを使います。

% perl -MO=Deparse sample.pl
sub sports {
    use warnings;
    use strict;
    return [{'english', 'soccer', 'japanese', "サッカー"}, {'english', 'baseball', 'japanese', "野球"}];
}
sub colors {
    use warnings;
    use strict;
    return [{'english', 'red', 'japanese', "赤"}, {'english', 'yellow', 'japanese', "黄"}];
}
sub get_english_to_japanese {
    use warnings;
    use strict;
    my $sports = sports();
    my $colors = colors();
    return {'apple', "りんご", 'phone', "電話", map({$_->{'english'}, $_->{'japanese'};} @$sports, map({$_->{'english'}, $_->{'japanese'};} @$colors))};
}

Deparseしたことで原因がわかりましたね。
つまり、下記のように、mapの解釈が意図と違って解釈されていた、というわけなのです。
一つ目のmapが取る配列が、二つ目のmapの結果の配列と結合しているんですね。

    return +{
        apple => 'りんご',
        phone => '電話',
        map { $_->{english} => $_->{japanese} }
          ( $sports->@*, map { $_->{english} => $_->{japanese} } $colors->@* ),
    };

mapのところを一部展開してみると、こんな感じですね。どうみてもおかしいね。

        map { $_->{english} => $_->{japanese} } (
            (
                {
                    english  => 'soccer',
                    japanese => 'サッカー'
                },
                {
                    english  => 'baseball',
                    japanese => '野球',
                },
            ),
            ( red => '赤', yellow => '黄' )
        ),

というわけで、解決策は、「(一番最後以外の)mapに明示的に括弧をつける」でした。
ただ、次にまたmapが増えるかもしれないことを考えると、統一感を出して全てに括弧をつけるようにすることが良いと思います。
括弧の付け方は二通りあります。

( map { $_->{english} => $_->{japanese} } $sports->@* ),
map ( { $_->{english} => $_->{japanese} } $colors->@* ),

この状態でDeparseすると、括弧の位置が意図した通りになっています。

map({$_->{'english'}, $_->{'japanese'};} @$sports), map({$_->{'english'}, $_->{'japanese'};} @$colors)

おわり

というわけなので、mapとハッシュと配列を一緒に扱うときは気をつけましょう。

そして、DPPとか、Deparseとか、これらは困った時に役立ちますね。
はやくこういったツールを使いこなせるようになりたいと思います。

ところで、はてなキーワードにはPerl関係のキーワードが豊富そうで、書いていて面白いですね。
読んでいてわからなかったらキーワードに飛んでくれ!ってできそうなところが良い。

id:CNaanのこれからのご活躍にご期待ください( ˘ω˘ )
では。

Rubyでも同じようなことできるのかなって思って途中までやったけどPerlと全然違うコードになってきたのでやめた (¦3ꇤ[▓▓]

*1:特に既存のコードの雰囲気を真似て書こうとする人とか