はじめに
Haskellで自然言語処理100本ノックの第1章を解いてみる。【後編】の続きです。
正直、第2章はHaskellで解く気はなくてRustやPerl6あたりでも使ってみるかと思ったのですが、なんだかんだ言ってHaskellで書くの楽だしもう少しだけHaskellで解いてみるかという感じでやってきました。
解答に関して
第1章を解く際に掲げたこころざしを再掲しておきます。
簡潔さとわかりやすさ重視で書きました。
が、Haskell初心者がかいたものなのでたいして簡潔になってないかもしれません。
ただ、わかりやすさと天秤にかけたらわかりやすさのほうがおもいかな。
Haskellは抽象的にかけるぶんむずかしくなってしまうので。
あと、今回は文字列はなるべくString
型ではなくText
型を扱うようにしました。
文字列を文字のリストとして扱いたい場合でも大方これで大丈夫そうだったのとこっちのほうが推奨されてそうだったので。
問題と解答
第2章: UNIXコマンドの基礎
hightemp.txtは,日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,hightemp.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.
10. 行数のカウント
行数をカウントせよ.確認にはwcコマンドを用いよ.
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 -l hightemp.txt
24 hightemp.txt
11. タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.
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 '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コマンドを用いよ.
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
はテキストのリストに対しhPutStrLn
をmapM_
していき内容を対象のファイルに吐き出している。
>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コマンドを用いよ.
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.txt
とcol2.txt
の内容をzipWith
でくっつけていく。
くっつける関数は今回Textを扱うのでListを扱う際に使う++などではなくappend
やらsnoc
を使った。
>cat merge.txt
高知県 江川崎
埼玉県 熊谷
岐阜県 多治見
山形県 山形
山梨県 甲府
和歌山県 かつらぎ
静岡県 天竜
山梨県 勝沼
埼玉県 越谷
群馬県 館林
群馬県 上里見
愛知県 愛西
千葉県 牛久
静岡県 佐久間
愛媛県 宇和島
山形県 酒田
岐阜県 美濃
群馬県 前橋
千葉県 茂原
埼玉県 鳩山
大阪府 豊中
山梨県 大月
山形県 鶴岡
愛知県 名古屋
>diff <(cat merge.txt) <(paste col1.txt col2.txt)
>
14. 先頭からN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.
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章の解説に比べて、無味乾燥な記事になってしまった感じもあるし、後半はあるかわかりません。(そもそも需要ない説ある。。)