Edited at

Clojureの入出力の関数とIOFactoryプロトコル

More than 3 years have passed since last update.

入出力系の処理はよく使うわりに、使い方をよく忘れてしまいます。

Clojureで入出力によく使う以下の6つの関数の用法を列挙してみました。

返り値1

入力
出力

nil
slurp
spit

BufferedReader / BufferedWriter

clojure.java.io/reader
clojure.java.io/writer

BufferedInputStream / BufferedOutputStream

clojure.java.io/input-stream
clojure.java.io/output-stream

元ネタはclojuredocsのinput-streamのサンプルコードです。

また、その過程でclojure.java.io/IOFactoryなどのコードからClojureの抽象化の方法を少し学んだので、あわせて整理しました。


1. 入力系の関数


clojure.core/slurp

第1引数の対象を読み込んでjava.lang.Stringで返します。

;; local file

(slurp "foo.txt")
(slurp "/tmp/foo.txt")
(slurp "file:///tmp/foo.txt")
(slurp (java.io.File. "/tmp/foo.txt"))
(slurp (java.io.FileInputStream. "/tmp/foo.txt"))
(slurp (java.net.URL. "file:///tmp/foo.txt"))
(slurp (java.net.URI. "file:///tmp/foo.txt"))

;; remote resource

(slurp "http://clojuredocs.org/")
(slurp (java.net.URL. "http://clojuredocs.org"))
(slurp (java.net.URI. "http://clojuredocs.org"))
(let [socket (java.net.Socket. "clojuredocs.org" 80)
out (java.io.PrintStream. (.getOutputStream socket))]
(.println out "GET /index.html HTTP/1.0")
(.println out "Host: clojuredocs.org\n\n")
(slurp socket))

;; in memory source

(slurp (.getBytes "text"))
(slurp (java.io.ByteArrayInputStream. (.getBytes "text")))
(slurp (byte-array [116 101 120 116]))


clojure.java.io/reader

第1引数に対してjava.io.BufferedReaderを開いて返します。

(require '[clojure.java.io :refer [reader]])

;; local file

(reader "foo.txt")
(reader "/tmp/foo.txt")
(reader "file:///tmp/foo.txt")
(reader (java.io.File. "/tmp/foo.txt"))
(reader (java.io.FileInputStream. "/tmp/foo.txt"))
(reader (java.net.URL. "file:///tmp/foo.txt"))
(reader (java.net.URI. "file:///tmp/foo.txt"))

;; remote resource

(reader "http://clojuredocs.org/")
(reader (java.net.URL. "http://clojuredocs.org"))
(reader (java.net.URI. "http://clojuredocs.org"))
(let [socket (java.net.Socket. "clojuredocs.org" 80)
out (java.io.PrintStream. (.getOutputStream socket))]
(.println out "GET /index.html HTTP/1.0")
(.println out "Host: clojuredocs.org\n\n")
(reader socket))

;; in memory source

(reader (.getBytes "text"))
(reader (java.io.ByteArrayInputStream. (.getBytes "text")))
(reader (byte-array [116 101 120 116]))


clojure.java.io/input-stream

第1引数に対してjava.io.BufferedInputStreamを開いて返します。

(require '[clojure.java.io :refer [input-stream]])

;; local file

(input-stream "foo.txt")
(input-stream "/tmp/foo.txt")
(input-stream "file:///tmp/foo.txt")
(input-stream (java.io.File. "/tmp/foo.txt"))
(input-stream (java.io.FileInputStream. "/tmp/foo.txt"))
(input-stream (java.net.URL. "file:///tmp/foo.txt"))
(input-stream (java.net.URI. "file:///tmp/foo.txt"))

;; remote resource

(input-stream "http://clojuredocs.org/")
(input-stream (java.net.URL. "http://clojuredocs.org"))
(input-stream (java.net.URI. "http://clojuredocs.org"))
(let [socket (java.net.Socket. "clojuredocs.org" 80)
out (java.io.PrintStream. (.getOutputStream socket))]
(.println out "GET /index.html HTTP/1.0")
(.println out "Host: clojuredocs.org\n\n")
(input-stream socket))

;; in memory source

(input-stream (.getBytes "text"))
(input-stream (java.io.ByteArrayInputStream. (.getBytes "text")))
(input-stream (byte-array [116 101 120 116]))


入力系の各関数の関係

これら3つの入力系の関数は同様の値を引数に取るようです。

関数の実装を見ていきます。

slurp内部でclojure.java.io/readerを呼んでいます。2

user=> (source slurp)

(defn slurp
"docstringは省略"
{:added "1.0"}
([f & opts]
(let [opts (normalize-slurp-opts opts)
sw (java.io.StringWriter.)]
(with-open [^java.io.Reader r (apply jio/reader f opts)]
(jio/copy r sw)
(.toString sw)))))

clojure.java.io/reader内部でclojure.java.io/make-readerを呼んでいます。

user=> (source clojure.java.io/reader)

(defn ^Reader reader
"docstringは省略"
{:added "1.2"}
[x & opts]
(make-reader x (when opts (apply hash-map opts))))

そして、clojure.java.io/input-streamのほうは、内部でclojure.java.io/make-input-streamを呼んでいます。

user=> (source clojure.java.io/input-stream)

(defn ^InputStream input-stream
"docstringは省略"
{:added "1.2"}
[x & opts]
(make-input-stream x (when opts (apply hash-map opts))))

このmake-readermake-input-streamは、clojure.java.io/IOFactoryプロトコルで定義されています。つまり、clojure.java.io/IOFactoryプロトコルを実装すれば、そのデータ型はslurpreaderinput-streamの3つの関数で処理できるようになります。ということは、java.lang.Stringjava.io.Fileなどなどは、Clojureの標準ライブラリ内でclojure.java.io/IOFactoryプロトコルを実装しているということです。


2. 出力系の関数


clojure.core/spit

第1引数をclojure.java.io/writerで開いて、第2引数のjava.lang.Stringを書き込んで、終わったら開いたストリームを閉じてくれます。

;; local file

(spit "foo.txt" "hello")
(spit "/tmp/foo.txt" "hello")
(spit "file:///tmp/foo.txt" "hello")
(spit (java.io.File. "/tmp/foo.txt") "hello")
(spit (java.io.FileOutputStream. "/tmp/foo.txt") "hello")
(spit (java.net.URL. "file:///tmp/foo.txt") "hello")
(spit (java.net.URI. "file:///tmp/foo.txt") "hello")

URIを使用する場合はhttpなどのスキーマは当然使えません。

(spit "http://clojuredocs.org/" "hello")

;=> IllegalArgumentException Can not write to non-file URL <http://clojuredocs.org/> clojure.java.io/fn--9526 (io.clj:242)

第3引数でいくつかのオプションが指定できます。

;; append mode option

(spit "foo.txt" "hello" :append true)

;; encoding option
(spit "foo.txt" "ハロー" :encoding "EUC-JP")


clojure.java.io/writer

第1引数に対してjava.io.BufferedWriterを開いて返します。

(require '[clojure.java.io :refer [writer]])

;; local file

(with-open [w (writer "foo.txt")]
(.write w "hello"))

(with-open [w (writer "/tmp/foo.txt")]
(.write w "hello"))

(with-open [w (writer "file:///tmp/foo.txt")]
(.write w "hello"))

(with-open [w (writer (java.io.File. "/tmp/foo.txt"))]
(.write w "hello"))

(with-open [w (writer (java.io.FileOutputStream. "/tmp/foo.txt"))]
(.write w "hello"))

(with-open [w (writer (java.net.URL. "file:///tmp/foo.txt"))]
(.write w "hello"))

(with-open [w (writer (java.net.URI. "file:///tmp/foo.txt"))]
(.write w "hello"))

URIを使用する場合はhttpなどのスキーマは当然使えません。

;; can not write not write to non-file URL

(writer "http://clojuredocs.org/")
;=> IllegalArgumentException Can not write to non-file URL <http://clojuredocs.org/> clojure.java.io/fn--9526 (io.clj:242)

第3引数でいくつかのオプションが指定できます。

;; append mode option

(writer "foo.txt" "hello" :append true)

;; encoding option
(writer "foo.txt" "ハロー" :encoding "EUC-JP")


clojure.java.io/output-stream

第1引数に対してjava.io.BufferedOutputStreamを開いて返します。

(require '[clojure.java.io :refer [output-stream]])

;; local file

(with-open [o (output-stream "foo.txt")]
(doseq [c [104 101 108 108 111]] (.write o c)))

(with-open [o (output-stream "/tmp/foo.txt")]
(doseq [c [104 101 108 108 111]] (.write o c)))

(with-open [o (output-stream "file:///tmp/foo.txt")]
(doseq [c [104 101 108 108 111]] (.write o c)))

(with-open [o (output-stream (java.io.File. "/tmp/foo.txt"))]
(doseq [c [104 101 108 108 111]] (.write o c)))

(with-open [o (output-stream (java.io.FileInputStream. "/tmp/foo.txt"))]
(doseq [c [104 101 108 108 111]] (.write o c)))

(with-open [o (output-stream (java.net.URL. "file:///tmp/foo.txt"))]
(doseq [c [104 101 108 108 111]] (.write o c)))

(with-open [o (output-stream (java.net.URI. "file:///tmp/foo.txt"))]
(doseq [c [104 101 108 108 111]] (.write o c)))

URIを使用する場合はhttpなどのスキーマは当然使えません。

(output-stream "http://clojuredocs.org/")

;=> IllegalArgumentException Can not write to non-file URL <http://clojuredocs.org/> clojure.java.io/fn--9526 (io.clj:242)


出力系の各関数の関係

これら3つの出力系の関数もまた同様の値を引数に取ります。 入力系の関数と同様の構成です。


  1. spit内部でclojure.java.io/writerを呼んでいます。


  2. clojure.java.io/writer内部でclojure.java.io/make-writerを呼んでいます。


  3. そして、clojure.java.io/output-streamのほうは、内部でclojure.java.io/make-output-streamを呼んでいます。


  4. また、make-writer/make-output-streamclojure.java.io/IOFactoryプロトコルとの関係も同様です。



3. IOFactoryデフォルト実装と、nilの実装

ちなみに、標準ライブラリにおけるmake-〜関数のデフォルト実装(つまりIOFactoryのデフォルト実装)は次の通りです。

user=> (source clojure.java.io/default-streams-impl)

(def default-streams-impl
{:make-reader (fn [x opts] (make-reader (make-input-stream x opts) opts))
:make-writer (fn [x opts] (make-writer (make-output-stream x opts) opts))
:make-input-stream (fn [x opts]
(throw (IllegalArgumentException.
(str "Cannot open <" (pr-str x) "> as an InputStream."))))
:make-output-stream (fn [x opts]
(throw (IllegalArgumentException.
(str "Cannot open <" (pr-str x) "> as an OutputStream."))))})

make-input-stream/make-output-streamは、IllegalArgumentExceptionをthrowします。

さらに以下のようにnilIOFactoryextendし、上記デフォルト実装のmake-reader/make-writerIllegalArgumentExceptionをthrowするように上書き 3 しています。4

(extend nil

IOFactory
(assoc default-streams-impl
:make-reader (fn [x opts]
(throw (IllegalArgumentException.
(str "Cannot open <" (pr-str x) "> as a Reader."))))
:make-writer (fn [x opts]
(throw (IllegalArgumentException.
(str "Cannot open <" (pr-str x) "> as a Writer."))))))

なので、slurpspitなどにnilを食わせると、NullPointerExceptionではなく(分かりやすい例外メッセージを持った)IllegalArgumentExceptionがthrowされます。


4. 入出力系の関数に任意のデータ型を対応させる

上記で見たように、slurpspitなどの引数に、任意のデータ型を食わせることができるようにするには、そのデータ型が単にclojure.java.io/IOFactoryプロトコルを実装すれば良いようです。

java.lang.Longでやってみます。

;; 素の状態では slurp は当然例外を投げる

user=> (spit 123 "hello")

IllegalArgumentException Cannot open <123> as an OutputStream. clojure.java.io/fn--9500 (io.clj:170)

;; 素の状態では spit も当然例外を投げる
user=> (slurp 123)

IllegalArgumentException Cannot open <123> as an InputStream. clojure.java.io/fn--9498 (io.clj:167)

;; Long を extend する
user=> (extend Long
#_=> clojure.java.io/IOFactory
#_=> {:make-reader (fn [^Long x opts] (clojure.java.io/make-reader (str x) opts))
#_=> :make-writer (fn [^Long x opts] (clojure.java.io/make-writer (str x) opts))
#_=> :make-input-stream (fn [^Long x opts]
#_=> (clojure.java.io/make-input-stream (str x) opts))
#_=> :make-output-stream (fn [^Long x opts]
#_=> (clojure.java.io/make-output-stream (str x) opts))})
nil

;; 入出力系の関数が利用できるようになった
user=> (spit 123 "hello")
nil
user=> (slurp 123)
"hello"

実装した挙動は数値をファイル名として扱うというてきとうな内容ですが、うまくできました。


5. まとめ


  • 入出力系の6つの関数は、clojure.java.io/IOFactoryプロトコルで抽象化されており、引数の構成も似ています。

  • これらの関数は、Stringjava.io.Filejava.net.URLなどの多くのクラスをサポートしています。

  • それは、Clojureの標準ライブラリ内でIOFactoryextendしているためです。

  • これらの関数がある特定のデータ型をサポートするようにしたい場合、IOFactoryextendすれば良い。

  • これらの関数がBuffered〜を使用するのは、デフォルトの実装でしかなく、変更できる。

単に、入出力の関数のサンプルコードの列挙を整理するつもりが、長くなってしまいました。。





  1. Buffered〜のバッファ系のインスタンスが使用されるのは単にデフォルトの実装の挙動です。各関数の宣言で書かれている型は、それぞれjava.io.Reader, java.io.Writer, java.io.InputStream, java.io.OutputStreamです。 



  2. jioネームスペースがrequireされたclojure.java.ioの別名 



  3. extendでプロトコルの実装を指定するのには単なるマップを使用するため、デフォルト実装のマップに対して変更したい関数をassocで上書きしています。 



  4. nilってextendできたんだ・・