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

# はじめに

[Haskellで自然言語処理100本ノックの第2章を解いてみる。【前編】](https://qiita.com/ayase/items/a03e9e8484b492e6bc08)の続きです。
アクセス数やいいねを見て需要ない感じが半端じゃなく、更新してませんでしたが、ここにきてまさかの更新です！

# 解答に関して

- 簡潔さとわかりやすさ重視で書きました。
    - 一部の問題は今までの問題より少しだけ難しめなので重視できなかったかもしれません。。
- 文字列はなるべくString型ではなくText型を扱うようにしました。

# 問題と解答

## 第2章: UNIXコマンドの基礎

>**hightemp.txtは，日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである．以下の処理を行うプログラムを作成し，hightemp.txtを入力ファイルとして実行せよ．さらに，同様の処理をUNIXコマンドでも実行し，プログラムの実行結果を確認せよ．**

## 15. 末尾のN行を出力

>**自然数Nをコマンドライン引数などの手段で受け取り，入力のうち末尾のN行だけを表示せよ．確認にはtailコマンドを用いよ．**

解答はこちら

```haskell:15.hs
-- 15. 末尾のN行を出力
-- 自然数Nをコマンドライン引数などの手段で受け取り，入力のうち末尾のN行だけを表示せよ．確認にはtailコマンドを用いよ．

module Main where

import qualified System.IO as IO
import qualified System.Environment as Env
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Text.Read as Read

import System.Environment(getArgs, getProgName)

tail :: Int -> Text.Text -> IO()
tail n input = TextIO.putStr $ tail' n input
    where
        tail' :: Int -> Text.Text -> Text.Text
        tail' n input = Text.unlines $ List.drop (List.length (Text.lines input) - n) (Text.lines input)

main :: IO()
main = do
    input <- TextIO.readFile "hightemp.txt"
    prog <- Env.getProgName
    args <- Env.getArgs
    case args of
        [n] -> Main.tail (Read.read n :: Int) input
        _ -> usage prog
    where
        usage :: String -> IO()
        usage prog = IO.putStrLn $ "usage: " ++ prog ++ " N (N must be a nutural number.)"

```

**前問のコードとほぼ同じ。**

というより、`head`との対称性を考えてなるべくコードの変更はしなかった。

違いは**`List.take`**が**`List.drop`**になり、引数が変わった程度。
文字列に関しては`Text.Lazy`ではなく`Text`を用いたが特筆する話ではないだろう。

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

>**自然数Nをコマンドライン引数などの手段で受け取り，入力のファイルを行単位でN分割せよ．同様の処理をsplitコマンドで実現せよ．**

```haskell:16.hs
-- 16. ファイルをN分割する
-- 自然数Nをコマンドライン引数などの手段で受け取り，入力のファイルを行単位でN分割せよ．同様の処理をsplitコマンドで実現せよ．

module Main where

import qualified System.IO as IO
import qualified System.Environment as Env
import qualified Data.List as List
import qualified Data.Tuple as Tuple
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Text.Read as Read
import qualified Text.Show as Show
import qualified Control.Monad as Monad

import System.Environment(getArgs, getProgName)

writeLineFile :: FilePath -> [Text.Text] -> IO()
writeLineFile filepath xs = IO.withFile filepath IO.WriteMode $ \handle ->
    Monad.mapM_ (TextIO.hPutStrLn handle) xs

split' :: [Text.Text] -> Int -> Int -> Int -> IO()
split' lines n m index = Main.writeLineFile filepath contents
    where
        filepath = "split/hightemp_split_" ++ Show.show index ++ ".txt"
        contents = List.take m (List.drop (n * index) lines)

split :: Int -> Text.Text -> IO()
split n input = Monad.zipWithM_ (Main.split' (Text.lines input) n) ms [0..]
    where
        divmod = divMod (List.length $ Text.lines input) n
        ms = List.replicate (Tuple.fst divmod) n ++ if Tuple.snd divmod == 0 then [] else [Tuple.snd divmod]

main :: IO()
main = do
    input <- TextIO.readFile "hightemp.txt"
    prog <- Env.getProgName
    args <- Env.getArgs
    case args of
        [n] -> Main.split (Read.read n :: Int) input
        _ -> usage prog
    where
        usage :: String -> IO()
        usage prog = IO.putStrLn $ "usage: " ++ prog ++ " N (N must be a nutural number.)"

```

第2章で一番むずかしいというか、一番面倒で時間かかったのはこの問題だった。
ただ、他の言語での解答を見てもどちらにせよそこそこ面倒そうだったのでこんなもんだろうか。
Haskellでもゴリ押しのコードが書けるようになったと前向きに考えておく。:grin:

**`writeLineFile`**は前編からそのまま引用しました。

**`split`**は前問の`head`と前前問の`tail`の型シグネチャとわざわざ合致させるためになんとなく作った関数で、肝心な方は**`split'`**の方で行っている。
**`split`**でやっていることはN分割したときに`List.take`で取ってきたい行の数のリスト(`ms`がそうです..)を作成して、何分割目かを確認するためのindex値のリスト(`[0..]`がそうです..)と一緒に**`zipWithM_`**してしまおう！といった感じです。
**`zipWithM_`**は返り値ぽいっとして`m()`とします。ここでの`m`は`Applicative`のインスタンスで今回は`IO`がそれに該当します。:v::v_tone1:

**`split'`**は**`index+1`分割目**の役割を果たします。出力ファイル名はもちろん適当です。

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

>**1列目の文字列の種類（異なる文字列の集合）を求めよ．確認にはsort, uniqコマンドを用いよ．**

```haskell:17.hs
-- 17. １列目の文字列の異なり
-- 1列目の文字列の種類（異なる文字列の集合）を求めよ．確認にはsort, uniqコマンドを用いよ．

module Main where

import qualified Data.List as List
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Control.Monad as Monad

main :: IO()
main = do
    input <- TextIO.readFile "hightemp.txt"
    let
        row1 = List.map (List.head . Text.words) (Text.lines input)
        uniq = Set.fromList row1
    Monad.mapM_ TextIO.putStrLn uniq

```

`row1`が1列目の文字列のリストで`uniq`がその集合です。
このコードに関しては特に語ることがありません。。

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

>**各行を3コラム目の数値の逆順で整列せよ（注意: 各行の内容は変更せずに並び替えよ）．確認にはsortコマンドを用いよ（この問題はコマンドで実行した時の結果と合わなくてもよい）．**

```haskell:18.hs
-- 18. 各行を3コラム目の数値の降順にソート
-- 各行を3コラム目の数値の逆順で整列せよ（注意: 各行の内容は変更せずに並び替えよ）．確認にはsortコマンドを用いよ（この問題はコマンドで実行した時の結果と合わなくてもよい）．

module Main where

import qualified Data.Ord as Ord
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Control.Monad as Monad

main :: IO()
main = do
    input <- TextIO.readFile "hightemp.txt"
    let
        words = List.map Text.words $ Text.lines input
        sorted = List.sortBy (\xs ys -> Ord.compare (xs !! 2) (ys !! 2)) words
        lines = List.map Text.unwords sorted
    Monad.mapM_ TextIO.putStrLn lines

```

3カラム目でソートしてます。なんで17問目では列表記だったのに18問目でコラム表記なのでしょうか。。

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

>**各行の1列目の文字列の出現頻度を求め，その高い順に並べて表示せよ．確認にはcut, uniq, sortコマンドを用いよ．**

この問題に関してはコラム表記と列表記が混じってますね。なんでだろう。。:sweat_smile:

```haskell:19.hs
-- 19. 各行の1コラム目の文字列の出現頻度を求め，出現頻度の高い順に並べる
-- 各行の1列目の文字列の出現頻度を求め，その高い順に並べて表示せよ．確認にはcut, uniq, sortコマンドを用いよ．

module Main where

import qualified Data.Function as Function
import qualified Data.Ord as Ord
import qualified Data.List as List
import qualified Data.Tuple as Tuple
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Control.Monad as Monad

main :: IO()
main = do
    input <- TextIO.readFile "hightemp.txt"
    let
        row1 = List.map (List.head . Text.words) (Text.lines input) -- List[都道府県(重複あり)..]
        init = HashMap.fromList (List.zip row1 (repeat 0))          -- HashMap[(都道府県, 0)..]
        counted = HashMap.mapWithKey (count row1) init              -- HashMap[(都道府県, 出現頻度)..]
        sorted = List.sortBy (Function.flip Ord.compare `Function.on` Tuple.snd) (HashMap.toList counted)
    Monad.mapM_ (TextIO.putStrLn . Tuple.fst) sorted
    where
        count :: [Text.Text] -> Text.Text -> Int -> Int
        count r k _ =  List.length $ List.filter (== k) r

```

`row1`で一列目のリストを作成して、`init`でvalueがすべてゼロのHashMap作ります。
`counted`で出現頻度をvalueに入れて、`sorted`で出現頻度についてソートしてます。

**`HashMap`**はUnorderedなMapでKeyのOrderを気にしなくていい場合はMapよりも基本的にパフォーマンスが高いコンテナです。
まあこのレベルであれば普通に`Map`でも困ることはなさそうです。

# さいごに

結局２章までやってしまったが、正規表現やパーサー書いてどうこうするというよくある感じのあれこれまで至っておらず、第３章もやるかもしれません。
正規表現やパーサー書くっていうのはいろいろやってきているのであまりやったことがないという理由で`Haskell`続投する可能性があるかも…？