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

本当は怖い情報科学

情報系大学院生の趣味&実益ブログ。

Clojure記事紹介<百日修行>(5):「よりわかりやすいClojureを書く」

clojure

Clojure記事紹介<百日修行>では、英文で書かれたClojureの記事を要約してひたすら紹介していきます。モットーは「質より量」です。

紹介記事

Writing Friendlier Clojure

他の言語と同様、Clojureにおいても、気づいたらわかりにくいプログラムを書いてしまうことはよくあります。特に、REPL上で試行錯誤しながら少しずつ書き進めたコードだと、そうなりがちです。そこでこの記事では、とあるClojureのコードを取りあげ、それをリファクタリングする過程を通して、わかりやすいClojureコードを書くコツが紹介されています。

例題となるコード

この記事で取りあげられているコードは、redditClojureスレから持ってきたそうです。

(defn markov-data [text]
  (let [maps
        (for [line (clojure.string/split text #"\.")
              m (let [l (str line ".")
                      words
                      (cons :start (clojure.string/split l #"\s+"))]
                  (for [p (partition 2 1 (remove #(= "" %) words))]
                    {(first p) [(second p)]}))]
          m)]
    (apply merge-with concat maps)))

(defn sentence [data]
  (loop [ws (data :start)
         acc []]
    (let [w (rand-nth ws)
          nws (data w)
          nacc (concat acc [w])]
      (if (= \. (last w))
        (clojure.string/join " " nacc)
        (recur nws nacc)))))

コードの意味については、原文で軽く触れられているので参照してください。

リファクタリング

ここでは、記事で紹介されているテクニックを要約して紹介し、リファクタリング後のコードを紹介するにとどめるので、途中経過が見たい方は元記事を参照して下さい。

紹介されているテクニックは以下の通りです。

  • 変数には長くてもわかりやすい名前を付ける
  • 意味のまとまりがある部分を切り出して関数にする
  • 深いletネストをthreading macro -> で書き換える
  • loop構文で値をaccumulateしていくようなコードは、reduceを使えないか検討する
  • firstsecond等でシーケンスを切り出すときは、restructuringを活用する

結果

このようにしてリファクタリングされたコードが以下です。

(defn sentence->wordlist [sentence]
  (-> sentence
      (str ".")
      (clojure.string/split #"\s+")
      (->> (cons :start)
           (remove #(= "" %)))))

(defn make-markov-mapping [sentence]
  (let [wordlist (sentence->wordlist sentence)]
    (for [[word & words] (partition 2 1 wordlist)]
      {word words})))

(defn markov-data [text]
  (->> (clojure.string/split text #"\.")
       (mapcat make-markov-mapping)
       (apply merge-with concat)))

(defn pick-next-word [mapping this-word]
  (let [choices (get mapping this-word)]
    (rand-nth choices)))

(defn sentence [mapping]
  (loop [words []
         this-word :start]
    (let [next-word (pick-next-word mapping this-word)
          words (conj words next-word)]
      (if (= (last next-word) \.)
        (clojure.string/join " " words)
        (recur words next-word)))))

行数は長くなっていますが、関数名や変数名にわかりやすい名前が割り当てられ、ここの関数は短いので、理解しやすいコードになっています。

繰り返しですが詳しい説明や途中経過は記事を参照してください。

Clojure Applied: From Practice to Practitioner

Clojure Applied: From Practice to Practitioner

プログラミングClojure 第2版

プログラミングClojure 第2版

【広告】