読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

少しだけClojure 4

clojure

プログラミングClojure第2版を読みながら続き。

方針

  • 基本的に本の説明をそのままor自分の言葉に直しただけで書き直すようなことはしない
  • サンプルコードが長くて理解しづらい、練習したいなと思うものは細かく試して書いてみる
  • 読んだ時点で書かれてなかったり省略されているか、疑問に思ったことを調べて書く
  • 後から書かれていたら参照のために簡単にメモする

ファイルのシーケンス

Javaメソッドはそのまま関数としては扱えないみたいだ

user=> (map .lastModified (.listFiles (File. ".")))

CompilerException java.lang.RuntimeException: Unable to resolve symbol: .lastModified in this context, compiling:(/tmp/form-init1982058670582211121.clj:1:1) 

user=> (map #(.lastModified %) (.listFiles (File. ".")))
(1426701126000 1426701126000 1426829963000 1426701126000 1426701126000 1426701126000 1426703114000 1426840345000 1426701126000 1426701126000 1426703114000 1426701126000 1426701126000 1426703114000 1426703114000 1426701126000 1426829963000)

recently-modified?の30分が決め打ちなのが気持ち悪いので、初期値としてみる。

(defn recently-modified?
  ([file] (recently-modified? file 30))
  ([file mins]
   (> (.lastModified file)
      (- (System/currentTimeMillis) (minutes-to-msec mins)))))

このやり方がClojureらしいのかな、多分。

& argsだけじゃやりづらいと思ったけど、こっちのほうがスッキリ書けて好きかもしれない。

ストリーム

with-openの評価が終わった時点でストリームが閉じられるので、中で遅延シーケンスを現実化しないとその後扱えなくなるみたいだ。

user=> (with-open [rdr (reader "./src/examples/introduction.clj")] rdr)
#<BufferedReader java.io.BufferedReader@1236b3c3>

user=> (with-open [rdr (reader "./src/examples/introduction.clj")] (line-seq rdr))
IOException Stream closed  java.io.BufferedReader.ensureOpen (BufferedReader.java:122)
;; 閉じられてる

user=> (with-open [rdr (reader "./src/examples/introduction.clj")] (map println (line-seq rdr)))
(ns examples.introduction)

IOException Stream closed  java.io.BufferedReader.ensureOpen (BufferedReader.java:122)
;; 遅延シーケンスのヘッドだけ評価されるのか、なるほど

user=> (with-open [rdr (reader "./src/examples/introduction.clj")] (dorun (map println (line-seq rdr))))

;; これでようやくファイルの中身が表示される

ファイルに行番号をつけてみる

すごいHaskellにあったZipWithみたいなの無いかなと思ったけど、普通にmap使えばいいのか。

参考:https://groups.google.com/forum/#!topic/clojure-ja/mMxOO3vLSig

ついでに、lambdaの略記法では普通に式が入れ子でも%1 %2は処理されるみたいだ。

user=> (with-open [rdr (reader "./src/examples/introduction.clj")] (dorun (map #(println (format "%04d: %s" %1 %2)) (whole-numbers) (line-seq rdr))))

;; ...
0057: (defn fizzbuzz-maker [& defs]
0058:   "FizzBuzzの定義のリストを受け取り、数字のシーケンスに適用する関数を作る"
0059:   (let [fzbz (fizzbuzz-x (reduce-fn (map fzbz->fn defs)))]
0060:     (fn [xs] (map fzbz xs))))
0061: 
0062: (defn fizzbuzz [n]
0063:   ((fizzbuzz-maker
0064:     {:number 3  :message "Fizz"}
0065:     {:number 5  :message "Buzz"}
0066:     {:number 11 :message "Jazz"}
0067:     {:number 17 :message "Funk"}
0068:     ) (take n (iterate inc 1))))
;; ...

XML

まず上のコードを利用して、ファイル内容に行番号をつけて表示する関数を作っておく。

(defn line-format [seq] (map #(format "%05d: %s" %1 %2) (whole-numbers) seq))

(defn print-all [seq] (dorun (map println seq)))

(defn print-file-nu [path]
  (with-open [rdr (reader (java.io.File. path))]
    (print-all (line-format (line-seq rdr)))))

書籍のサンプルXMLの中身を表示してみる。

user=> (print-file-nu "data/sequences/compositions.xml")
00001: <compositions>
00002:   <composition composer="J. S. Bach">
00003:     <name>The Art of the Fugue</name>
00004:   </composition>
00005:   <composition composer="F. Chopin">
00006:     <name>Fantaisie-Impromptu Op. 66</name>
00007:   </composition>
00008:   <composition composer="W. A. Mozart">
00009:     <name>Requiem</name>
00010:   </composition>
00011: </compositions>
00012: 

さて、clojure.xml/parseと、それにxml-seqを適用するみたいだけど、parseの結果は単に:tag :attrs :contentのマップみたいだからわかるとして、xml-seqの結果がシーケンスと言われてもどうなってるのかよくわからない。

user=> (parse (java.io.File. "data/sequences/compositions.xml"))
{:tag :compositions, :attrs nil, :content [{:tag :composition, :attrs {:composer "J. S. Bach"}, :content [{:tag :name, :attrs nil, :content ["The Art of the Fugue"]}]} {:tag :composition, :attrs {:composer "F. Chopin"}, :content [{:tag :name, :attrs nil, :content ["Fantaisie-Impromptu Op. 66"]}]} {:tag :composition, :attrs {:composer "W. A. Mozart"}, :content [{:tag :name, :attrs nil, :content ["Requiem"]}]}]}

user=> (xml-seq (parse (java.io.File. "data/sequences/compositions.xml")))
({:tag :compositions, :attrs nil, :content [{:tag :composition, :attrs {:composer "J. S. Bach"}, :content [{:tag :name, :attrs nil, :content ["The Art of the Fugue"]}]} {:tag :composition, :attrs {:composer "F. Chopin"}, :content [{:tag :name, :attrs nil, :content ["Fantaisie-Impromptu Op. 66"]}]} {:tag :composition, :attrs {:composer "W. A. Mozart"}, :content [{:tag :name, :attrs nil, :content ["Requiem"]}]}]} {:tag :composition, :attrs {:composer "J. S. Bach"}, :content [{:tag :name, :attrs nil, :content ["The Art of the Fugue"]}]} {:tag :name, :attrs nil, :content ["The Art of the Fugue"]} "The Art of the Fugue" {:tag :composition, :attrs {:composer "F. Chopin"}, :content [{:tag :name, :attrs nil, :content ["Fantaisie-Impromptu Op. 66"]}]} {:tag :name, :attrs nil, :content ["Fantaisie-Impromptu Op. 66"]} "Fantaisie-Impromptu Op. 66" {:tag :composition, :attrs {:composer "W. A. Mozart"}, :content [{:tag :name, :attrs nil, :content ["Requiem"]}]} {:tag :name, :attrs nil, :content ["Requiem"]} "Requiem")

シーケンスらしいので上のline-formatとprint-allで行番号つけて表示してみる。

user=> (print-all (line-format (xml-seq (parse (java.io.File. "data/sequences/compositions.xml")))))
00001: {:tag :compositions, :attrs nil, :content [{:tag :composition, :attrs {:composer "J. S. Bach"}, :content [{:tag :name, :attrs nil, :content ["The Art of the Fugue"]}]} {:tag :composition, :attrs {:composer "F. Chopin"}, :content [{:tag :name, :attrs nil, :content ["Fantaisie-Impromptu Op. 66"]}]} {:tag :composition, :attrs {:composer "W. A. Mozart"}, :content [{:tag :name, :attrs nil, :content ["Requiem"]}]}]}
00002: {:tag :composition, :attrs {:composer "J. S. Bach"}, :content [{:tag :name, :attrs nil, :content ["The Art of the Fugue"]}]}
00003: {:tag :name, :attrs nil, :content ["The Art of the Fugue"]}
00004: The Art of the Fugue
00005: {:tag :composition, :attrs {:composer "F. Chopin"}, :content [{:tag :name, :attrs nil, :content ["Fantaisie-Impromptu Op. 66"]}]}
00006: {:tag :name, :attrs nil, :content ["Fantaisie-Impromptu Op. 66"]}
00007: Fantaisie-Impromptu Op. 66
00008: {:tag :composition, :attrs {:composer "W. A. Mozart"}, :content [{:tag :name, :attrs nil, :content ["Requiem"]}]}
00009: {:tag :name, :attrs nil, :content ["Requiem"]}
00010: Requiem

なるほど、ツリーの各ノードへのリファレンスを、深さ優先で並べたもの、かな。

なのでforで(ハッシュか文字列かの判定を使わずに)曲名を抜き出す場合は、次のようにできる。

user=> (xml-seq (parse (java.io.File. "data/sequences/compositions.xml")))
;; さっきのxmlのシーケンス

user=> (for [x *1 :when (= (:tag x) :name)] (first (:content x)))
("The Art of the Fugue" "Fantaisie-Impromptu Op. 66" "Requiem")

タグが:nameのものについて、内容(のfirst)を取り出した。

どんなタグが使われているかは、シーケンスのうちマップのものから:tagを取り出し、集合に変換すればいいかな。

user=> (for [x compositions :when (map? x)] (:tag x))
(:compositions :composition :name :composition :name :composition :name)

user=> (set (for [x compositions :when (map? x)] (:tag x)))
#{:compositions :name :composition}

できた。便利だ。

マップを扱う関数

前に疑問に思った「マップの値がnilだった場合の、キーがあるかどうかの判定」はcontains?を使うか、getに3番目の引数を渡す。このgetの3番目の引数はちょっとおもしろいな

user=> (get {:hoge 1} :fuga :not-found)
:not-found

user=> (get {:hoge 1} :fuga {:fuga 1})
{:fuga 1}

なんとなくだけど、マクロに使えそう。

セットを扱う関数

関数の前に、セットがどれぐらい同じものだと判定できるか気になったので、試してみる。

マップ同士はどうだろう?

user=> #{ {:hoge 1} {:hoge 1} }

IllegalArgumentException Duplicate key: {:hoge 1}  clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:68)

あれ、例外だ。

user=> #{ {:hoge 1} {:hoge 2} }
#{{:hoge 2} {:hoge 1}}

マップが追加できないわけじゃなさそうだ。

user=> #{ 1 2 1 }

IllegalArgumentException Duplicate key: 1  clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:68)

マップ記法で作るときのチェックで(createWithCheck?)同じものがあるとダメということか。ここは覚えておこう。set関数なら大丈夫だったから、意識しておく。

変数だとどうだろう?

user=> (def h1 {:hoge 1})
#'user/h1
user=> (def h2 {:hoge 1})
#'user/h2
user=> #{ h1 h2 }

IllegalArgumentException Duplicate key: {:hoge 1}  clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:56)

ダメだった。罠にハマらないために、変数をセットに入れるときはset/hash-set関数を使おう。

で、同じマップはどうなるだろうか?

user=> (conj (hash-set {:hoge 1}) {:hoge 1})
#{{:hoge 1}}

user=> (conj (hash-set h1) h2)
#{{:hoge 1}}

同じものとして判定されるっぽい。入れ子になったマップとシーケンスはどうだろう?

user=> (conj (hash-set {:hoge [{:fuga 2}]}) {:hoge [{:fuga 2}]})
#{{:hoge [{:fuga 2}]}}

同じものだった。

循環参照している場合はどうなるんだろう?無限ループせずに途中で判定してくれるだろうか?循環参照の作り方がわからなかったので後回しにする。

関係演算

関係演算そのものをよく知らないのだけど、とてもおもしろそう。

renameに渡すrename-mapは一つに限らない。

user=> (use 'examples.sequences)
nil

user=> nations
#{{:language "Italian", :nation "Italy"} {:language "German", :nation "Germany"} {:language "German", :nation "Austria"}}

user=> (rename nations {:language :kotoba :nation :kuni})
#{{:kuni "Germany", :kotoba "German"} {:kuni "Austria", :kotoba "German"} {:kuni "Italy", :kotoba "Italian"}}

直積(ちょくせき)は直積そのものの関数があるわけじゃなくて、forで実現するか、実際の開発で使うjoinなどの特殊化されたものを利用するかってことかな。最初読んだとき何故か頭から滑っていった。

で、関係演算は慣れてないので実際に少しずつ使ってみる。まず対象のサンプルデータはこうなっているので

user=> compositions
#{{:name "Musical Offering", :composer "J. S. Bach"} {:name "The Art of the Fugue", :composer "J. S. Bach"} {:name "Requiem", :composer "Giuseppe Verdi"} {:name "Requiem", :composer "W. A. Mozart"}}

user=> nations
#{{:language "Italian", :nation "Italy"} {:language "German", :nation "Germany"} {:language "German", :nation "Austria"}}

user=> composers
#{{:composer "Giuseppe Verdi", :country "Italy"} {:composer "J. S. Bach", :country "Germany"} {:composer "W. A. Mozart", :country "Austria"}}

全部直積してみよう。まずcompositionsの:composerとcomposersの:composerでjoinしてみる。

user=> (join compositions composers)
#{{:name "Requiem", :composer "W. A. Mozart", :country "Austria"} {:name "Musical Offering", :composer "J. S. Bach", :country "Germany"} {:name "Requiem", :composer "Giuseppe Verdi", :country "Italy"} {:name "The Art of the Fugue", :composer "J. S. Bach", :country "Germany"}}

次に、それとnationsをjoinする。前者は:country、後者は:nationに国名がセットされているので、その対応のマップも渡す。

user=> (join *1 nations {:country :nation})
#{{:name "The Art of the Fugue", :language "German", :composer "J. S. Bach", :country "Germany", :nation "Germany"} {:name "Requiem", :language "Italian", :composer "Giuseppe Verdi", :country "Italy", :nation "Italy"} {:name "Musical Offering", :language "German", :composer "J. S. Bach", :country "Germany", :nation "Germany"} {:name "Requiem", :language "German", :composer "W. A. Mozart", :country "Austria", :nation "Austria"}}

次に選択。国がGermanyのものを探してみる。

user=> (select #(= (:country %) "Germany") *1)
#{{:name "The Art of the Fugue", :language "German", :composer "J. S. Bach", :country "Germany", :nation "Germany"} {:name "Musical Offering", :language "German", :composer "J. S. Bach", :country "Germany", :nation "Germany"}}

最後に射影。作曲者と曲集名を取得する。

user=> (project *1 [:name :composer])
#{{:composer "J. S. Bach", :name "Musical Offering"} {:composer "J. S. Bach", :name "The Art of the Fugue"}}

そういえばインヴェンションとシンフォニアもう一度買おうと思ってたの思い出した。実家にあるの多分埃だらけなんだよな……

感想など

Parenscriptでいろいろ書いてて、ClojureScriptが人気みたいだけど参考になるだろうか、せっかく「プログラミングClojure」持ってるし目を通してみよう、と軽い気持ちで触り始めたけど、かなりClojure好きになってる。

シーケンスって何さ!リストでいいでしょ!とか、JVMか、そうか……みたいな意識が最初にあったけど、払拭されてしまった。なんとなくだけど、「こう動いてくれるでしょ!」が通るような、一貫性が強く感じられる言語/処理系が好きなのかもしれない。あとエラーメッセージがわかりやすい処理系かな。

広告を非表示にする