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にも書いてあったような気がする

contrib.seq-utilsを使ってみた。

使い方のカタログ的なもの。
(use 'clojure.contrib.seq-utils)が必要。

separate

gaucheのpartitionみたいなもの。述語をひとつ取り真か偽かで仕分けする。

(def nums (take 10 (iterate inc 0))) 
(filter odd? nums) ; => (1 3 5 7 9)
(filter (complement odd?) nums) ; => (0 2 4 6 8)
(separate odd? nums) ; => [(1 3 5 7 9) (0 2 4 6 8)]

rec-seq

自身を利用した遅延sequenceを作る際に便利なマクロ。reductionsを定義するのに使われている。

(let [c [1 2 3]] (rec-seq c [10 20 30])) ; => (10 20 30)
(let [c [1 2 3]] (rec-seq c (concat [10 20 30] c))) ; => (10 20 30 10 20 30 10 20 30 10 ...)
(let [coll [10 1 2 3 4]]
  (rec-seq self (cons (first coll) (map + self (rest coll))))) ; => (10 11 13 16 20)

indexed

clojure版with-indexと考えれば良い。
(indexed (take 5 (iterate (comp char inc int) \a))) ; => ([0 \a] [1 \b] [2 \c] [3 \d] [4 \e])

partition-by

haskellのgroup関数の汎用版.どのようにgroupingするのか述語を取れる

(partition-by identity nums)
;; => ((0) (1) (2) (3) (4) (5) (6) (7) (8) (9))
(partition-by identity '(1 1 1 2 2 2 1 1 2 1 1 1 1 2 2 2))
 ;; => ((1 1 1) (2 2 2) (1 1) (2) (1 1 1 1) (2 2 2))

rec-cat

lazy-catをもう少し楽に行うためのマクロ。フィボナッチ数列を楽に書ける。

(def fibs (lazy-cat [0 1] (map + fibs (next fibs))))
(rec-cat fibs [0 1] (map + fibs (next fibs)))

frequencies

集計するときに使える。自分で書いてもそんなに面倒じゃない。名前が覚えられない。

(frequencies '(1 1 1 2 2 2 1)) ; => {2 3, 1 4}
(reduce #(assoc %1 %2 (inc (get %1 %2 0))) {} '(1 1 1 2 2 2 1)) ; => {2 3, 1 4}

shuffle

名前のとおり。sort-byで似非shuffleをする必要がなくなった。

(shuffle (take 10 nums))  ; => (3 0 7 1 4 9 8 5 6 2)

reductions

mapaccumに近いようなもの。この関数のソースを見るとrec-seqの使い方が分かる。

(reductions + [10 0 0 0]) ; => (10 10 10 10)
(reductions + [10 1 2 3]) ; => (10 11 13 16)
(reductions vector [10 1 2 3]) ; => (10 [10 1] [[10 1] 2] [[[10 1] 2] 3])
(reductions * [1 2 3 4 5]) ; => (1 2 6 24 120)

seq-on

seqのようなものmulti-methodを使うときに利用するみたい。分からない。

rotations

全てのrotationの可能性を返す

(rotations [1 2 3])  ; => ((1 2 3) (2 3 1) (3 1 2))

partition-all

partitionの末尾まで返すversion。gaucheのutil.listのslicesはこんな感じ

(partition 4 nums) ; => ((0 1 2 3) (4 5 6 7))
(partition-all 4 nums) ; => ((0 1 2 3) (4 5 6 7) (8 9))

group-by

渡した関数でgroupingしたsortedMapを返す。(partition-byはlazy-sequence)

(defn abs [x] (if (neg? x) (- x) x))
(group-by abs [-1 -2 0 1 2]) ; => {0 [0], 1 [-1 1], 2 [-2 2]}

includes?

member的なもの

(includes?(take 5 nums) 5) ; => false
(includes?(take 5 nums) 3) ; => true

find-first

find-firstがよくあるプログラミングでのfind。
ソースを見て気づいたけれど、lazy-sequenceだから(comp first filter)で良いのか

((comp first filter) odd? [2 1 3 4]) ; => 1
(find-first odd? [2 1 3 4]) ; => 1

flatten

これはsourceで中を覗いてみるべき関数かもしれない。seq?ではなくsequential?を使うとか,
filter+(complement sequential?)の前にrestをつけるとか地味に気づかない。*1

rand-elt

randomに要素を選択する(重複あり)

(let [coll (range 1 5)]
  (map (fn [_] (rand-elt coll)) coll)) ; => (3 2 4 2)

*1:単にあまり考えていないだけだけれど