Haskell で簡単な CLI ツール習作
intro
すごい H 本で、 CLI の todo ツールを作るところまで読み終えました。
これでやっと、動くツール的なものが作れるようになったので、
復讐と練習を兼ねて、ちょっとツールを作ってみようかと思い、作ってみました。
今回作ったのは、非常に簡単なファイル削除ツールです。
(今思うと、引数も入力もない、ツールってかバッチ?w)
Haskell を写経しながらコンパイルして動かしたりしていると、
気がついたらディレクトリ内がこんな感じになっています。
. ├── capslocker ├── capslocker.hi ├── capslocker.hs ├── capslocker.o ├── shortLine ├── shortLine.hi ├── shortLine.hs ├── shortLine.o
で、ついカッとなって .hs 以外のファイルを一気に削除したくなったので、
それをツールにしてみようかと。
処理の流れ
- ディレクトリを取得
- ファイル名一覧を取得
- ファイル名から拡張子を取得
- .hs 以外のファイルのリストを取得
- それをごっそり消す
ちょっとづつ進めたいので、とりあえず型宣言は書かず、
出来上がってから型を書く感じで。
ファイル一覧取得
とりあえずカレントのファイル一覧取得。
System.Directory の関数を使う。
import System.Directory main = do d <- getCurrentDirectory c <- getDirectoryContents d print c
$ ./cleanup [".","..","cleanup","cleanup.hi","cleanup.hs","cleanup.o"]
d, c とかこの辺の命名はどんなもんなんだろ。
拡張子切り出し
haskell で split 的なことがしたい。
Data.List に splitAt というのがあるらしい。
Prelude> import Data.List Prelude Data.List> splitAt 4 "hoge.hs" ("hoge",".hs")
あーでも、これインデックスで割るのか。
じゃあ、インデックスを出したい。
これかな? findIndex
ドットとの比較を述語に指定(ここでは最初のドットしか見れないけど、ファイル名中ドットは一個ということで)
Prelude> import Data.List Prelude Data.List> findIndex (=='.') "hoge.hs" Just 4
'.' がない場合があるから、 Maybe Int が返るわけか。
Maybe Int が返るため、このままさっきの splitAt と組み合わせるわけにはいかない。(splitAt は Int が必要)
Prelude Data.List> splitAt (findIndex (=='.') "hoge.hs") "hoge.hs" <interactive>:1:10: Couldn't match expected type `Int' with actual type `Maybe Int' In the return type of a call of `findIndex' In the first argument of `splitAt', namely `(findIndex (== '.') "hoge.hs")' In the expression: splitAt (findIndex (== '.') "hoge.hs") "hoge.hs"
なるほど。
ということは、 Just n と Nothing でパターンをわけないといけないのかな。
import System.Directory import Data.List findDotIndex a = case findIndex (=='.') a of Just x -> x otherwise -> 0 splitAtDot x = splitAt (findDotIndex x) x main = do d <- getCurrentDirectory c <- getDirectoryContents d let splitted = map splitAtDot c print splitted
うーん、小さめの変数の命名作法が掴めてないことがわかった。
$ ./cleanup [("","."),("",".."),("",".git"),("","cleanup"),("cleanup",".hi"),("cleanup",".hs"),("cleanup",".o")]
とりあえず、タプルのリストになることは、なった。
.hs 以外のファイルのリストを取得
というか、消したい奴だけ選ばないといけない。
消去法で消したくない奴だけ、削ることにする。
Filter で True のやつだけが残るんだから、
パターンマッチで、消したくない奴だけ Flase を返せばいいのかな?
名前は、、
import System.Directory import Data.List findDotIndex a = case findIndex (=='.') a of Just x -> x otherwise -> 0 splitAtDot x = splitAt (findDotIndex x) x forRemove (_, ".") = False forRemove (_, "..") = False forRemove (_, ".hs") = False forRemove (_, ".git") = False forRemove (_, "cleanup") = False forRemove _ = True main = do d <- getCurrentDirectory c <- getDirectoryContents d let splitted = map splitAtDot c let target = filter forRemove splitted print target
これ cleanup.hs で、ビルドしたバイナリファイル(実行可能な cleanup) を置いて実行するから、それも残さないとだった。
なんか、こう、綺麗な感じがあまりしない。が、しかたない。。
対象の削除
削り出しが終わったので、タプルからファイル名を復元して消す。
Map とラムダでファイル名復元して、それをまた let で束縛したけど、
この時点で let での一時的な束縛が、三列ならんでしまった。
これは、どうなんだろ。もうすこし繋げて書くべき?
let splitted = map splitAtDot c let target = filter isntHs splitted let targetFiles = map (\(a,b) -> a++b) target print targetFiles
$ ./cleanup ["cleanup.hi","cleanup.o"]
で、作った対象ファイルを、 removeFile で削除。
ラムダを mapM した。(本にあった例外処理は、ちょっと割愛した。)
まだ mapM のあたりが、完全には理解できてないなぁ。
アクションが絡むから、程度の理解。復習しないと。
完成
最後に型情報を書き足した。
haskeller はこれ、 TDD 的な感じで、最初から書いていくのかな?
import System.Directory import Data.List findDotIndex :: String -> Int findDotIndex a = case findIndex (=='.') a of Just x -> x otherwise -> 0 splitAtDot :: String -> (String, String) splitAtDot x = splitAt (findDotIndex x) x forRemove :: (String, String) -> Bool forRemove (_, ".") = False forRemove (_, "..") = False forRemove (_, ".hs") = False forRemove (_, ".git") = False forRemove (_, "cleanup") = False forRemove _ = True main = do d <- getCurrentDirectory c <- getDirectoryContents d let splitted = map splitAtDot c let target = filter forRemove splitted let targetFiles = map (\(a, b) -> a ++ b) target mapM (\file -> removeFile file) targetFiles
反省点
これだけでも案外時間がかかってしまった。
特に、実行経過をところどころで print デバッグしていく流れが、
できるまでは進めにくかった。
今回は ghci と print を使って進められたけど、
この辺ももう少し作法が知りたいところ。
気づいたのは。
- 短い変数の命名習慣がわからない
- let での一時的な束縛はどの程度つかうのか
- 型宣言は、いつ書くのか
- 例外処理もっとする
- mapM まわりもう少し理解
まあ、学び始めの黒歴史として取っておこうと思いました。
すごい H 本は本当に良書だと思うので、
引き続き読み進めて、作法も一緒に学びたいです。
splitOn だと?
hackage には、 splitOn (文字で区切れる) があった模様。
http://hackage.haskell.org/packages/archive/split/0.1.1/doc/html/Data-List-Split.html
まあ、でも習作なのでよい。