夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Clojure勉強日記(その25 抽象化に向けたプログラミング

こんにちは。今回からまた話題が変わります。

「抽象化」というJavaでもよくつかわれる技法Clojure独自の抽象化方式で
どう表されるのか・・という内容です。
元々、Clojureはproxyとgenclassを用いて「Javaの抽象化をうまく利用できる」という方針でした。
ですが、プロトコルという概念の導入により、事情は大きく変わったようです。

内容としては以下のようです。

  • プロトコルJavaのインタフェースの代替として、高性能な多態メソッドディスパッチを可能にする。
  • データ型はJavaクラスの代替として、プロトコルやインタフェースで規定された抽象化の実装を作るのに使う。

このプロトコルとデータ型の導入によって、Javaのインタフェース/クラスを書かずとも
柔軟な抽象化と実装をClojureで書くことができるようになったそうです。
新たな抽象化を定義してそれらを実装する新たなデータ型を書くこともできるし、
既にあるデータ型に新しい抽象化を後付けすることもできる・・・

ということのため、実際にClojureJavaの抽象化/拡張をなぞって確認して行く形になります。

1.抽象化へ向けたプログラミング

spitとslurp関数は「読み込み」「書き出し」の2つの抽象化の上に実装された入出力関数です。
抽象化のおかげで様々な入力元、出力先(ファイル、URL、ソケット、後はメモリ)を使うことができ、
かつ新しい入力元、出力先を拡張することができる。

  • slurp関数は入力元を受け取り、中身を全て読みだして文字列として返す
  • spit関数は出力先と値を受け取り、値を文字列に変換して出力先に送り出す

・・・なのですが、この関数を使ったらこういうのができるよ、でおわるため、
実際に抽象化がどういう過程で行われるかを実際に関数を拡張しながら作ってみます。

とりあえず、まずはファイルからのみ読み込めるmin-spitを。

(ns reader.min-spit
  (:import (java.io FileInputStream InputStreamReader BufferedReader)))

(defn min-spit [src]
    (let [sb (new StringBuilder)]
        (with-open [reader (-> src FileInputStream. InputStreamReader. BufferedReader.)]
            (loop [readChar (.read reader)]
                (if (neg? readChar) (str sb) 
                    (do (.append sb (char readChar))
                        (recur (.read reader))))))))
user=> (load-file "src/reader/min-spit.clj")
#'reader.min-spit/min-spit
user=> (use 'reader.min-spit)
nil
user=> (min-spit "src/reader/min-spit.clj")
"(ns reader.min-spit\n  (:import (java.io FileInputStream InputStreamReader BufferedReader)))\n(defn min-spit [src]\n    (let [sb (new StringBuilder)]\n        (with-open [reader (-> src FileInputStream. InputStreamReader. BufferedReader.)]\n            (loop [readChar (.read reader)]\n                (if (neg? readChar) (str sb) \n                    (do (.append sb (char readChar))\n                        (recur (.read reader))))))))\n\n\n"

とりあえずこんな感じに書けました。
同じように、ファイルにのみ書きだせるmin-slurpを。

(ns reader.min-slurp
  (:import (java.io FileOutputStream OutputStreamWriter BufferedWriter)))

(defn min-slurp [dst content]
    (with-open [writer (-> dst FileOutputStream. OutputStreamWriter. BufferedWriter.)]
        (.write writer (str content))))
user=> (load-file "src/reader/min-slurp.clj")
#'reader.min-slurp/min-slurp
user=> (use 'reader.min-slurp)
nil
user=> (min-slurp "output/Output" "TestTestTest")
nil
; ファイル「"output/Output"」に「TestTestTest」を出力
どうすれば抽象化につながる?

上記のプログラムは以下の2つの流れに分かれています。

  1. Reader/Writerを生成する
  2. 生成したReader/Writerから読み込み/書き込みを行う

そのうち、後者は共通のため、前者を差し替え可能にすれば抽象化につながるのではないか・・・
と考え、まずはReader/Writerを生成する個所を別関数に切りだしてみます。

すると以下のようになります。

(defn make-reader [src] (-> src FileInputStream. InputStreamReader. BufferedReader.))

(defn min-spit [src]
    (let [sb (new StringBuilder)]
        (with-open [reader (make-reader src)]
            (loop [readChar (.read reader)]
                (if (neg? readChar) (str sb) 
                    (do (.append sb (char readChar))
                        (recur (.read reader))))))))

(defn make-writer [dst] (-> dst FileOutputStream. OutputStreamWriter. BufferedWriter.))

(defn min-slurp [dst content]
    (with-open [writer (make-writer dst)]
        (.write writer (str content))))

後はmin-spit/min-slurpに別のデータ入出力元を扱わせたい場合はmake-reader/make-writer関数を
与えられたオプションなどで返すreader/writerを切り替えられるようにすればOKとなりますね。

(defn make-reader [src] (-> src FileInputStream. InputStreamReader. BufferedReader.))

(defn min-spit [src]
    (let [sb (new StringBuilder)]
        (with-open [reader (make-reader src)]
            (loop [readChar (.read reader)]
                (if (neg? readChar) (str sb) 
                    (do (.append sb (char readChar))
                        (recur (.read reader))))))))

(defn make-writer [dst] (-> dst FileOutputStream. OutputStreamWriter. BufferedWriter.))

(defn min-slurp [dst content]
    (with-open [writer (make-writer dst)]
        (.write writer (str content))))

実際に切り替えることを行う関数は以下のようになりました。

(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.))


(defn make-writer [dst] (-> (condp = (type dst)
                              java.io.OutputStream dst
                              java.io.File (FileOutputStream. dst)
                              java.lang.String (FileOutputStream. dst)
                              java.net.Socket (.getOutputStream dst)
                              java.net.URL (if (= "file" (.getProtocol dst))
                                             (-> dst .getPath FileOutputStream.)
                                             (throw (IllegalArgumentException. "Can’t write to non-file URL"))))
                          OutputStreamWriter.
                          BufferedWriter.))

・・・なのですが、このコードには大きな問題があります。
後から違うデータ型を追加しようとした場合、既存クラスを修正する必要があること点です。
#拡張に対して閉じているといも言います。
そのため、このままでは実質的には使えない・・・となりますね。

これをどうやったらうまくハンドリングできるかを次回確認します。