fishで言語処理100本ノック (第1章)

# はじめに
fishシェルスクリプトの練習のため，[言語処理100本ノック](http://www.cl.ecei.tohoku.ac.jp/nlp100/)に挑戦してみました．

シェルから`awk`等を呼ぶと趣旨が変わってしまうので，できる限りfishに組み込みのコマンドで解くことにします．
ただし，連番生成のための`seq`は使ってもよいものとしました．
(わざわざカウンタ付きwhileループするのも冗長なので...)

参考にした記事: [言語処理100本ノック with Python（第1章）](http://qiita.com/gamma1129/items/37bf660cf4e4b21d4267)

# 第1章: 準備運動
お題となる文字列を変数`input`に格納したところからスタートします．

```sh
set -l input # まずローカル変数inputを生成．以下，これを使いまわす．
```
## 00. 文字列の逆順
> 文字列"stressed"の文字を逆に（末尾から先頭に向かって）並べた文字列を得よ．


```sh
set input 'stressed'
string join '' (string split '' $input)[-1..1]
```

文字列→文字の配列は`string split ''`，文字の配列→文字列は`string join ''`で変換．
fishのコマンド置換`()`で複数行を出力すると配列に展開されるので，それをその場で`[-1..1]`でひっくり返します．

## 01. 「パタトクカシーー」
> 「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ．

```sh
set input 'パタトクカシー'
string join '' (string split '' $input)[1 3 5 7]
```

前の問題とほぼ同じ．

## 02. 「パトカー」＋「タクシー」＝「パタトクカシーー」 
>「パトカー」＋「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ．

```sh
set input 'パトカー' 'タクシー'
for i in (seq 1 (string length $input[1]))
    string sub -s $i -l 1 $input
end | string join ''
```

変数`input`を配列として，「パトカー」，「タクシー」を格納した状態からスタート．文字数の分だけ`for`ループを回して処理します．

`string sub`が配列`input`の各要素を処理してくれるので，`for`ブロックからの出力を`string join`でくっつけると完成．

## 03. 円周率
>"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し，各単語の（アルファベットの）文字数を先頭から出現順に並べたリストを作成せよ．

```sh
set input 'Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.'
echo (string length (string match -ar '\w+' $input))
```

単語への分解は`string match`で実行．前の問題の`string sub`もそうでしたが，`string`は複数の文字列を同時に処理してくれるのでそのまま`string length`に渡すと完成．

## 04. 元素記号
>"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し，1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字，それ以外の単語は先頭に2文字を取り出し，取り出した文字列から単語の位置（先頭から何番目の単語か）への連想配列（辞書型もしくはマップ型）を作成せよ．

```sh
set input 'Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.'
set -l elem (string match -ar '\w+' $input)
for i in (seq 1 (count $elem))
    set elem_(contains $i 1 5 6 7 8 9 15 16 19 
    and string sub -s 1 -l 1 $elem[$i]
    or string sub -s 1 -l 2 $elem[$i]) $i
end
```
fishに辞書型なんてものは存在しない[^assoc]ので`elem_元素名`という変数を作って擬似的な辞書とします．上記を実行した環境では`echo $elem_Al`で`13`が得られます．

[^assoc]: fishで連想配列を使うためのプラグインがoh-my-fishにあります([oh-my-fish/plugin-assoc](https://github.com/oh-my-fish/plugin-assoc))．...が，これは内部でrubyを呼び出しています．それをするくらいなら最初からrubyで書けばよいのではという気がしないでもないです．

## 05. n-gram
>与えられたシーケンス（文字列やリストなど）からn-gramを作る関数を作成せよ．この関数を用い，"I am an NLPer"という文から単語bi-gram，文字bi-gramを得よ．

```sh
function n-gram -a num
    set -e argv[1]
    if test "$num" -le (count $argv)
        string join - $argv[1..$num]
        n-gram $num $argv[2..-1]
    end
end

set input "I am an NLPer"
n-gram 2 (string match -ar '\w+' $input) # 単語bi-gram
n-gram 2 (string split '' $input) # 文字bi-gram
```

指定された数の要素だけ配列から取り出して再帰でループ．単語n-gramのために区切り文字が必要なので，ここでは`-`を使いました．

## 06. 集合
>"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を，それぞれ, XとYとして求め，XとYの和集合，積集合，差集合を求めよ．さらに，'se'というbi-gramがXおよびYに含まれるかどうかを調べよ．

```sh
function unique
    set -l uniq
    for a in $argv
        if not contains $a $uniq
            set uniq $uniq $a
            echo $a
        end
    end
end

function union -S
    unique $$argv
end

function intersect -S
    for x in $$argv[1]
        contains $x $$argv[2]; and echo $x
    end
end

function difference -S
    for x in $$argv[1]
        contains $x $$argv[2]; or echo $x
    end
end

set input "paraparaparadise" "paragraph"

set -l X (unique (n-gram 2 (string split '' $input[1])))
set -l Y (unique (n-gram 2 (string split '' $input[2])))

echo "X" $X
echo "Y" $Y
contains s-e $X; and echo "s-e は X に含まれます!"
contains s-e $Y; and echo "s-e は Y に含まれます!"
echo "X|Y" (union X Y)
echo "X-Y" (difference X Y)
echo "X&Y" (intersect X Y)
```

fish関数縛りをしているのでなければ，配列から重複を消すのには`sort`と`uniq`を使うべきな気がする．

和集合・差集合・積集合の作成には複数の配列を受け取る必要があるので`-S(--no-scope-shadowing)`で関数を定義．`$`が連続する独特な記法になります．和集合`union`の定義がキモいですね．

## 07. テンプレートによる文生成
> 引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ．さらに，x=12, y="気温", z=22.4として，実行結果を確認せよ．

```sh
function template -a x y z
    echo "$x時の$yは$z"
end

template 12 気温 22.4
```

シェルなのでこれは非常に簡単．

## 08. 暗号文
>与えられた文字列の各文字を，以下の仕様で変換する関数cipherを実装せよ．
>
>- 英小文字ならば(219 - 文字コード)の文字に置換
>- その他の文字はそのまま出力
>
>この関数を用い，英語のメッセージを暗号化・復号化せよ．

```sh
function cipher
    for c in (string split '' "$argv")
        string match -qr '[a-z]' $c
        and echo -e \\x(printf '%x' (math 219 - (printf '%d' \'$c)))
        or echo "$c"
    end | string join ''
end

cipher (cipher "Hello world!")
```

fishだけで文字コード取得とかできるのだろうか...としばらく悩みましたが`printf`で可能な模様です．

## 09. Typoglycemia
>スペースで区切られた単語列に対して，各単語の先頭と末尾の文字は残し，それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ．ただし，長さが４以下の単語は並び替えないこととする．適当な英語の文（例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."）を与え，その実行結果を確認せよ．

```sh
set input "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."

function typoglycemia
    set -l array (string split ' ' "$argv")
    if test (count $array) -lt 3 # 入力が3語以下なら何もしない
        echo $array
    else
        echo $array[1]
        set -l buf (for word in $array[2..-2] # 5文字以上の単語を一時変数に格納
            test (string length $word) -gt 4; and echo $word
        end)

        for word in $array[2..-2]
            if test (string length $word) -gt 4 # 5文字以上の単語を他の語に入れ替え
                set -l i (random 1 (count $buf) ^/dev/null; or echo 1)
                echo $buf[$i]
                set -e buf[$i]
            else
                echo $word
            end
        end
        echo $array[-1]
    end | string join ' '
end

# pasteで確認
paste (string split ' ' $input | psub) (string split ' ' (typoglycemia $input) | psub)
```

fishのコマンド置換`()`は途中で普通に改行してforループ回せたりするので，それを利用して入れ替えに使う単語の配列(`buf`)を作成．
他のスクリプト言語だともっとあっさり書けるんですかねこれ...？

# おわりに
fishで言語処理100本ノック (第2章) に続きたいです．