TL;DR
spitとslurpは「読み込み」と「書き出し」の抽象化の上に実装されているので、ファイルや URL、ソケットなどを入出力に指定できる。- 練習として代替関数
gulpとexpectorateを実装し、condpで入出力の型ごとに処理を切り分けると、新しい入出力を足すたびに書き換えが要る「閉じた」コードになる。
内容
spit と slurp
Clojure の spit と slurp は、「読み込み」と「書き出し」の 2 つの抽象化の上に実装された入出力関数である。抽象化されているので、さまざまな入力元や出力先(ファイルや URL、ソケットなど)を指定できる。
slurp: 入力元を受け取り、中身をすべて読み込んで文字列として返すspit: 出力先と値を受け取り、値を文字列に変換して出力先へ送る
gulp と expectorate
練習として、slurp と spit の基本的な代替関数(gulp、expectorate)を実装する。
ファイルだけを対象とする基本的な実装
まずは、ファイル(java.io.File)だけを対象とする。
(ns programming-clojure.gulp
(:import
(java.io BufferedReader FileInputStream InputStreamReader)))
(defn gulp [src]
(let [sb (StringBuilder.)]
(with-open [reader (-> src
FileInputStream.
InputStreamReader.
BufferedReader.)]
(loop [c (.read reader)]
(if (neg? c)
(str sb)
(do
(.append sb (char c))
(recur (.read reader))))))))gulp 関数は受け取った File オブジェクトから BufferedReader を作り、loop 〜 recur で 1 文字ずつ読み込んで StringBuilder に格納していく。そして入力が最後まで達したら、文字列を返す。
expectorate 関数はより簡単に書ける。
(ns programming-clojure.expectorate
(:import
(java.io BufferedWriter FileOutputStream OutputStreamWriter)))
(defn expectorate [dst content]
(with-open [writer (-> dst
FileOutputStream.
OutputStreamWriter.
BufferedWriter.)]
(.write writer (str content))))BufferedWriter を作り、content 引数の値を文字列に直して書き出す。
リーダとライタの作成を関数に切り出す
ファイル以外にも対応できるように、まずはリーダとライタの作成部分を関数に切り出しておく。
make-reader: 与えられた入力元からBufferedReaderを作るmake-writer: 与えられた出力先からBufferedWriterを作る
(defn make-reader [src]
(-> src FileInputStream. InputStreamReader. BufferedReader.))
(defn make-writer [dst]
(-> dst FileOutputStream. OutputStreamWriter. BufferedWriter.))gulp と expectorate 関数で、これらの関数を使うように変える。
(defn gulp [src]
(let [sb (StringBuilder.)]
(with-open [reader (make-reader src)]
(loop [c (.read reader)]
(if (neg? c)
(str sb)
(do
(.append sb (char c))
(recur (.read reader))))))))
(defn expectorate [dst content]
(with-open [writer (make-writer dst)]
(.write writer (str content))))ファイル以外の入出力を扱うようにする
gulp と expectorate でファイル以外の入出力を扱うには、make-reader と make-writer を変えればよい。その中でも最も簡単なのは、cond や condp を使い、入出力の型に応じて処理を切り分ける方法である。
(defn make-reader [src]
(-> (condp = (type src)
java.io.InputStream src
java.lang.String (FileInputStream. src)
java.io.File (FileInputStream. src)
java.net.Socket (.getInputStream src)
java.net.URL (if (= "file" (.getProtocol src))
(-> src .getPath FileInputStream)
(.openStream src)))
InputStreamReader.
BufferedReader.))
(defn make-writer [dst]
(-> (condp = (type dst)
java.io.OutputStream dst
java.lang.String (FileOutputStream. dst)
java.io.File (FileOutputStream. dst)
java.net.Socket (.getOutputStream dst)
java.net.URL (if (= "file" (.getProtocol dst))
(-> dst .getPath FileOutputStream.)
(throw (IllegalArgumentException.
"Can't write to non-file URL"))))
OutputStreamWriter.
BufferedWriter.))このアプローチの問題は、コードが「閉じている」ことである。つまり、新しい入出力を足したい場合、make-reader と make-writer を必ず書き換える必要がある。
求めるのは、後から簡単に入出力を追加できる「開いた」アプローチである。そのためには、読み込みと書き出しのそれぞれについて抽象化が必要である(Programming Clojure, Third Edition 第7章 インタフェース)。