Middleware Pattern

先日、 Clojure 座談会という勉強会を行った。まぁ幾つかの反省点はあるものの概ね良かったと思う。 何より Clojure の話題というだけで休日に 10 人を超える人間を集めることが出来たので、キャパがあってしっかり宣伝していればもう少し人は集まったように思う。

それはさておき、その勉強会の中で僕は “Middleware Pattern” というタイトルで LT をさせてもらった。

Middleware Pattern

既にこのパターンに関しては実例が幾らかあるので、真新しいものではないのだけど日本で注目されているところはあまり見たことないし、面白いかなと思って今回の発表に至った。最もポピュラーな例としては Ring Middleware ですね。

このパターンがもっと色んなところで使えるじゃんという風に気付いたのは、 Twilog scraper というしょうもないライブラリ/ CLI tool を書いていたときだったりします。

元々 Skyscraper というウェブサイトを構造的にスクレイピングするという面白いライブラリがあって、僕はこれをいいじゃんと思って使い始めたんだけど、このライブラリの素敵なところが処理結果を遅延シーケンスで返してくれるところなんですね。つまり、どういうことかというと takedrop などといった lazy manipulator を使うことでウェブサイト全てのスクレイピングを行わずに欲しい分だけをスクレイピング出来るということです。

そして Twilog scraper を書いているときに、「最大何件だけを取得したいなどという条件をユーザーが変更出来るようにしたい」と思ったんです。具体的な例をあげて説明しましょう。

(def ls (lazy-seq (range)))

(defn proc [& {:keys [num]}]
  (take num ls))

(proc :num 10)

こういうコードがあったときに num が必ず存在するなら、つまりユーザーの入力が必ず 0 以上であるなら何も問題ありません。しかし、例えば ls が有限の長さを持つシーケンスで [1] ユーザーからの入力がない場合にそのシーケンスの全てを取得したい場合はどうでしょう。こう書きますか?

(def ls (lazy-seq (range 100)))

(defn proc [& {:keys [num]}]
  (take (or num (count ls)) ls))

この場合 ls がどのくらいの長さか分からないけど有限であるのが分かっているのなら、ありかもしれませんね。それではここにもう少し条件を付け加えてみましょう。取り出した値の中から更にある数値以下までのものを取り出す、というような場合どうでしょう。

(defn proc [& {:keys [num max]}]
  (take-while #(< % max) (take (or num (count ls)) ls)))

こんな感じになりそうですよね。気付いた方もいると思いますが、これは ls 長さに比例して処理時間が長くなります。どういうことかというと、 count 関数を実行したタイミングで遅延シーケンスがリアライズされてしまうのです。 そうなるとあまり嬉しくなさそうですよね [2] 。もし無限なら処理自体が終わらなくなってしまいます(いずれエラーで落ちるにしても良くない)。 勿論、デフォルト値などを予め定めてしまうのも手でしょうけど、どのくらいの長さか分からないものに対して取得できるだけ全部を取りたいのに取れないようになってしまうのでこれもよろしくなさそうです。

そうなると実行時にどれを適用する/しないを決めれるような仕組みが欲しくなるわけですが、これを実現するのに Middleware Pattern が有効なのです。

上記の例を綺麗に Middleware Pattern を使って書きなおしてみましょう。

(def ls (lazy-seq (range 100)))

(defn wrap-take [handler num]
  (fn [res]
    (handler (take num res))))

(defn wrap-take-while [handler max]
  (fn [res]
    (handler (take-while #(< % max) res))))

(defn wrap [handler middleware opt]
  (if opt
    (middleware handler opt)
    handler))

(defn make-handler [opts]
  (-> identity
      (wrap wrap-take-while (:max opts))
      (wrap wrap-take (:num opts))))

(defn proc [& {:keys [num max] :as opts}]
  (let [handler (make-handler opts)]
    (handler ls)))

綺麗にといったわりに少々汚いですが Middleware Pattern を適用した結果、動的に Handler を作ることに成功しました。これで先ほどのように欲しい分だけが処理されるので、処理時間も短くなりますしコードの見通しがよくなりました。

このように Composable な関数を書きたい場合に Ring で使われている Middleware Pattern はとても有効だと思います。実際に Boot でも Middleware Pattern が使われています :)

というわけで Middleware Pattern の話でした。

追記

以下のコード例について「 count で最大件数とらなくてもいいんじゃないか」という指摘を頂きました。

(defn proc [& {:keys [num]}]
  (take (or num (count ls)) ls))

次のようにすることで確かに回避することは出来ます。

(defn proc [& {:keys [num]}]
  (if num (take num ls) ls))

この記事で伝えたかったことは Composable な関数を作るのに、 Middleware pattern 使うと便利だよということだったので例を考えるのを少々手抜きしてしまいました。もう少し考えて今度から書こうと思います。

[1]つまり現実的なウェブサイトなどのスクレイピング結果
[2]実際に Skyscraper みたいな必要なときに必要な分だけ実行出来るように lazy していたのにもかかわらず、全部処理しなければいけなくなってしまうのでコストが高くなります