Perl 6でNLP 5本ノック 『第2章: UNIXコマンドの基礎』 15 ~ 19

こんにちは、Perl 6アドベントカレンダーの五日目の投稿になります。

NLP 5本ノックということで、東北大学の言語処理100本ノックの『第2章: UNIXコマンドの基礎』の 15~19までの5本をPerl 6で解きつつ解説をしていきたいと思います。

問題文は掲載しませんので、ブラウザの別窓で下記ページを参照しながらお楽しみください：
http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch2

# 読み進める前に

## 注意!
+ 読者レベルとしては、[Perl 6アドベントカレンダー1日目](http://qiita.com/titsuki/items/549ac95b8c37feb1dee1)で紹介した[Perl 6 introduction](https://github.com/hankache/perl6intro)などのチュートリアルを一通り終えたレベルを想定しています。
+ わかりやすく紹介するために、正確ではない表現がところどころ出てくるかもしれません。
 + より正確な情報を知りたい場合は、[Perl 6アドベントカレンダー1日目](http://qiita.com/titsuki/items/549ac95b8c37feb1dee1)で紹介した[公式ドキュメント](http://qiita.com/titsuki/items/549ac95b8c37feb1dee1#%E5%85%AC%E5%BC%8F%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88) や公式テストケースの[roast](http://qiita.com/titsuki/items/549ac95b8c37feb1dee1#roast)などを参照ください。
+ 日本語訳が定着していないようなPerl 6独特の英単語を目にするかもしれません。基本的に解説中ではそのまま英単語で書き、その後補足しますのでご容赦ください。
+ Perl 6のモットーはTMTOWTDI (やり方は一つじゃない) です。特に正解はありません。でも、「これのほうがもっとかっこよく簡潔に書けるよ！」といった指摘は大歓迎です！
+ `《一列目の要素》`といった表記が出てくるかと思いますが、 "《"や"》"は単に強調のために使っているだけでありPerl 6自身の文法ではないので注意してください。

# 15. 末尾のN行を出力

## コード

```pl:15.p6
sub MAIN(Int :$N!) {
    .say for gather for $*IN.lines { take $_ }.tail($N);
}
```

## 解説

+ `sub MAIN`を使うとコマンドライン引数を受け取とれるようになります
  + `:$N!`の`!`はrequiredの意味です。つまり必須オプションです。 ちなみに`?`だと任意のオプションになります。

1. `gather for take` イディオムで遅延リストを生成します
   + `$*IN.lines { take $_ }` で各行を取り出します
2. `.tail($N)` でコマンドライン引数`N`で与えた値と同数の行を遅延リストの末尾から抜き出します
3. `.say for 《.tail($N)の返したリスト》` で各行を出力していきます

# 16. ファイルをN分割する

## コード

```pl:16.p6
sub MAIN(Int :$N!) {
    my @filenames = ["xaa".."xzz"];
    repeat {
        state $idx = 0;
        my @group = do for ^$N {
            last if $*IN.eof;
            ~$*IN.get;
        }
        my $fh = open(@filenames[$idx], :w);
        $fh.say(@group.join("\n"));
        $fh.close;
        $idx++;
    } until $*IN.eof;
}
```

## 解説

1. `["xaa".."xzz"]`で出力ファイル名のリストを生成し、変数に代入します
2. `repeat-until`文を使って、標準入力が尽きるまで`:$N`行ごとにファイルに書き出していきます
  1. `state $idx = 0`で現在のファイル(e.g. 0なら`@filename[0]` eq "xaa", 1なら`@filename[1]` eq "xab")を示すための変数を宣言します。`state`は初期化を一度しか行わないという宣言子です。
  2. `do for ^$N 《ブロック》`で《ブロック》内の操作を`$N`回呼び出し、結果をリストに代入します。《ブロック》内では`$*IN.get`を使って標準入力から一行ずつ読み取りを行います
  3. `@filename[$idx]`で書き込みモードでファイルハンドルをオープンします
  4. `$fh.say(@group.join("\n"))`でファイルに書き込みます
  5. `$fh.close`でファイルハンドルを閉じます
  6. `$idx`をインクリメントし、次のループへ進みます

# 17. １列目の文字列の異なり

## コード

```pl:17.p6
$*IN.lines.map({ .split("\t")[0] }).Set.elems.say;
```

## 解説

1. `$*IN.lines`で標準入力から遅延リストを生成します
2. `.map`を使って各行を変換します
  + `.split("\t")[0]`でタブで分割して一列目だけ取り出します
3. `.Set`でSet型に変換します
4. `.elems`で要素数を算出します
5. `.say`で出力します

# 18. 各行を3コラム目の数値の降順にソート

## コード

```pl:18.p6
.say for $*IN.lines.map({ .split("\t") })\
.sort({ $^b[2] <=> $^a[2] })>>.join("\t");
```

## 解説

1. `$*IN.lines`で標準入力から遅延リストを生成します
2. `.map`を使って各行を変換します
  + `.split("\t")`でタブで分割してリストにします
3. `.sort({ $^b[2] <=> $^a[2] })`で３列目の数字の降順にソートします
4. `>>.join("\t")`で各行を再度タブ区切りに戻します
5. `.say for 《4.で生成されたリスト》` で各行を出力します

# 19. 各行の1コラム目の文字列の出現頻度を求め，出現頻度の高い順に並べる

## コード

```pl:19.p6
.say for $*IN.lines.map({ .split("\t")[0] })\
.categorize({ $_ })\
.map({ (.key, .value.elems) })\
.sort({ $^b[1] <=> $^a.[1] })>>.join("\t")
```

## 解説

1. `$*IN.lines`で標準入力から遅延リストを生成します
2. `.map`を使って各行を変換します
  + `.split("\t")[0]`で一列目だけ取り出します
3. `.categorize`で要素それ自身をキーとして、valueも要素それ自身を入れます(※)
4. `.map({ (.key, .value.elems) })`でinvocantを`(《ハッシュのキー》,《そのキーに紐づく値の要素数》)`のリストに変換します
5. `.sort({ $^b[1] <=> $^a.[1] })`でinvocantのリストの列の2番目(i.e. 《そのキーに紐づく値の要素数》)の要素の降順に並べ替えます
6. `>>.join("\t")`で各要素をタブ区切りの文字列にします
7. `.say for 《6. で出力されたリスト》`で各行を出力します

## 補足

+ (※)適当なサンプルコードで回すと`.categorize`はこんな感じです
+ 
```pl:profession
$ perl6 -e '.say for ("エンジニア","バーサーカー","バーサーカー","バーサーカー","アルケミスト").categorize({ $_ })'
アルケミスト => [アルケミスト]
バーサーカー => [バーサーカー バーサーカー バーサーカー]
エンジニア => [エンジニア]
```

+ categorizeについて詳しく知りたい方は次のページを参照してください：https://docs.perl6.org/routine/categorize

以上、Perl 6でNLP 5本ノック 『第2章: UNIXコマンドの基礎』 15 ~ 19　でした。

**P.S.**
あらかじめ書いておくと、私がNLP5本ノックに取り組んでいるのは2つ目的があります：

1. Web検索しても現行のPerl6をNLPのシーンで真面目に使っている例があまり見つからないので、そういうのをちょっとは作っておきたい
2. 拙作[p6-MeCab](https://github.com/titsuki/p6-MeCab)でサクッとPerl6ishに0~39の問題を解いてみるため

というわけで、今年のアドベントカレンダーで100本全部をやりきるつもりは全くないので、期待していた方はすみません。
また、せっかくなので別のネタを書いてみたいというのもあります。