Enliveをさわってみたのでメモ

Enliveをさわってみたのでメモ。

導入

  • project.clj の :dependencies に下記を追加
[enlive "1.1.5"]

REPLから試用

user=> (use 'net.cgrand.enlive-html)
nil

user=> (dir net.cgrand.enlive-html)
(中略)
html
html-content
html-resource
html-snippet
(中略)
select
(以下略)
html関数
user=> (doc html)
-------------------------
net.cgrand.enlive-html/html
([& nodes-specs])
  Allows to define inline fragments with a hiccup-like syntax.
nil

user=> (html [:div {:id "divHoge"} [:input {:id "textHoge" :type "text"}]])
({:tag :div, :attrs {:id "divHoge"}, :content ({:tag :input, :attrs {:type "text", :id "textHoge"}, :content ()})})
html-resource関数
user=> (doc html-resource)
-------------------------
net.cgrand.enlive-html/html-resource
([resource] [resource options])
  Loads an HTML resource, returns a seq of nodes.
nil

user=> ; clojure.java.io の reader 関数を用いて HTML resource を取得して渡してみる
user=> (use 'clojure.java.io)
nil

user=> (doc reader)
-------------------------
clojure.java.io/reader
([x & opts])
  Attempts to coerce its argument into an open java.io.Read
   Default implementations always return a java.io.Buffered

   Default implementations are provided for Reader, Buffere
   InputStream, File, URI, URL, Socket, byte arrays, charac
   and String.

   If argument is a String, it tries to resolve it first as
   as a local file name.  URIs with a 'file' protocol are c
   local file names.

   Should be used inside with-open to ensure the Reader is
   closed.
nil

user=> (reader "http://google.com")
#<BufferedReader java.io.BufferedReader@1969526c>

user=> (html-resource (reader "http://google.com"))
({:type :dtd, :data ["html" nil nil]} {:tag :html, :attrs {
(以下略)
select関数
user=> (doc select)
-------------------------
net.cgrand.enlive-html/select
([node-or-nodes selector])
  Returns the seq of nodes or fragments matched by the specified selector.
nil

user=> (select
  #_=>   (html [:div {:id "divHoge"} [:input {:id "textHoge" :type "text"}]])
  #_=>   [:input])
({:tag :input, :attrs {:type "text", :id "textHoge"}, :content ()})

user=> (select
  #_=>   (html [:div {:id "divHoge"} [:input {:id "textHoge" :type "text"}] [:input {:id "textToge" :type "check"}]])
  #_=>   [:input#textToge])
({:tag :input, :attrs {:type "check", :id "textToge"}, :content ()})

user=> (select
  #_=>   (html [:html [:div {:id "divHoge"} [:input {:id "textHoge" :type "text"}]] [:input {:id "textToge" :type "check"}]])
  #_=>   [:div :input])
({:tag :input, :attrs {:type "text", :id "textHoge"}, :content ()})

user=> (select
  #_=>   (html [:html [:div {:id "divHoge"} [:input {:id "textHoge" :type "text"}]] [:input {:id "textToge" :type "check"}]])
  #_=>   #{[:div :input] [:#textToge]})
({:tag :input, :attrs {:type "text", :id "textHoge"}, :content ()} {:tag :input, :attrs {:type "check", :id "textToge"}, :content ()})

実用

強引感が半端ないが、Yahoo!ファイナンスから板気配を取得するコードを書いてみた。

(use 'net.cgrand.enlive-html)
(use 'clojure.java.io)

(defn kehai-url
  [meigara-code]
  (str "http://stocks.finance.yahoo.co.jp/stocks/detail/?code="
       meigara-code
       "."
       "T"))

(defn kehai
  [meigara-code]
  (let [[title-row row1 row2]
        (select
          (html-resource (reader (kehai-url meigara-code)))
          [:.main2colL :table :tr])]
    {:meigara-code meigara-code
     :uri-kehai
      (first
        (:content
          (first
            (select row1 [[:td (nth-of-type 1)]]))))
     :uri-kehai-kabuka
      (first
        (:content
          (first
            (select row1 [[:td (nth-of-type 3)] :strong]))))
     :kai-kehai
      (first
        (:content
          (first
            (select row2 [[:td (nth-of-type 5)]]))))
     :kai-kehai-kabuka
      (first
        (:content
          (first
            (select row2 [[:td (nth-of-type 3)] :strong]))))}))
user=> (map kehai [4689 4755])
({:meigara-code 4689, :uri-kehai "---", :uri-kehai-kabuka "---", :kai-kehai "---", :kai-kehai-kabuka "---"} 
{:meigara-code 4755, :uri-kehai "---", :uri-kehai-kabuka "---", :kai-kehai "---", :kai-kehai-kabuka "---"})

Clojureでステートマシン書いてみたのでメモ

おもちゃ未満ながら、Clojureでステートマシン書いてみたのでメモ。
(2013/12/16 微編集)

コード

(def states-def
  "状態定義"
  {:state01                               ;状態ID
     [[#(println "->state01")]            ;上記状態に到達した際に評価される関数のベクタ
      {:event01                           ;イベントID
         [[#(println "event01 happened!")];上記状態で上記イベントが発生した際に評価される関数のベクタ
          :state02]}]                     ;次の状態のID 

   :state02              
     [[#(println "->state02")] 
      {:event02                
         [[#(println "event02 happened!")] 
          :state01]}]})           

(defn happen-event!
  "イベント発生"
  [state event & param]
  (let [[event-acts next-state] (event (second (state states-def)))
        next-state-acts         (first (next-state states-def))]
    (dorun (map #(%) event-acts))
    (dorun (map #(%) next-state-acts))
    next-state))

実行例

state.core=> (happen-event! (happen-event! :state01 :event01) :event02)

event01 happened!
->state02
event02 happened!
->state01
:state01


state.core=> (-> :state01 (happen-event! :event01)
                          (happen-event! :event02))

event01 happened!
->state02
event02 happened!
->state01
:state01

マクロについてメモ

Lispのマクロについて、書籍の抜粋メモ。

マクロの使いドコロ

On Lisp 8.1 他の手段では不可能なとき の項より

マクロにできて関数にできないことは2つある:

  • まずマクロは引数の評価を制御(または抑制)できること
  • また呼び出し側のコンテキスト内へ直に展開されること

だ。結局マクロが必要なアプリケーションには、これらの性質の片方または両方が必要なのだ。

LOL 2.3 制御構造 の項より

「関数ではできない時だけマクロを使う」という言葉は、

  • ある引数を評価しないか
  • 違う順番で評価をするか
  • 二度以上評価する

ような定義をしたければ、どんな定義でもマクロを使う必要があるだろう、
という意味である。

LOL 2.4 自由変数 の項より

事実、いったんそれに慣れてしまうと、できる限りいつでもマクロを書いてレキシカルコンテキストを拡張しようとし、関数を使うのは引数を評価する必要がある時か、単に怖気づいたか、新しいレキシカルコンテキストが欲しい時だけになるLispプログラマもいるくらいである。

LOL 2.7 構文の二重性 の項より

この二重構文を使えば、単一で共通のインタフェースを持ちながら、ダイナミックコンテキストとレキシカルコンテキストの両方で便利な展開を行うマクロを書ける。

On Lisp 7.1 マクロはどのように動作するか の項より

コンパイラが読む情報を書き換えられるというのはコンパイラを書き換えられるのとほとんど同じだ。

On Lisp 8.3 マクロの応用例 の項より

マクロが一般にどう使われるかについては、「主に構文変換に使われる」と言えば一番近いだろう。

LOL 4.4 macroletを使ったコードウォーク

macroletはCOMMON LISPのコードウォーカーとコミュニケーションするためのものであり、マクロの展開がいつ行われるかについては、コンパイル済み関数の実行時より前に完了することだけが保証される。

マクロの使い方

On Lisp 4.1 ユーティリティの誕生 の項より

ボトムアッププログラミングに上達するには、足りないオペレータがまだ書かれていないものだったときにも、同じような不快感を抱くようにならなくてはいけない。

LOL 4.2 トップダウンプログラミング の項より

本格的なマクロを構築するその第一歩は常に、マクロのユースケースを書くことである。

参考文献

On Lisp

On Lisp

LET OVER LAMBDA Edition 1.0

LET OVER LAMBDA Edition 1.0

Luminusを触ってみたのでメモ

Luminusを触ってみたのでメモ。

概要

  • 軽量ライブラリをベースにした小さなWEBアプリケーションフレームワーク
  • 以下を狙いとする
    • 堅牢
    • スケーラブル
    • プラットフォームの利用を簡単に

試用

Leiningen テンプレートLuminus-Templateを利用してテンプレートプロジェクトを作成。
なお、今回利用する Leiningen のバージョンは 2.1.3。

lein new luminus myapp
cd myapp
lein ring server
依存ライブラリ

上記で作成すると、project.clj(抜粋)は↓の感じに。

  :dependencies [[org.clojure/clojure "1.5.1"]
                 [lib-noir "0.7.1"]
                 [compojure "1.1.5"]
                 [ring-server "0.3.0"]
                 [selmer "0.4.3"]
                 [com.taoensso/timbre "2.6.2"]
                 [com.postspectacular/rotor "0.1.0"]
                 [com.taoensso/tower "1.7.1"]
                 [markdown-clj "0.9.33"]]
  :plugins [[lein-ring "0.8.7"]]

Ring, Compojure, lib-noir はいいとして、他を簡単に調べてみた。

  • Selmer: テンプレートシステム(Djangoにインスパイアされた)
  • Timbre: ロギング&プロファイリング ライブラリ
  • rotor: ログローテーションのアペンダ
  • Tower: 国際化のライブラリ
  • markdown-clj: マークダウンパーサ(ClojureおよびClojureScriptへコンパイルする)
生成されたソースの構成
src
└─myapp
    │  handler.clj …defines the base routes for the application, this is the entry point into the applicationand any pages
    │  repl.clj …provides functions to start and stop the application from the REPL
    │  util.clj
    │
    ├─routes …routes and controllers for our homepage are located
    │      home.clj …a namespace that defines the home and about pages of the application
    │
    └─views
        │  layout.clj …a namespace for the layout helpers
        │
        └─templates
                base.html …the base layout for the site
                home.html
                about.html

誤解を恐れず描くなら、リクエストの流れは概ね↓のイメージだろう。

チュートリアルをやってみる

Your first applicationに沿ってやってみる。
テンプレートを編集して、HOMEページをメッセージボードに置き換えるという内容。

プロジェクト新規作成
lein new luminus guestbook +h2

(H2の組み込みデータベースエンジンをサポートするテンプレートを作成)

(補足)+h2 オプションによって増えた依存ライブラリ
   [com.h2database/h2 "1.3.172"]
   [korma "0.3.0-RC5"]
   [log4j
    "1.2.17"
    :exclusions
    [javax.mail/mail
     javax.jms/jms
     com.sun.jdmk/jmxtools
     com.sun.jmx/jmxri]]]
(補足)生成されたソースの構成
src
│  ★+h2オプションによって増えた部分★
│  log4j.xml …logging configuration for Korma
│
└─guestbook
    │  handler.clj
    │  repl.clj
    │  util.clj
    │
    ├─models ★+h2オプションによって増えた部分★
    │      db.clj …used to house the functions for interacting with the database
    │      schema.clj …used to define the connection parameters and the database tables
    │
    ├─routes
    │      home.clj
    │
    └─views
        │  layout.clj
        │
        └─templates
                base.html
                home.html
                about.html
DB接続情報の編集

schema.clj の db-spec および db-store を編集する。
なお、create-tables関数が呼ばれたタイミングで、site.db.h2.dbファイルが resources フォルダ直下に生成される。

(def db-store "site.db")

(def db-spec {:classname "org.h2.Driver"
              :subprotocol "h2"
              :subname (str (io/resource-path) db-store)
              :user "sa"
              :password ""
              :naming {:keys clojure.string/lower-case
                       :fields clojure.string/upper-case}})
テーブル生成関数の編集

schema.clj に create-guestbook-table 関数を追加し、create-tables から呼び出すように編集する。

(defn create-guestbook-table []
  (sql/with-connection
    db-spec
    (sql/create-table
      :guestbook
      [:id "INTEGER PRIMARY KEY AUTO_INCREMENT"]
      [:timestamp :timestamp]
      [:name "varchar(30)"]
      [:message "varchar(200)"])
    (sql/do-commands
      "CREATE INDEX timestamp_index ON guestbook (timestamp)")))
(defn create-tables
  "creates the database tables used by the application"
  []
  (create-guestbook-table))
DBアクセス関数の編集

db.cljに以下を追記。

(defentity guestbook)

(defn save-message
  [name message]
  (insert guestbook
          (values {:name name
                   :message message
                   :timestamp (new java.util.Date)})))

(defn get-messages []
  (select guestbook))
アプリケーションの初期化処理の編集(DB初期化処理を追加)

handler.cljの関数 init(アプリケーション開始時に一度だけ呼ばれる関数)に、DB初期化処理(テーブル生成関数の呼び出し)を追加する。
requireに以下を追加。

[guestbook.models.schema :as schema]

init関数内の set-config! に続けて、以下を追加。

  ;;initialize the database if needed
  (if-not (schema/initialized?) (schema/create-tables))
コントローラの編集

home.clj の home-page 関数の定義を以下の通り置き換え(ゲストブック表示用のパラメータを追加)。

;(defn home-page []
;  (layout/render
;    "home.html" {:content (util/md->html "/md/docs.md")}))

(defn home-page [& [name message error]]
  (layout/render "home.html"
                 {:error    error
                  :name     name
                  :message  message
                  :messages (db/get-messages)}))

require に以下を追加。

[guestbook.models.db :as db]

また、ゲストブックへのメッセージ登録用のコントローラを追加する。

(defn save-message [name message]
  (cond
    (empty? name)
      (home-page name message "Somebody forgot to leave a name")
    (empty? message)
      (home-page name message "Don't you have something to say?")
    :else
      (do
        (db/save-message name message)
        (home-page))))

home-routes の定義に以下を追加する。

(POST "/" [name message] (save-message name message))
HTMLテンプレートの編集

home.html を以下の内容に置き換える。

{% extends "guestbook/views/templates/base.html" %}

{% block content %}
<ul>
{% for item in messages %}
  <li>
  	  <time>{{item.timestamp|date:"yyyy-MM-dd HH:mm"}}</time> 
      <p>{{item.message}}</p>
      <p> - {{item.name}}</p>      
  </li>
{% endfor %}
</ul>

{% if error %}
<p>{{error}}</p>
{% endif %}

<form action="/" method="POST">
    <p>
       Name: 
       <input type="text" name="name" value={{name}}>
    </p>
    <p>
       Message: 
       <textarea rows="4" cols="50" name="message">
           {{message}}
       </textarea>
    </p>
    <input type="submit" value="comment">
</form>
{% endblock %}
レイアウトを整える

resources/public/css/screen.css に以下を追記。

form {
    width: 200px;
    clear: both;
}
form input {
    width: 50%;
    clear: both;
}
パッケージング
  • 実行可能jar
lein ring uberjar

java -jar target/guestbook-0.1.0-SNAPSHOT-standalone.jar
  • war
lein ring uberwar

Clojure開発用Eclipseプラグインメモ その2

以前の日記「Clojure開発用Eclipseプラグインメモ - oknknicの日記」では軽くしか触れてなかったので、もう少し具体的な内容をメモ。
予想してたよりかなり高機能で、Eclipseでの開発を本命にしちゃいそうな気分になってきた。

Counterclockwiseプラグイン

Counterclockwiseの本日(2013/08/17)時点の最新安定版は0.12.0のよう。

導入

Eclipse

上記指定のとおり、Eclipse Juno(4.2)を導入。
Eclipse IDE for Java EE Developers Juno Service Release 2)

Counterclockwiseプラグイン

1.「Help>Eclipse Marketplace」にて「Clojure」で検索
2.「Counterclockwise」のInstallボタン押下

  • > Version 0.12.3.STABLE001 が導入された。(上記では0.12.0が最新とされていたが・・・)

プロジェクトの作成

1. New > Other > Clojure > Clojure Project
2. "Leiningen Template to use" には、プロジェクトのテンプレートを指定。例えば、
    "default" なら通常のClojureプロジェクト
    "compojure" ならCompojureによるWEBアプリプロジェクト

実行(通常のプロジェクトの場合)

ファイルを右クリック>Run As>Clojure Application で、該当ソースを名前空間としたREPLを起動できる。(なお、プロジェクトフォルダを右クリックした場合は、名前空間はuserになる)
エディタ内で右クリック>Clojure>"Load File in REPL" で、編集後のソースを読み込むことも可能。
また、S式を選択してCtrl-Enter(右記でも同じ:右クリック>Clojure>"Evaluate current selection or top-level form in the visible REPL")でS式単位の評価も可能。
(カーソル位置の最も外側のS式。範囲選択している場合は、その範囲内のみを評価)

実行(Compojureプロジェクトの場合)

別途cmdから実行しても良いが、
以下のとおり外部ツールとして「lein ring server」を登録しておいてもよいだろう。
1. Run > External Tools > External Tools Configurations
2. 「Program」を選択し、「New」を押下
3. 「Main」タブにて以下のとおり設定して「Apply」を押下
 Location: lein.batのフルパス(例:C:\lein\lein.bat)
 Working Directory:「Browse Workspace」を押下して上記プロジェクトを指定
 Arguments: ring server
4. Run > External Tools > Organize Favorites
5. 「Add」から、上記で登録した外部ツールを登録する

コードアシスト

上記に記載のとおり、Clojureのシンボルも java もアシストされる。
java は .(ドット)始まりで入力したときにアシストされる。
ただし、java は(型指定がないため)アシスト候補が膨大なため、少々遅い。

<20013/08/22追記>
なお、コードアシストやタグジャンプ、ドキュメント表示機能等は、
REPLを起動していてかつその名前空間から参照(再帰的な参照も含む)しているものに対してのみ可能なよう。(挙動から推察する限りは)
そのため、コーディングする際は、core.cljの右クリックからREPLを起動した状態で行うのが良さそう。

その他機能

Java開発時と同様の機能など、以下のようなものも実装されている!!

  • マウスカーソルを合わせるとドキュメント表示
  • F3で定義へジャンプ
  • デバッグ実行
  • strict/pareditモード(エディタで右クリック>Clojure>SwitchEditMode)
  • 名前検索(Window>ShowView>Other>ClojureView>NamespaceBrowser)

などなど

Clojure対応のIDE Light Table を使ってみた

リアルタイムに評価・おしゃれに結果表示してくれるClojure対応のIDEである Light Table を使ってみた。
アルファ版(2013/08/17時点のバージョンは0.4.11)とのことで、今後の展開にも期待。

導入

  • ダウンロード、解凍

http://www.lighttable.com/

REPL起動

command から「Instarepl: Open a clojure instarepl」
(なお、右上に表示されている「live」ボタンのON/OFFで、リアルタイム評価の有効無効の切り替えが可能)

現在開いているエディタをREPLにする

command から「Instarepl: Make current editor an instarepl」

エディタでのキーバインド

C-Enter S式単位の評価
C-Shift-Enter ファイル全体の評価
TAB オートコンプリート

キーバインド一覧の確認

command から「Settings: Change key bindings/shortcuts」を選択

Clojureで単体テストメモ〜clojure.test と Midje〜

Clojureでの単体テストについてメモ。
ざっと、以下の選択肢があるよう。

  • clojure.test 標準テストライブラリ
  • Midje テストフレームワーク。可読性の高いテスト記述をサポートする
  • test.generative テストデータ生成、テスト実行ライブラリ。期待値ではなく関数の性質を記述し、乱数引数に対する結果がその性質を満たすかをテストする

今回は、 clojure.test と Midje についてメモ。

clojure.test

利用例
  • テスト対象(src/testapp/core.clj)
(ns testapp.core)

(defn square "自乗" [x] (* x x))
  • テストコード(test/testapp/core_test.clj)
(ns testapp.core-test
  (:use clojure.test
        testapp.core))

; is 利用の場合
(deftest square-test-example01
  (testing "自乗"
    (is (= 4 (square 2)))))

; are 利用の場合
(deftest square-test-example02
  (testing "自乗"
    (are [y x] (= y (square x))
          1 1
          4 2
          9 3)))

; testing の階層化も可能
(deftest square-test-example03
  (testing "squareは"
    (testing "引数の自乗を返す"
      (are [y x] (= y (square x))
            1 1
            4 2
            9 3))
    (testing "正の数を返す"
      (are [x] (<= 0 (square x))
            -1
            0
            1))
    (testing "数字以外が渡された場合はClassCastExceptionを投げる"
      (are [x] (thrown? java.lang.ClassCastException (square x))
            "hoge"
            [1]))))
Leiningenでテスト実行
lein test
REPLでテスト実行
(ns testapp.core-test)
(run-tests)

Midje

「テストコード => 期待値」という直感的な記述ができるのが嬉しい。

導入
  • project.clj の :profiles に以下を追加
{:dev {:dependencies [[midje "1.5.1"]]}}
  • project.clj の :plugins に以下を追加
[lein-midje "3.0.0"]
利用例
  • テストコード(test/testapp/core_test.clj)
(ns testapp.core-test
  (:use midje.sweet
        testapp.core))

(facts "square関数"
  (fact "自乗である"
    (square 1) => 1
    (square 2) => 4))
テスト実行
lein midje