はじめに
Haskellで自然言語処理100本ノックの第2章を解いてみる。【前編】の続きです。
アクセス数やいいねを見て需要ない感じが半端じゃなく、更新してませんでしたが、ここにきてまさかの更新です!
解答に関して
- 簡潔さとわかりやすさ重視で書きました。
- 一部の問題は今までの問題より少しだけ難しめなので重視できなかったかもしれません。。
- 文字列はなるべくString型ではなくText型を扱うようにしました。
問題と解答
第2章: UNIXコマンドの基礎
hightemp.txtは,日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,hightemp.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.
15. 末尾のN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.
解答はこちら
-- 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コマンドで実現せよ.
-- 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でもゴリ押しのコードが書けるようになったと前向きに考えておく。
**writeLineFile
**は前編からそのまま引用しました。
**split
は前問のhead
と前前問のtail
の型シグネチャとわざわざ合致させるためになんとなく作った関数で、肝心な方はsplit'
**の方で行っている。
**split
でやっていることはN分割したときにList.take
で取ってきたい行の数のリスト(ms
がそうです..)を作成して、何分割目かを確認するためのindex値のリスト([0..]
がそうです..)と一緒にzipWithM_
**してしまおう!といった感じです。
**zipWithM_
**は返り値ぽいっとしてm()
とします。ここでのm
はApplicative
のインスタンスで今回はIO
がそれに該当します。
split'
はindex+1
分割目の役割を果たします。出力ファイル名はもちろん適当です。
17. 1列目の文字列の異なり
1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはsort, uniqコマンドを用いよ.
-- 17. 1列目の文字列の異なり
-- 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コマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい).
-- 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コマンドを用いよ.
この問題に関してはコラム表記と列表記が混じってますね。なんでだろう。。
-- 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
でも困ることはなさそうです。
さいごに
結局2章までやってしまったが、正規表現やパーサー書いてどうこうするというよくある感じのあれこれまで至っておらず、第3章もやるかもしれません。
正規表現やパーサー書くっていうのはいろいろやってきているのであまりやったことがないという理由でHaskell
続投する可能性があるかも…?