Block Rockin’ Codes

back with another one of those block rockin' codes

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 の関数を使う。

Haskell : getCurrentDirectory

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

ソース

ここまでのもの。

https://gist.github.com/2910543

反省点

これだけでも案外時間がかかってしまった。
特に、実行経過をところどころで 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

まあ、でも習作なのでよい。