(Clojure) vsClojureを触ってみた
この記事は Clojure Advent Calendar 2014 - Qiita 15日目の記事です。
まえおき
去る2014/11/12、無償オープンソース版VisualStudio である VisualStudio Community 2013 が公開されました。Pro相当で機能的な制限はなし。となれば、Clojureの.Net版である Clojure CLR にも今後何らかの動きがあるかもしれない、という期待を抱きつつ久しぶりに触ってみました。このエントリはそのすったもんだです。
vsClojureの導入
VisualStudio(以下 VSと略)のClojure拡張は「vsClojure」です。ツールメニューの「拡張機能と更新プログラム」からインストールできます。
今回は触れませんが、Clojurescriptという記述もありますね。
インストール後VSを再起動するとプロジェクトテンプレートにClojure向け4種のテンプレートが選択できるようになります。
上二つはライブラリ、下二つは実行形式アセンブリのプロジェクトです。といいつつも、実は3と4のプロジェクト形態は違いが無く、Windows Forms を選んでも実行時にコマンドプロンプトを表示してしまいます。
C#等のプロジェクトのようなプロジェクト設定ペインは見当たらず、プロンプトを表示せずに実行する方法はわかりませんでした。無いのかも。
プロジェクトプロパティを開くとこんなシンプルすぎるダイアログが開くのみです。
Clojure Version 1.5 です。なんと変更できません!*1...このあたりがテンションを下げるところです。どうなってんの? 責任者はどこだ?
githubですかねやっぱ。
で、github
プロジェクトページを開くと最初に目に飛び込むのはビートルズばりのシャウトで始まるREADME.md
!!!HELP!!! THIS PROJECT NEEDS DEVELOPERS !!!HELP!!!
お察しください。
仕方がないのでプラグイン開発に必要な Visual Studio2013 SDK をインストールし、git clone作ってビルドしようとしましたが、どうもVS2010辺りじゃないとバージョンが合わないらしくあえなく挫折。ああ...。
まーでも、1.5ならギリギリ許容できるラインかなーと納得して次に行きます。納得するしかないし。
vsClojureの開発環境はこんな感じ
書き忘れてますが、vsClojureの開発環境はこんな感じです。
一見良さそうですが「プログラムのデバッグ出力」「repl」「エラー表示」は連携が取れてなくて行ったり来たりすることになります。ブレークポイントのマークは付きますがブレークしません。インテリセンスは有ります、が、Ctrl+Space を押した後候補が出るまで5秒くらい待たされます。当然その間はフリーズしてます。
ま、でもオブジェクトブラウザでアセンブリの中を眺められるのはいいかもしれませんね。gensymっぽいシンボルでいっぱいですが。
さて、ノープランですが
グチばかりではアレですので、簡単なプログラムでお茶を濁したいと思います。
手抜きreplでも作りますか。
ソースコードをutf8で保存しないと、ソース内の日本語が化けますのでご注意を。
おわりに
vsClojureは大いに進展してほしいプロジェクトです。
来年は頑張ってね。
追記 (12/16)
コードを修正しました。
- システムコントロール系のdynamic Var の binding をすることで、set! による挙動の変更が可能に。
- 初期 *ns* を user に移動。
- マクロ等をevalした時に CompilerException で落ちてしまうので、catch を追加 (Exceptionじゃ捕まえられない?)
- pool-vec と line を連結するとき スペースじゃなく 改行を挟むように変更。read-lineで読んでいるので改行文字が欠落してしまう。それをスペースで連結するとコードが変わってしまう。
プログラム実行例も作り直しました。以前の画像では.Netらしさが無かったので、Formsアセンブリを使ってみました。PowerShellになっているのは特に意味はありません。
(Clojure)when-let と some->>
タイトルを見て何のことか想像つく人には多分つまらない話です。ついでに nil と seq というキーワードも追加します。おそらく想像通りの内容です。あしからず。
少し長い前置き
前のエントリで逆引き天気予報のプログラムを書きましたが、書いていて何か違和感を感じていました。この部分です。
(defn report* [re id] (let [rss (rss-zip (city-url id))] (when-let [fcs (-> (search-forecast re rss) seq)] (let [city (title-city rss)] (->> fcs (map (fn [fc] {:forecast fc :city city :id id})))))))
解説します。
reが予報の検索条件で正規表現のpatternオブジェクトを渡します。idは地域を表す番号です。このへんはどうでもいいです。
最初のletで束縛するrssは、Webから取得したRSSデータです。そのRSSデータから search-forecastで条件 re に該当するものを抽出して fcs を束縛します。fcs は when-let による束縛なので値が「真」の場合のみ内側の式が評価されます。「偽」ならば内側は評価されずwhen-letの値、延いてはreport*の値が nil になります。
さて、ここにClojureならではの小細工があります。この部分。
(when-let [fcs (-> (search-forecast re rss) seq)] (->> fcs expr ...))
search-forecastは条件に合ったすべての予報をコレクションで返します。この返却されたコレクションが空か空でないかをwhen-letで判定したいのですが、Clojureでは「空のコレクション」はBooleanの文脈で「真」と評価されてまうんです。それじゃあ困る。「偽」になってくれないと内側の式を実行してしまう。
そこで登場するのが seq です。seq はコレクションをシーケンスに変換する関数ですが、空のコレクションに適用すると nil を返すという特性を持っています。nilならば偽値なので、めでたくwhen-letが期待どおりの動作、つまり「空の場合内側をスキップ」してくれる、という訳です。
when-letをsome->>で書き換える
when-letは対象の真偽値によって後続の処理を取捨選択します。Clojure1.5でこれとよく似た役割を持ったマクロが追加されました。some-> と some->> です。some->>を使って上記when-letのコードを書き替えると...
(some->> (search-forecast re rss) seq expr ...)
スッキリしましたね。束縛 fcs が消えました。
some->> はマクロ ->> と同様に式を順に評価してゆき、評価値がnilになるとそれ以降の式は評価せず、最終的に nil を返します。この例では、search-forecast が空のコレクションを返し、seqによって nil となったところで評価が終わります。最初に書いた when-let に期待する動作とほぼ同じ挙動です。
when-let に対する違和感の正体はこれでした。こっちの方がスッキリ書けたんです。
(title-city rss)の置き場所
もう一度最初のコードのwhen-let以降を見てみます。
(when-let [fcs (-> (search-forecast re rss) seq)] (let [city (title-city rss)] (->> fcs (map (fn [fc] {:forecast fc :city city :id id})))))
先ほど書いたコードではわざと無視しましたが、実はこのプログラムではwhen-let と (->> fcs expr ..) の間に let があります。
title-city は rss から唯一の値 city を取り出す関数です。cityは予報対象の都市(地域名)です*1。コレクションfcsは map によって「【3つのキーを持つマップコレクション】を要素とするシーケンス」に変換されます。その「マップコレクション」に city を含めています。rssにおいてcityは唯一の値なので、title-cityは一度評価すればよく、以降生成するマップコレクションには同じcityを渡してゆけばよいことになります。
では、これを some->> で書きなおす場合、cityはどこで取得すればよいのでしょうか?
分かりやすいのはこうでしょう。
(let [city (title-city rss)] (some->> (search-forecast re rss) seq (map (fn [fc] {:forecast fc :city city :id id}))))
これは when-let のコードと同じではありません。city取得の式を外側に移動したため、search-forecastの返却値が空であってもtitle-cityが評価されます。返却値が空の場合 city は不要なので無駄になります。
じゃあどこに置いたらいいのか考えたんですが、結局こうなりました。
(some->> (search-forecast re rss) seq (map (fn [city fc] {:forecast fc :city city :id id}) (repeat (title-city rss))))
うわっ、微妙。実に微妙。
可読性というのは人それぞれですが、これを分かりやすいかと聞かれたら個人的にはギリギリセーフくらいかなぁと思います。微妙だ。
でも嫌いじゃない。
おわりに
しょうもない些細なことをうだうだと書いてみました。
パフォーマンスを追及しているわけでもガイドラインを提案しているわけでもなく、何となく持っているこだわりみたいな物ですかね。
これもまたClojureならではのお話かと。
追記
だったらもうこれでいい気がしました。
(defn report* [re id] (let [rss (-> id city-url rss-zip)] (map (fn [city fc] {:forecast fc :city city :id id}) (repeat (title-city rss)) (search-forecast re rss))))
(Clojure)サーバーにやさしい逆引き天気予報
[追記] tenkijp の rss サービスが終わってしまいました。下記プログラムももう使えません。
ポイント
clojure.core.cache で取得データを5分間保持しサーバー負荷を軽減しています。replなどの起動しっぱなしの実行環境でなければ意味はありませんが。
依存ライブラリ
:dependencies [[org.clojure/clojure "1.5.1"] [org.clojure/data.zip "0.1.1"] [org.clojure/core.cache "0.6.3"]]
実行例
関東甲信越地方で予報に「雪」が含まれるものを検索。
50 と 77 が地域を表すIDです。report に 50 77 を指定すると、「50,新潟県 下越(新潟)」~「76,山梨県 東部・富士五湖(河口湖)」の間の全地域が検索対象になります。
地域IDの一覧も、コードと一緒にアップしてあります。
tenki.core> (require '[clojure.pprint :refer [pprint]]) nil tenki.core> (->> (report #"雪" 50 77) (sort-by :forecast) (map (juxt :forecast :city)) pprint) (["18日(火) 曇時々雪 3℃/-1℃" "新潟県 下越(新潟)"] ["18日(火) 雪時々止む(雪は時々止む) ---/---" "新潟県 中越(長岡)"] ["18日(火) 雪時々止む(雪は時々止む) 3℃/1℃" "新潟県 上越(高田)"] ["19日(水) 曇一時雪 3℃/-2℃" "新潟県 上越(高田)"] ["19日(水) 雪のち曇 2℃/-2℃" "新潟県 中越(長岡)"] ["19日(水) 雪のち曇 3℃/0℃" "新潟県 下越(新潟)"] ["21日(金) 曇時々雪 0℃/-5℃" "群馬県 北部(みなかみ)"] ["21日(金) 曇時々雪 1℃/-1℃" "新潟県 中越(長岡)"] ["21日(金) 曇時々雪 1℃/-1℃" "新潟県 上越(高田)"] ["21日(金) 曇時々雪 1℃/-4℃" "長野県 北部(長野)"] ["21日(金) 曇時々雪 2℃/-1℃" "新潟県 下越(新潟)"] ["21日(金) 曇時々雪 2℃/0℃" "新潟県 佐渡(相川)"] ["22日(土) 曇一時雪 3℃/-1℃" "新潟県 下越(新潟)"] ["22日(土) 曇一時雪 3℃/-1℃" "新潟県 中越(長岡)"] ["22日(土) 曇一時雪 3℃/-1℃" "新潟県 上越(高田)"] ["22日(土) 曇一時雪 3℃/-1℃" "新潟県 佐渡(相川)"] ["26日(水) 晴一時雪 10℃/2℃" "栃木県 南部(宇都宮)"] ["26日(水) 晴一時雪 11℃/-1℃" "栃木県 北部(大田原)"] ["26日(水) 晴一時雪(雨の可能性) 10℃/1℃" "群馬県 南部(前橋)"] ["26日(水) 晴一時雪(雨の可能性) 8℃/-1℃" "長野県 中部(松本)"] ["26日(水) 晴一時雪(雨の可能性) 8℃/0℃" "山梨県 東部・富士五湖(河口湖)"] ["26日(水) 晴一時雪(雨の可能性) 9℃/-1℃" "山梨県 中・西部(甲府)"] ["26日(水) 晴一時雪(雨の可能性) 9℃/-2℃" "長野県 南部(飯田)"] ["26日(水) 曇一時雪 10℃/0℃" "長野県 北部(長野)"] ["26日(水) 曇時々雪 6℃/-4℃" "群馬県 北部(みなかみ)"] ["27日(木) 晴一時雪 11℃/-2℃" "長野県 北部(長野)"]) nil tenki.core>
おわりに
core.cacheを初めて使いました。
少し癖のあるモジュールですが、今回のように期限付きのメモ化を実現するのに有効のようです。