clojureのファイルIO

プログラミングclojureは読み物としては面白いのですが、実際に自分でclojureのコードを書こうとするとすこし手間取ってしまいます。*1
ファイルIOもそのようなうちのひとつなのですが、clojureでのファイルIOについて少し調べてみました。
とりあえず以下のことができれば十分そうです。

  • ファイルを開いて(何か処理して)閉じる
    • 1行読み込めれば十分
  • ファイルの中身をstreamとして取り出す

あとはファイルへの出力の方もできればいいですね。

1行だけ読み込んでファイルをcloseする

よく分からないですけど、javaではBufferedReader FileInputStream InputStreamReaderを使うのが普通みたいです。
javaのコードをべたにclojureで書いた後に、clojureで利用できる関数を使って短くしていこうと思います。
(面倒だったら、最後の部分だけ見れば良いですね。)

(import '(java.io BufferedReader FileInputStream InputStreamReader))
(let [br (BufferedReader. (InputStreamReader. (FileInputStream. "rdic-io.clj")))]
  (println (. br readLine))
  (. br close))

with-openマクロを使いましょう。rubyのFile.openにブロックを渡した時と同様に自動的にcloseを読んでくれます。

(with-open [br (BufferedReader. (InputStreamReader. (FileInputStream. "rdic-io.clj")))]
  (println (. br readLine)))

BufferedReaderを手にするために複数のコンストラクタを多段に重ねるのは面倒ですね。他の方法を考えましょう。
たとえば、一度にやってくれる関数を作るというのもひとつの手です。*2

(defn file-reader [file]
  "receive filename and return received file's BuffredReader object."
  (BufferedReader. (InputStreamReader. (FileInputStream. file))))

(with-open [br (file-reader "rdic-io.clj")]
  (println (. br readLine)))

実際のところ、clojure.contrib.duck-streamsにもっと汎用的なreader関数が用意されています。
そんなわけで以下のように書けます。

(use '[clojure.contrib.duck-streams (:only reader)])
(with-open [r (reader "rdic-io.clj")]
  (println (. r readLine)))

out of topic

ファイルの中身をsequenceとして取り出す話に移る前にライブラリで提供しているメソッドを調べる方法についても書いて置きます。
既にnamespaceがrequireされているか調べるにはfind-nsで探せば良いです。

(find-ns 'clojure.contrib.duck-streams) ; => nil
(require 'clojure.contrib.duck-streams)
(find-ns 'clojure.contrib.duck-streams) ; => #<Namespace clojure.contrib.duck-streams>

以下のようにすればuseもしくはimportしたnamespaceで定義された関数などが見れます。

(keys (ns-interns 'clojure.contrib.duck-streams))
;;readerがあればwriterもあるのでしょうか?
(some #{'writer} (keys (ns-interns 'clojure.contrib.duck-streams))) ; => writer

ちなみにns-mapを利用するとinternされているとか関係なく渡したnamespaceで定義されたsymbolをMapとして手にすることが出来ます。

ファイルの中身をsequenceとして取り出す。

clojureなのにFor-loopを使いたくないですね。doseqなどをつかいたいです。
それにはファイルの中身をsequeneとして取り出せば良いです。*3
同様にjavaのメソッドを直に使った長い記述から徐々に短くしていきます。

(let [file "rdic-io.clj"
      br (BufferedReader. (InputStreamReader. (FileInputStream. file)))]
  (letfn [(read-lines*
	   [br]
	   ((fn step []
	      (lazy-seq
	       (if-let [line (. br readLine)]
		 (cons line (step))
		 (. br close))))))]
    (doseq [line (read-lines* br)]
      (println line))))

if-letはonLispなどでよくあるaifのようなものです。

(if-let [var pred]
    then-clause
    else-clause)
;;上の式は以下のように展開される(正確じゃない)
(let [tmp pred]
  (if tmp then-clause else-clause))

あとは単にletfnでread-lines*を定義してsequenceに変換したあと、doseqで一行ずつ取り出してprintしているだけですね。
実際のところ、read-lines*にあたる関数がreaderとどうように定義されていてそれを使えばもっと楽が出来ます。

(use '[clojure.contrib.duck-streams :only (reader read-lines)])
(let [file "rdic-io.clj"]		
  (doseq [line (read-lines (reader file))] ;readerも使っている
    (println line)))

read-linesは文字列を渡すとそれを自動的にファイル名として扱ってくれるのでもっと便利です。

(doseq [line (read-liens "rdic-io.clj")] 
  (println line))

ファイルへの書き込み

readerの代わりにwriter,write-linesを使えば良いですね。

(take 5 (iterate (comp char inc int) \a)) ; => (\a \b \c \d \e)
(write-lines (writer "abc.txt")
	     (take 30 (iterate (comp char inc int) \a)))

まとめ

ファイルを開く

(with-open (br (reader "rdic-io.clj"))
  (do-something br))

ファイルの読み込み

(doseq [line (read-lines "rdic-io.clj")] (println line))

ファイルへの書き込み

(write-lines (writer "nums.txt") (range 1 10))

*1:単にしっかり読んでいないだけかもしれないけれど

*2:始めはreduceなどを使って短くできないか考えたのですが無理でした。(special-formとmacroはfirst class objectじゃない)

*3:これについてはプログラミングclojureにも書いてあったような気がする