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


入出力系の処理はよく使うわりに、使い方をよく忘れてしまいます。
Clojureで入出力によく使う以下の６つの関数の用法を列挙してみました。

| 返り値[^1] | 入力 | 出力 |
|:--|:--|:--|
| `nil` | [slurp](#clojurecoreslurp) | [spit](#clojurecorespit) |
| `BufferedReader` / `BufferedWriter`            | [clojure.java.io/reader](#clojurejavaioreader) | [clojure.java.io/writer](#clojurejavaiowriter) |
| `BufferedInputStream` / `BufferedOutputStream` | [clojure.java.io/input-stream](#clojurejavaioinput-stream) | [clojure.java.io/output-stream](#clojurejavaiooutput-stream) |


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

元ネタは[clojuredocsのinput-streamのサンプルコード](https://clojuredocs.org/clojure.java.io/input-stream#example-56f54678e4b09295d75dbf3f)です。
また、その過程で`clojure.java.io/IOFactory`などのコードからClojureの抽象化の方法を少し学んだので、あわせて整理しました。




## 1. 入力系の関数

### [clojure.core/slurp](https://clojuredocs.org/clojure.core/slurp)
第１引数の対象を読み込んで[java.lang.String](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/String.html)で返します。

```clojure
;; 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](https://clojuredocs.org/clojure.java.io/reader)
第１引数に対して[java.io.BufferedReader](https://docs.oracle.com/javase/jp/8/docs/api/java/io/BufferedReader.html)を開いて返します。

```clojure
(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](https://clojuredocs.org/clojure.java.io/input-stream)
第１引数に対して[java.io.BufferedInputStream](https://docs.oracle.com/javase/jp/8/docs/api/java/io/BufferedInputStream.html)を開いて返します。

```clojure
(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`](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/core.clj#L6664)を呼んでいます。[^2]

[^2]: `jio`ネームスペースがrequireされた`clojure.java.io`の別名

```clojure
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`](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/java/io.clj#L102)を呼んでいます。

```clojure
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`](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/java/io.clj#L136)を呼んでいます。

```clojure
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`プロトコルを実装している](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/java/io.clj#L182-L293)ということです。




## 2. 出力系の関数

### [clojure.core/spit](https://clojuredocs.org/clojure.core/spit)
第１引数を`clojure.java.io/writer`で開いて、第２引数の[java.lang.String](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/String.html)を書き込んで、終わったら開いたストリームを閉じてくれます。

```clojure
;; 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などのスキーマは当然使えません。

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

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

```clojure
;; append mode option
(spit "foo.txt" "hello" :append true)

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


### [clojure.java.io/writer](https://clojuredocs.org/clojure.java.io/writer)
第１引数に対して[java.io.BufferedWriter](https://docs.oracle.com/javase/jp/8/docs/api/java/io/BufferedWriter.html)を開いて返します。

```clojure
(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などのスキーマは当然使えません。

```clojure
;; 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)
```

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

```clojure
;; append mode option
(writer "foo.txt" "hello" :append true)

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


### [clojure.java.io/output-stream](https://clojuredocs.org/clojure.java.io/output-stream)
第１引数に対して[java.io.BufferedOutputStream](https://docs.oracle.com/javase/jp/8/docs/api/java/io/BufferedOutputStream.html)を開いて返します。

```clojure
(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などのスキーマは当然使えません。

```clojure
(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つの出力系の関数もまた同様の値を引数に取ります。　入力系の関数と同様の構成です。

0. `spit`は[内部で`clojure.java.io/writer`](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/core.clj#L6673)を呼んでいます。

0. `clojure.java.io/writer`は[内部で`clojure.java.io/make-writer`](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/java/io.clj#L119)を呼んでいます。

0. そして、`clojure.java.io/output-stream`のほうは、[内部で`clojure.java.io/make-output-stream`](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/java/io.clj#L153)を呼んでいます。

0. また、`make-writer`/`make-output-stream`と`clojure.java.io/IOFactory`プロトコルとの関係も[同様](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/java/io.clj#L182-L293)です。




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

ちなみに、標準ライブラリにおける[`make-〜`関数のデフォルト実装（つまり`IOFactory`のデフォルト実装）](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/java/io.clj#L164-L172)は次の通りです。

```clojure
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`](https://github.com/clojure/clojure/blob/clojure-1.8.0/src/clj/clojure/java/io.clj#L285-L293)し、上記デフォルト実装の`make-reader`/`make-writer`が`IllegalArgumentException`をthrowするように上書き [^4] しています。[^5]

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

[^5]: `nil`って`extend`できたんだ・・

```clojure
(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`でやってみます。

```clojure
;; 素の状態では 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〜`を使用するのは、デフォルトの実装でしかなく、変更できる。

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