入出力系の処理はよく使うわりに、使い方をよく忘れてしまいます。
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-readerとmake-input-streamは、clojure.java.io/IOFactoryプロトコルで定義されています。つまり、clojure.java.io/IOFactoryプロトコルを実装すれば、そのデータ型はslurp、reader、input-streamの3つの関数で処理できるようになります。ということは、java.lang.Stringやjava.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つの出力系の関数もまた同様の値を引数に取ります。 入力系の関数と同様の構成です。
-
spitは内部でclojure.java.io/writerを呼んでいます。 -
clojure.java.io/writerは内部でclojure.java.io/make-writerを呼んでいます。 -
そして、
clojure.java.io/output-streamのほうは、内部でclojure.java.io/make-output-streamを呼んでいます。 -
また、
make-writer/make-output-streamとclojure.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します。
さらに以下のようにnilがIOFactoryをextendし、上記デフォルト実装のmake-reader/make-writerがIllegalArgumentExceptionを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."))))))
なので、slurpやspitなどにnilを食わせると、NullPointerExceptionではなく(分かりやすい例外メッセージを持った)IllegalArgumentExceptionがthrowされます。
4. 入出力系の関数に任意のデータ型を対応させる
上記で見たように、slurpやspitなどの引数に、任意のデータ型を食わせることができるようにするには、そのデータ型が単に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プロトコルで抽象化されており、引数の構成も似ています。 - これらの関数は、
Stringやjava.io.File、java.net.URLなどの多くのクラスをサポートしています。 - それは、Clojureの標準ライブラリ内で
IOFactoryをextendしているためです。 - これらの関数がある特定のデータ型をサポートするようにしたい場合、
IOFactoryをextendすれば良い。 - これらの関数が
Buffered〜を使用するのは、デフォルトの実装でしかなく、変更できる。
単に、入出力の関数のサンプルコードの列挙を整理するつもりが、長くなってしまいました。。