Perl 6でNLP 5本ノック 『第1章: 準備運動』 05 ~ 09

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

NLP 5本ノックということで、東北大学の言語処理100本ノックの『第1章: 準備運動』の 05~09までの5本をPerl 6で解きつつ解説をしていきたいと思います。

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

# 読み進める前に

## 注意!
+ 読者レベルとしては、[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 (やり方は一つじゃない) です。特に正解はありません。でも、「これのほうがもっとかっこよく簡潔に書けるよ！」といった指摘は大歓迎です！

# 05. n-gram

## コード

```pl:05.p6
char-ngram("I am an NLPer", 2).perl.say;
word-ngram("I am an NLPer", 2).perl.say;

my sub char-ngram(Str $text, Int $n) {
    my @chars = $text.comb;
    gather for ^@chars X $n -> ($start, $len) {
        next if $start + $len > @chars;
        take @chars[$start..^($start + $len)]
    }
}

my sub word-ngram(Str $text, Int $n) {
    my @words = $text.words;
    gather for ^@words X $n -> ($start, $len) {
        next if $start + $len > @words;
        take @words[$start..^($start + $len)]
    }
}
```

## 解説

### sub char-ngram(Str $text, Int $n)

1. `$text.comb` で引数の文字列を文字単位に分割し、リストを`@chars`に代入します
2. `gather for take` イディオムでn-gramの文字列を取り出して遅延リストを生成します
    1. `^@words X $n` で開始位置,文字列長の組のすべての組み合わせのリストを生成します
    2. 開始位置 + 文字列長が `@words`の長さを超える場合は、その開始位置,文字列長の組に対する処理を止めます
    3. `@words`中の[開始位置,開始位置 + 文字列長)`の箇所の文字列を取得します

### sub word-ngram(Str $text, Int $n)

`sub char-ngram(Str $text, Int $n)` における`$text.comb`が`$text.words`に置き換わっただけです。`$text.words`は引数の文字列を空白区切りで分割します

# 06. 集合

## コード

```pl:06.p6
my Set \X = char-ngram("paraparaparadise", 2).Set;
my Set \Y = char-ngram("paragraph", 2).Set;

say X ∪ Y;
say X ∩ Y;
say X ∖ Y;
say Y ∖ X;

my sub char-ngram(Str $text, Int $n) {
    my @chars = $text.comb;
    gather for ^@chars X $n -> ($start, $len) {
        next if $start + $len > @chars;
        take @chars[$start..^($start + $len)]
    }
}
```

## 解説

NOTE: `sub char-ngram` は`05. n-gram` で用いたものと同一です

1. "paraparaparadise" のbi-gramのリストを生成し、`.Set`でSet型に変換し`\X` に代入します
2. "paragraph" のbi-gramのリストを生成し、`.Set`でSet型に変換し`\Y` に代入します
3. `X ∪ Y` で `X` と `Y` の和集合を求め、`say`で出力します
4. `X ∩ Y` で `X` と `Y` の積集合を求め、`say`で出力します
5. `X ∖ Y` で 集合 `X - Y` を求め、`say`で出力します
6. `Y ∖ X` で 集合 `Y - X` を求め、`say`で出力します

## 補足

+ `\X` や `\Y` はsigilless variable と呼ばれる変数です。今回は数式っぽく見せるためにちょっと遊び心で用いてみましたが、一般的にはSeq型の値を代入するときによく用いられます(※)。
 + (※) https://github.com/perl6/roast/blob/d1baf2e7a3e56cd6619c46040d04ed6daebc1d02/S03-operators/context-forcers.t#L239-L261 
 + sigilless variableについてより詳しく知りたい方はこちらを参照してください: https://docs.perl6.org/language/variables#Sigilless_variables
+ そのほかの集合演算子については次のページを参照ください:  https://docs.perl6.org/language/setbagmix.html

# 07. テンプレートによる文生成

## コード

```pl:07.p6
from-template(12,"気温",22.4).say;

my sub from-template(Int $x, Str $y, Rat $z --> Str) {
    ($x, "時の", $y, "は", $z).join
}
```

## 解説

### sub from-template(Int $x, Str $y, Rat $z --> Str)

NOTE: `func ($piyo --> Str)` は `func ($piyo) returns Str` と書くこともできます

1. `$x`, `$y`, `$z` の3つの変数を受け取ります
2. `($x, "時の", $y, "は", $z)`というリストを生成し、`.join`で要素を結合し、文字列を返します

# 08. 暗号文

## コード

```pl:08.p6
cipher("abc東北123").say;

my sub cipher(Str $text --> Str) {
    gather for $text.comb {
        if $_ ~~ /<:Ll>/ {
            take 219 - $_.ord
        } else {
            take $_
        }
    }.join.Str
}
```

## 解説

### sub cipher(Str $text --> Str)

1. `gather for take` イディオムを用いて遅延リストを生成します
  1. `$text.comb` で`$text`を文字単位に分割してリストにし、ひとつひとつの文字を見ていきます
  2. 与えられた文字が`<:Ll>` つまり英小文字だった場合は `219 - その文字の文字コード` を取得します
  3. そうでない場合はそのまま文字を取得します


# 09. Typoglycemia

## コード

```p6:09.p6
"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."\
.words\
.map({ my @chars = .comb;
       if @chars.elems <= 4 {
           @chars.join
       } else {
           @chars.head ~ @chars[1..^@chars.end].pick(@chars - 2).join ~ @chars.tail
       }
     })\
.say;
```

## 解説

1. `.words` でinvocantの文字列を空白区切りで分割し、リストにします
2. `.map`でinvocantのリスト中の各単語を操作します
  1. 単語を文字単位で分割し、リストにします
  2. 文字数が4以下(i.e. 1.のリストの要素数が4以下)ならそのまま結合して文字列にします
  3. そうでないなら、`@chars`中の`[先頭+1,末尾)`の箇所から `.pick` を使ってランダムな順番で文字列を選び、`join`により結合します。そして、この文字列に対して、`@chars.head` を先頭に、`@chars.tail` を末尾に結合します。
3. `.say` で出力します

## 補足

+ 興味のある方は`pick`と`roll`の違いを公式ドキュメントで調べてみましょう



以上、Perl 6でNLP 5本ノック 『第1章: 準備運動』 05 ~ 09　でした。