Haskell
自然言語処理
NLP
言語処理100本ノック

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

はじめに

Haskellで自然言語処理100本ノックの第1章を解いてみる。【後編】の続きです。
正直、第2章はHaskellで解く気はなくてRustやPerl6あたりでも使ってみるかと思ったのですが、なんだかんだ言ってHaskellで書くの楽だしもう少しだけHaskellで解いてみるかという感じでやってきました。

解答に関して

第1章を解く際に掲げたこころざしを再掲しておきます。

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

あと、今回は文字列はなるべくString型ではなくText型を扱うようにしました。
文字列を文字のリストとして扱いたい場合でも大方これで大丈夫そうだったのとこっちのほうが推奨されてそうだったので。

問題と解答

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

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

10. 行数のカウント

行数をカウントせよ.確認にはwcコマンドを用いよ.

10.hs
module Main where

import qualified System.IO as IO
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO

main :: IO()
main = do
    input <- TextIO.readFile "hightemp.txt"
    IO.print $ List.length $ Text.lines input

第1章にもちらっと登場したが、もうモナドからは逃れることはできない。。
<-ってなんやねん!と思った方はぜひモナドやdo記法あたりでググるとGood。

とりあえず内容としてはhightemp.txtの中身を取り出して行数を数えている。

出力
24

wc -lで行数確認。合っている模様。

wc
>wc -l hightemp.txt
      24 hightemp.txt

11. タブをスペースに置換

タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.

11.hs
module Main where

import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO

main :: IO()
main = do
    input <- TextIO.readFile "hightemp.txt"
    TextIO.putStr $ Text.map (\c -> if c == '\t' then ' ' else c) input

hightemp.txtからひっぱってきた内容にタブをスペースに変えるラムダをmapして出力しているだけ。
特に語る内容がない。。

出力
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
岐阜県 多治見 40.9 2007-08-16
山形県 山形 40.8 1933-07-25
山梨県 甲府 40.7 2013-08-10
和歌山県 かつらぎ 40.6 1994-08-08
静岡県 天竜 40.6 1994-08-04
山梨県 勝沼 40.5 2013-08-10
埼玉県 越谷 40.4 2007-08-16
群馬県 館林 40.3 2007-08-16
群馬県 上里見 40.3 1998-07-04
愛知県 愛西 40.3 1994-08-05
千葉県 牛久 40.2 2004-07-20
静岡県 佐久間 40.2 2001-07-24
愛媛県 宇和島 40.2 1927-07-22
山形県 酒田 40.1 1978-08-03
岐阜県 美濃 40 2007-08-16
群馬県 前橋 40 2001-07-24
千葉県 茂原 39.9 2013-08-11
埼玉県 鳩山 39.9 1997-07-05
大阪府 豊中 39.9 1994-08-08
山梨県 大月 39.9 1990-07-19
山形県 鶴岡 39.9 1978-08-03
愛知県 名古屋 39.9 1942-08-02

sedでの確認。最後はdiffをとっている
diff <(...) <(...)のやっていることについては標準入力同士の diffを参照するとよいb

sed
>sed 's/\t/ /g' hightemp.txt
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
岐阜県 多治見 40.9 2007-08-16
山形県 山形 40.8 1933-07-25
山梨県 甲府 40.7 2013-08-10
和歌山県 かつらぎ 40.6 1994-08-08
静岡県 天竜 40.6 1994-08-04
山梨県 勝沼 40.5 2013-08-10
埼玉県 越谷 40.4 2007-08-16
群馬県 館林 40.3 2007-08-16
群馬県 上里見 40.3 1998-07-04
愛知県 愛西 40.3 1994-08-05
千葉県 牛久 40.2 2004-07-20
静岡県 佐久間 40.2 2001-07-24
愛媛県 宇和島 40.2 1927-07-22
山形県 酒田 40.1 1978-08-03
岐阜県 美濃 40 2007-08-16
群馬県 前橋 40 2001-07-24
千葉県 茂原 39.9 2013-08-11
埼玉県 鳩山 39.9 1997-07-05
大阪府 豊中 39.9 1994-08-08
山梨県 大月 39.9 1990-07-19
山形県 鶴岡 39.9 1978-08-03
愛知県 名古屋 39.9 1942-08-02
>diff <(sed 's/\t/ /g' hightemp.txt) <(stack runghc 11.hs)
>

12. 1列目をcol1.txtに,2列目をcol2.txtに保存

各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.

12.hs
module Main where

import qualified System.IO as IO
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

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

main :: IO()
main = do
    input <- TextIO.readFile "hightemp.txt"
    let lines = Text.lines input
        lines2 = List.map (List.take 2 . Text.words) lines -- [[高知県, 江川崎], [埼玉県, 熊谷], ...]
        col1  = [List.head xs | xs <- lines2]  -- [高知県, 埼玉県, ...]
        col2  = [List.last xs | xs <- lines2]  -- [江川崎, 熊谷, ...]
    Main.writeLineFile "col1.txt" col1
    Main.writeLineFile "col2.txt" col2

まず最初に[[col1_0, col2_0], [col1_1, col2_1], ..., [col1_n, col2_n]]のようなリストを作り、
そこから1行目のテキストのリストと2行目のテキストのリストを作って、それをwriteLineFile関数に渡している感じ。
writeLineFileはテキストのリストに対しhPutStrLnmapM_していき内容を対象のファイルに吐き出している。

ターミナルにて確認
>cut -f 1 hightemp.txt
高知県
埼玉県
岐阜県
山形県
山梨県
和歌山県
静岡県
山梨県
埼玉県
群馬県
群馬県
愛知県
千葉県
静岡県
愛媛県
山形県
岐阜県
群馬県
千葉県
埼玉県
大阪府
山梨県
山形県
愛知県
>cat col1.txt
高知県
埼玉県
岐阜県
山形県
山梨県
和歌山県
静岡県
山梨県
埼玉県
群馬県
群馬県
愛知県
千葉県
静岡県
愛媛県
山形県
岐阜県
群馬県
千葉県
埼玉県
大阪府
山梨県
山形県
愛知県
>diff <(cut -f 1 hightemp.txt) <(cat col1.txt)
>cat col2.txt
江川崎
熊谷
多治見
山形
甲府
かつらぎ
天竜
勝沼
越谷
館林
上里見
愛西
牛久
佐久間
宇和島
酒田
美濃
前橋
茂原
鳩山
豊中
大月
鶴岡
名古屋
>diff <(cut -f 2 hightemp.txt) <(cat col2.txt)
>

13. col1.txtとcol2.txtをマージ

12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ.

13.hs
module Main where

import qualified System.IO as IO
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

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

main :: IO()
main = do
    col1 <- TextIO.readFile "col1.txt"
    col2 <- TextIO.readFile "col2.txt"

    let col1' = Text.lines col1
        col2' = Text.lines col2
        output = List.zipWith (\x -> Text.append $ Text.snoc x '\t') col1' col2'

    Main.writeLineFile "merge.txt" output

col1.txtcol2.txtの内容をzipWithでくっつけていく。
くっつける関数は今回Textを扱うのでListを扱う際に使う++などではなくappendやらsnocを使った。

ターミナルにて
>cat merge.txt
高知県  江川崎
埼玉県  熊谷
岐阜県  多治見
山形県  山形
山梨県  甲府
和歌山県        かつらぎ
静岡県  天竜
山梨県  勝沼
埼玉県  越谷
群馬県  館林
群馬県  上里見
愛知県  愛西
千葉県  牛久
静岡県  佐久間
愛媛県  宇和島
山形県  酒田
岐阜県  美濃
群馬県  前橋
千葉県  茂原
埼玉県  鳩山
大阪府  豊中
山梨県  大月
山形県  鶴岡
愛知県  名古屋
>diff <(cat merge.txt) <(paste col1.txt col2.txt)
>

14. 先頭からN行を出力

自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.

14.hs
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.Lazy as TextLazy
import qualified Data.Text.Lazy.IO as TextLazyIO
import qualified Text.Read as Read

import System.Environment(getArgs, getProgName)

head :: Int -> TextLazy.Text -> IO()
head n input = TextLazyIO.putStr $ head' n input
    where
        head' :: Int -> TextLazy.Text -> TextLazy.Text
        head' n input = TextLazy.unlines $ List.take n $ TextLazy.lines input

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

Data.Textの遅延評価版のData.Text.Lazyを使ってみたかったので使いました。
巨大なファイルだったら遅延評価版の意味はあるんでしょうけど、この規模だったらどうなんでしょ。
面倒なのでベンチマーク図ったりはしていません。

ターミナルにて確認
>stack runghc 14.hs 4
高知県  江川崎  41      2013-08-12
埼玉県  熊谷    40.9    2007-08-16
岐阜県  多治見  40.9    2007-08-16
山形県  山形    40.8    1933-07-25
>head -n4 hightemp.txt
高知県  江川崎  41      2013-08-12
埼玉県  熊谷    40.9    2007-08-16
岐阜県  多治見  40.9    2007-08-16
山形県  山形    40.8    1933-07-25
>diff <(head -n4 hightemp.txt) <(stack runghc 14.hs 4)
>

さいごに

どうも記事を途中まで書いていて1ヶ月以上放置していたようですがとりあえず世に出すことにしました。
第1章の解説に比べて、無味乾燥な記事になってしまった感じもあるし、後半はあるかわかりません。(そもそも需要ない説ある。。)