『プログラミング Clojure 第2版』を読んでるんだけど、なかなか難しい。
今回は、p.145『6.3 プロトコル』のところを読んでるんだけど、本に記述ミスが
あったこともあり、読むのにすごく時間がかかった。
(サンプルファイルも同様にミスがあった)
とりあえず、名前空間を以下のように設定する。
また、必要なパッケージを読み込む
(ns examples.io
(:import (java.io FileInputStream InputStreamReader BufferedReader)))
例として以下のようなコマンドを考えてみる。
ファイルの内容を1文字ずつ読み込むというコマンドである。
gulp というのは、「飲み込む」という意味。
(同書 p.142)
(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))))))))
(let [sb (StringBuilder.)])
String sb = new StringBuilder() というのをこのように書ける。
with-open — オープンのあとリソースを開放してくれる(closeしてくれる)
(loop [c (.read reader)]
1文字ずつ読み込む
.read は BufferedReader. のメソッド
(if (neg? c)
neg? — ゼロより小さい場合は true
この場合は、入力が最後に達したということ
(str sb) — StringBuilderクラスにおける toStringメソッド
(.append sb (char c))
.append — javaのStringBuilderのメソッド
(char c) — 整数値c のキャラクター文字を返す
たとえば (char 65) は \A である。
この gulp のコードを java で書いてみた。
Gulp.java
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
public class Gulp {
public Gulp () {}
public String read (String filename) {
StringBuilder sb = new StringBuilder();
try {
FileInputStream fis = new FileInputStream( filename );
InputStreamReader isr = new InputStreamReader( fis );
BufferedReader reader = new BufferedReader( isr );
int c = reader.read();
while (c != -1) {
sb.append( (char)c );
c = reader.read();
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
System.out.println("ファイルを開けませんでした。");
}
return sb.toString();
}
}
こんな感じになるかと思う。
上の gulp はファイルを入出力の対象としているが、これを様々な インプットデバイスに対応するように書き換えてみる。
まず、手始めに、様々なソースを読み込む meke-reader を定義する。
入力元として ファイル(java.io.File もしくは文字列によるファイル名)、socket、URL、あるいは既に InputStream である場合をサポートする。
(同書 p.144)
(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.))
この make-reader を使って gulp を書き換える。
(同書 p.143)
(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))))))))
make-reader は、新しい入力を追加するとなると、書き直す必要がある。
インターフェースはそれを実装する場合、クラスを書き換える必要がある。
プロトコルは、既存のデータ型に新たなインターフェースへの実装を追加できる。
IOFactory というプロトロルを定義する。
(同書 p.146
(defprotocol IOFactory
"A protocol for things that can be read from and written to."
(make-reader [this] "Creates a BufferedReader."))
(extend-protocol IOFactory
InputStream
(make-reader [src]
(-> src InputStreamReader. BufferedReader.))
File
(make-reader [src]
(make-reader (FileInputStream. src)))
Socket
(make-reader [src]
(make-reader (.getInputStream src)))
URL
(make-reader [src]
(make-reader
(if (= "file" (.getProtocol src))
(-> src .getPath FileInputStream.)
(.openStream src))))
このコードでは、上記 gulp を実行するとエラーが出る。
(gulp "abc.txt")
Execution error (IllegalArgumentException) at examples.io/eval5933$fn$G (my-protocol.clj:37).
No implementation of method: :make-reader of protocol: #'examples.io/IOFactory found for class: java.lang.String
このエラーの意味がわからなかった。
要するに、java.lang.String のクラスにあるIOFactoryのメソッドの実装がない
というような意味かな。
Clojureのエラーメッセージは、ちょっとわかりにくいなあ。
抜けている部分を追加したコードは、これ。
(extend-protocol IOFactory
InputStream
(make-reader [src]
(-> src InputStreamReader. BufferedReader.))
String ;; <=== <1>
(make-reader [src] ;; この3行が
(make-reader (FileInputStream. src))) ;; 抜けていた
File
(make-reader [src]
(make-reader (FileInputStream. src)))
Socket
(make-reader [src]
(make-reader (.getInputStream src)))
URL
(make-reader [src]
(make-reader
(if (= "file" (.getProtocol src))
(-> src .getPath FileInputStream.)
(.openStream src))))
最初の gulp のコードでは、
(defn make-reader [src] … ) のところで、
java.lang.String (FileInputStream. src)
とやってる。それが抜けてたのか。
まとめ
javaのインターフェースは、ケースに応じて処理を変えたいときなどに使えるけど、
インターフェースを実装するためには、実装するクラスの内容を書き換えなければ
ならない。
Clojureのプロトコルは、既存のコードを変更せずに、必要な処理を既存の関数に
つけたすことができる。
いかにも 関数型言語らしいアプローチだと思った。