入出力系の処理はよく使うわりに、使い方をよく忘れてしまいます。
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〜
を使用するのは、デフォルトの実装でしかなく、変更できる。
単に、入出力の関数のサンプルコードの列挙を整理するつもりが、長くなってしまいました。。