本当は怖いHPC

HPC屋の趣味&実益ブログ

Clojure記事紹介<百日修行>(5):「よりわかりやすい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版

Clojure記事紹介<百日修行>(4):「PulsarでActorプログラミング」

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

紹介記事

レベル:中級

Pulsarを用いた、Actorモデルを使った並行プログラミングの紹介記事です。

取り上げられているライブラリ:

Actorモデルで並行プログラミング

Pulsarは、JavaのQuasarというライブラリのClojureバインディングで、Actorモデルを実現するライブラリです。

Actorモデルは、並行処理を記述するためのプログラミングモデルの1つで、Actorと呼ばれる互いに独立した(データを共有しない)プロセスがメッセージをやりとりすることによって処理を進めていきます。Erlang/Elixirといった言語や、Scala/JavaのAkkaなどが有名でしょう。

Actorモデルの利点は、 * 各Actorは共有データを持たないため、データの競合が起きない(起きにくい) * Erlang/Elixirでは、各Actorがメモリ管理などの面で独立しているため、エラー・障害に強い * 計量スレッドやグリーンスレッドで実装されることによって、通信・ディスクアクセスなどのIOを大量に行う際の並列性が確保でき、またレイテンシが隠蔽できるためスループットが向上する

といった物があります。逆にデメリットとしては、

  • 規模が大きくなると、多数のActorを管理するのはプログラミング上難しい
  • どれくらいのActorが並行動作できるかどうかは実装次第
  • 計量スレッドをサポートしていないプラットフォームでは実装がトリッキーになりがちで、制限が多いこともある(JVMとか)

あるかと思います。(私はプログラミング言語の専門ではないので詳しくないです)

Pulsarでプログラミング

この記事では、サンプルとして、非常に簡易なチャットシステムのようなものを作製しています。サーバーの方だけソースコードを転載するので、リンク先でクライアント側のソースも是非参照してください。

各Actorは、受信したメッセージによって場合分けをする、イベントループのような構造の関数を定義します。ソースコード中のreceiveと呼ばれる式がそうです。

(defn- get-name [ref clients]
  (-> (first (filter #(= (:ref %) ref) clients))
      :name))

(defn- broadcast [msg clients]
  (doseq [c clients]
    (! (:ref c) msg)))

(defsfn server [clients]
  (receive
   [:join ref name] (do
                      (link! ref)
                      (broadcast
                       [:info (str name " joined the chat")]
                       clients)
                      (recur (conj clients {:name name :ref ref})))
   [:send ref msg]  (do
                      (broadcast
                       [:new-msg (get-name ref clients) msg]
                       clients)
                      (recur clients))
   [:exit _ ref _]  (let [updated-clients (remove #(= (:ref %) ref) clients)]
                      (broadcast
                       [:info (str (get-name ref clients) " left the chat")]
                       updated-clients)
                      (recur updated-clients))
   :shutdown        (println "Shutting down")))

(defn create-server []
  (spawn :trap true server '()))

個人的な感想としては、ちょっと記法にクセがあるのが気になるのと、これならcore.asyncでよくね?という気がします。

Clojure Applied: From Practice to Practitioner

Clojure Applied: From Practice to Practitioner

プログラミングClojure 第2版

プログラミングClojure 第2版

Clojure記事紹介<百日修行>(3):「ログの基本」

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

紹介記事

Clojure in Production: Logging

レベル:初級〜中級

この記事では、「Clojure in production」シリーズとして、ログについて書かれています。ログ出力の方法について、初心者向けの基本的なチュートリアルとなっています。

取り上げられているライブラリ:

  • tools.logging

tools.logging + log4jが基本

ログ出力の基本は、Javaのライブラリであるlog4jを使うことです。 tools.loggingはそれ以外のライブラリもサポートしていますが、ひとまずはこれが基本となるでしょう。この記事の前半では、log4jおよびtools.loggingを使うためのleiningenと設定ファイルresources/log4j.propertiesの書き方、そしてログレベル(TRACE, DEBUG, INFO, WARN, ERROR, FATAL)の設定の仕方が説明されています。この部分をコピペするだけで、とりあえず使うことが出来ます。

log4j.rootLogger=ERROR, file

log4j.logger.rabbet.core=INFO

log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=log/rabbet.log
log4j.appender.file.MaxFileSize=1MB
log4j.appender.file.MaxBackupIndex=10
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} | %-5p | %t | %c | %m%n

正直プロパティファイルの書き方はよくわからないところが多いのですが、最初にコピペしてしまえば、使い方は簡単です

(ns rabbet.core (:require [clojure.tools.logging :as log]))
;; Output log statements at various levels:
(log/trace "This is the lowest (least important) log level")
(log/debug "For programmer info...")
(log/warn "Something bad might have happened.")
;; You can dump all kinds of things out to the log: 
(log/info "this happened:" {:foo 12 :bar :quux} "that was fun!") 
;; Exceptions have a special arity: 
(try (/ 10 (dec 1)) ;; <-- dividing by zero rarely works.
   (catch Exception e (log/error e "Dividing failed!")))

次に、一歩進んで、出力先の設定、出力フォーマットの変更、そして動的な設定も説明されています。このあたりは、Javaについて書かれた資料を見ながらClojure向けに脳内変換して調べざるを得ないことが多いので、Clojure視点で書かれているのは助かります。特に、動的な設定について書かれているのは勉強になりました。

オマケ:スタックトレースの見方

記事のオマケとして、例外発生時のスタックトレースに慣れていない人のために、スタックトレースの解読の仕方が説明されています。

スタックトレースが長くて読みにくいという点は、Clojureの(数少ない:)欠点の一つですが、コツをつかむとそれほど苦労せずに読むことが出来ます。

この記事で述べられているのは まずスタックトレースを上から順に見ましょう 冒頭には、発生した例外の種類が書かれています(例:java.lang.ArithmeticException)。通常はこの情報は役に立ちます。 例外のメッセージも確認しましょう。 どこで例外が投げられたか確認しましょう。非常に長いリストが出力されていますが、本当に見るべき箇所は多くありません。clojure.coreやjavaのレベルでバグがあると言うことは稀なので、飛ばしましょう。自分が書いているプログラムのネームスペースが出てきたら、きっとそこが本当の問題です。

というわけで、tools.loggingとlog4jを使ったログ出力についての記事を紹介しました。

【広告】