Haskellで自然言語処理100本ノックの第1章を解いてみる。【前編】

# なぜHaskellで自然言語処理か
文字列処理力を上げようと思ったのだが、いつも通りC++やらPythonやらを使うのはなんか<b>ふつう</b>な気がしたのだ
~~まあC++で文字列処理ってきほんしぬとはおもうけど~~

それを考えると普段業務で使っていないHaskellやRustが候補にあがる。
Haskellはかなり長い間使っていなかったので今回は久しぶりにHaskellでやることにした。
ちなみに筆者のHaskell力は[LYAHFGG](http://learnyouahaskell.com/chapters)と[RWH](http://book.realworldhaskell.org/read/)の気になる部分を読んでみた程度です。
そういう意味ではHaskellやってみたい人向けでもあるかもしれない。

とりあえずは[言語処理100本ノック](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の第1章をやってみる。
言語処理にはあまり明るくないが最初の数章はふつうのテキスト処理問題に思える。
Python用の問題集らしいがまあ、最初の方だけならなんとかなるだろう。

# 解答に関して
簡潔さとわかりやすさ重視で書きました。
が、Haskell初心者がかいたものなのでたいして簡潔になってないかもしれません。
ただ、わかりやすさと天秤にかけたらわかりやすさのほうがおもいかな。
Haskellは抽象的にかけるぶんむずかしくなってしまうので。

**\*2018/12/12追記**
**解答でしょっちゅうString型を使っていますが今はText型のほうが主流のようなのでみなさんはText型を使ってみることをおすすめします。**
**まあ最初のうちはString型で慣れるのもよいとは思いますが。。**


# 問題と解答
## 00. 文字列の逆順
>**文字列"stressed"の文字を逆に（末尾から先頭に向かって）並べた文字列を得よ．**

```haskell:00.hs
main = putStrLn $ reverse "stressed"
```
```shell:出力
desserts
```
もはやいうことはあるまい。
ただ単に文字列に対して`reverse`を適用しているだけ。


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

```haskell:01.hs
main = putStrLn ["パタトクカシーー" !! index | index <- [0, 2, 4, 6]]
```
```shell:出力
パトカー
```
**`リスト内包表記`**を使用した。`map`使うのと大差ないだろうがこの問題に関してはリスト内包表記のほうが簡潔にかけるとおもった。
雰囲気的にはパタトクカシーー !! 0とパタトクカシーー !! 2とパタトクカシーー !! 4とパタトクカシーー !! 6が並んでいるイメージ(こなみ)


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

```haskell:03.hs
import Data.List

main = do
  let objs = transpose ["パトカー", "タクシー"]  -- objs == ["パタ", "トク", "カシ", "ーー"]
  putStrLn $ foldr (++) [] objs
```
```shell:出力
パタトクカシーー
```
なんかこれに関しては一発でなんとかなる関数がありそうだが。。
畳み込みの練習になってしまった。

**2018/11/9追記**
**`foldr (++) []`は`concat`関数と同義のようだ。**
なので以下のようにも書ける。

```haskell:03.hs(concatを使用したバージョン)
import Data.List

main = do
  let objs = transpose ["パトカー", "タクシー"] -- objs == ["パタ", "トク", "カシ", "ーー"]
  putStrLn $ concat objs
```

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

```haskell:03.hs
module Main where

import qualified Data.List as List
import qualified Data.Char as Char
import qualified System.IO as IO

-- アルファベットのみのリストを生成し、その大きさを返す
numAlphas :: String -> Int
numAlphas = List.length . List.filter Char.isAlpha

main :: IO()
main = IO.print answer
    where
        ws      = List.words "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
        answer  = List.map Main.numAlphas ws
```
```shell:出力
[3,1,4,1,5,9,2,6,5,3,5,8,9,7,9]
```
いろいろとコンパイラに怒られ、とうとう`import qualified`を使用することにした。
地味に`map`と`filter`が初登場だ。

`main`の書き方はこれでいいのかなかなかわからん。
とりあえず`where`の中が短いのでここでは`where`でかいた。

最初、問題の意図を勘違いしてかなり時間をかけてしまったが、
`numAlphas`関数はその勘違いの副産物なのでよかったとしよう。
正直、こういう問題だったら出力例みたいなのも問題と一緒にあっていい気がするが


## 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文字を取り出し，取り出した文字列から単語の位置（先頭から何番目の単語か）への連想配列（辞書型もしくはマップ型）を作成せよ．**

```haskell:04.hs
module Main where

import qualified Data.List as List
import qualified Data.Map.Strict  as Map
import qualified System.IO as IO

makeKey :: (Eq a1, Num a1) => a1 -> [a2] -> [a2]
makeKey n = List.take (takeNum n)     -- List.take (1 or 2)
    where takeNum n' = if n' `List.elem` [1, 5, 6, 7, 8, 9, 15, 16, 19] then 1 else 2

main :: IO()
main = do
    let words   = List.words "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
        values  = [1..(List.length words)]               -- values == [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
        keys    = List.zipWith Main.makeKey values words -- keys   == ["H","He","Li","Be","B","C","N","O","F","Ne","Na","Mi","Al","Si","P","S","Cl","Ar","K","Ca"]
        map     = Map.fromList $ List.zip values keys
    IO.print map
```
```shell:出力
fromList [(1,"H"),(2,"He"),(3,"Li"),(4,"Be"),(5,"B"),(6,"C"),(7,"N"),(8,"O"),(9,"F"),(10,"Ne"),(11,"Na"),(12,"Mi"),(13,"Al"),(14,"Si"),(15,"P"),(16,"S"),(17,"Cl"),(18,"Ar"),(19,"K"),(20,"Ca")]
```
これもどうするかわりと悩んだ。
`1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語`は`先頭の1文字`を、それ以外の`2, 3, 4, 10, 11, ..., 20番目の単語`は`2文字`を取り出すので、
`[1,2,2,2,1,1,1,..(中略)..,2,1,2]`のようなリストを準備しておけばもっと簡潔になるかと思ったが、
変更に弱そうだし、わかりづらそうだし、やめときました。

`makeKey n xs`は`n`が`[1, 5, 6, 7, 8, 9, 15, 16, 19]`内に存在していたら`take 1 xs`、それ以外なら`take 2 xs`となる関数です。

全体的には`zip`と`zipWith`でくっつけていく感じになってます。

# 第１章前半を終えた感想
はっきり言って結構時間がかかってしまった。
なれない言語だとゴリ押しが効かないのでなかなかキツイ。

とりあえずここまでならhoogleでData.ListとData.Mapの中を覗きまくる方針でよさそう。

１章の後半に行く前に他の言語でどれくらいの時間で最初の5問が解けるかはかってみたいところだなあ。