4/23/2011

第4回 Common Lispライブラリガイド

 さて、Modern Common Lispはこれで4回目です。環境構築も完成に近づき、Common Lispでプログラムを始められる状態になりつつあります。ブログの主題も環境構築から実践へと移ります。

 今回はCommon Lispでプログラムを書く際によく必要になるであろうライブラリの紹介です。Common LispはSchemeと比べると仕様の大きな言語には違いありませんが、最近普及しているPythonなどに比べると標準ライブラリも小さいです。そのため、適切なライブラリを適切に使用するという能力は、他の言語以上にCommon Lispで必要になるでしょう。

 ここで紹介するライブラリはすべてQuicklispに入っているのですぐ利用できます。まだインストールしていない人は以下のエントリを参考にインストールしてください。

正規表現を使う - CL-PPCRE

 Common LispにはPerlのように標準で正規表現が付属するわけではありませんが、Perl5互換の正規表現ライブラリ「CL-PPCRE」があります。

 Let Over Lambdaでべた褒めされているのが印象的でした。ガリガリにチューニングされていてPerlの正規表現エンジン(つまりCで書かれた正規表現エンジン)よりも高速らしいです。

 以下はUserAgentを元にマッチングするサンプルコードです。

(defvar user-agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.28 (KHTML, like Gecko) Chrome/12.0.728.0 Safari/534.28")

 Common LispでUserAgentによるアクセス判定をしてみます。

(import '(ppcre:scan ppcre:scan-to-strings ppcre:regex-replace-all ppcre:split))

;; 通常のマッチング
;; Macからのアクセスか判定
(scan "\\(Macintosh;" user-agent)
;=> 12
;   23
;   #()
;   #()

;; マッチした部分を取り出したい
;; クライアントのMacのバージョンを知りたい
(scan-to-strings "Mac OS X ([^\\)]+)" user-agent)
;=> "Mac OS X 10_6_6"
;   #("10_6_6")

;; 文字列置換
;; Chromeの部分をMSIEに変える
(regex-replace-all "Chrome" user-agent "MSIE")
;=> "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.28 (KHTML, like Gecko) MSIE/12.0.728.0 Safari/534.28"
;   T

;; 文字列分割
;; クエリを分解
(split "&" "name=Eitarow%20Fukamachi&age=23")
;=> ("name=Eitarow%20Fukamachi" "age=23")

 Let Over Lambdaではリードマクロを使って関数適用のように正規表現を扱う方法が紹介されています。より簡単な表記をしたい人には参考になるでしょう。

日付を処理したい - LOCAL-TIME

 日付操作はお遊びプログラムなどで出てくる定番の一つです。年末になるとよくカウントダウンとかして、新年まであと何秒とかやりますね。Common Lispで日付を扱うには「LOCAL-TIME」を使います。

 簡単なサンプル。

(import '(local-time:now local-time:parse-timestring))

;; 現在時刻 (返り値はTIMESTAMPオブジェクト)
(now)
;=> @2011-04-11T18:57:23.493142+09:00

;; 文字列からのparse
(parse-timestring "2015-02-18")
;=> @2015-02-18T09:00:00.000000+09:00

 ちなみに2015/02/18は僕が生まれて10000日目らしいです(日齢計算より)。実際に試してみます。

(import '(local-time:timestamp-difference local-time:parse-timestring))

(/ (local-time:timestamp-difference
    (local-time:parse-timestring "2015-02-18")
    (local-time:parse-timestring "1987-10-03"))
   (* 24 60 60))
;=> 10000

 逆に自分の生誕10000日目を知るには以下の関数に自分の誕生日を渡せばよいです。

(import '(local-time:timestamp+ local-time:parse-timestring))

(defun your-10000th-day (daystring)
  (timestamp+
    (local-time:parse-timestring daystring)
    864000000
    :sec))

シェルコマンドを実行したい - trivial-shell

 Lispコードからシェルコマンドを実行するには「trivial-shell」が使えます。

;; シェルコマンドを実行
(trivial-shell:shell-command "ls ~/Programs/lib/clack/")
;=> (#P"/Users/fukamachi/Programs/lib/clack/.git/"
;    #P"/Users/fukamachi/Programs/lib/clack/.gitignore"
;    #P"/Users/fukamachi/Programs/lib/clack/README.markdown"
;    #P"/Users/fukamachi/Programs/lib/clack/clack-test.asd"
;    #P"/Users/fukamachi/Programs/lib/clack/clack.asd"
;    #P"/Users/fukamachi/Programs/lib/clack/src/"
;    #P"/Users/fukamachi/Programs/lib/clack/t/"
;    #P"/Users/fukamachi/Programs/lib/clack/tmp/")

;; シェルをZshに変更 (デフォルトは "/bin/sh")
(setf trivial-shell:*bourne-compatible-shell* "/bin/zsh")
;=> "/bin/zsh"

;; 環境変数の値を取得
(trivial-shell:get-env-var "PATH")
;=> "/usr/bin:/bin:/usr/sbin:/sbin"

ファイル操作をしたい - CL-FAD

 日常の簡単なスクリプトなどを書くとき、Lispからファイルシステムにアクセスすることもあるでしょう。「CL-FAD」を使えばOSに依存しない可搬なプログラムが書けます。

(import '(cl-fad:list-directory cl-fad:walk-directory cl-fad:delete-directory-and-files))

;; ディレクトリのファイル一覧
(list-directory #p"~/Programs/lib/clack/")
;=> (#P"/Users/fukamachi/Programs/lib/clack/.git/"
;    #P"/Users/fukamachi/Programs/lib/clack/.gitignore"
;    #P"/Users/fukamachi/Programs/lib/clack/README.markdown"
;    #P"/Users/fukamachi/Programs/lib/clack/clack-test.asd"
;    #P"/Users/fukamachi/Programs/lib/clack/clack.asd"
;    #P"/Users/fukamachi/Programs/lib/clack/src/"
;    #P"/Users/fukamachi/Programs/lib/clack/t/"
;    #P"/Users/fukamachi/Programs/lib/clack/tmp/")

;; ホームディレクトリ以下のMP3ファイルを再帰的に検索
(walk-directory
  #p"~/"
  (lambda (file)
    (when (string-equal (pathname-type file) "mp3")
      (fresh-line)
      (princ file))))
;-> /Users/fukamachi/nagaku.mp3
;   /Users/fukamachi/narunode.mp3
;   /Users/fukamachi/shoryaku/shimasu.mp3
;   ...

HTTPリクエストを投げたい - Drakma

 LispでHTTPリクエストを投げるには「Drakma」を使います。

 http-requestにURLを与えるとコンテンツ、HTTPステータスコード、HTTPヘッダなどが多値で返ってきます。以下はGoogle翻訳のAPIを叩いてみたサンプルです。

(import '(drakma:http-request))

(multiple-value-bind (body status header)
    (drakma:http-request "http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&langpair=en%7Cja&q=Hello%2C%20World")
  (format t "~&Status: ~A~%Body: ~A~%Header: ~A~%"
          status body header))
;-> Status: 200
;   Body: {"responseData": {"translatedText":"こんにちは、世界"}, "responseDetails": null, "responseStatus": 200}
;   Header: ((CACHE-CONTROL . no-cache, no-store, max-age=0, must-revalidate) (PRAGMA . no-cache) (EXPIRES . Fri, 01 Jan 1990 00:00:00 GMT) (DATE . Thu, 07 Apr 2011 12:10:42 GMT) (CONTENT-TYPE . text/javascript; charset=utf-8) (X-BACKEND-CONTENT-LENGTH . 80) (X-EMBEDDED-STATUS . 200) (X-CONTENT-TYPE-OPTIONS . nosniff) (X-FRAME-OPTIONS . SAMEORIGIN) (X-XSS-PROTECTION . 1; mode=block) (SERVER . GSE) (CONNECTION . close))
;=> NIL

 bodyがJSONで返ってきているので、これをパースすれば英語を翻訳した結果だけを受け取れそうです。パースはCL-JSONを使うのが丁寧でしょうが、今回はそこまでする必要も感じないのでCL-PPCREで翻訳部分だけ抜き取ります。

(ppcre:scan-to-strings
 "(?<=translatedText\":\")[^\"]*"
 (nth-value 0
  (drakma:http-request "http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&langpair=en%7Cja&q=Hello%2C%20World")))
;=> "こんにちは、世界"

 余裕がある人はGoogle翻訳する関数を書いてみるといいかもしれません。

単体テスト - CL-TEST-MORE

 自動化されたテストがないプロダクトはレガシーだと言われるようになったのはどうも最近のように思われますが、Lispは意外にも自動テストに関して熱心な傾向があります。

 単体テストフレームワークもいくつか選択肢があります。ここでは「CL-TEST-MORE」というPerlのTest::Moreというモジュールに影響されたライブラリを紹介します。

 関数などはほぼTest::Moreと同じです。

(import '(cl-test-more:is cl-test-more:plan cl-test-more:deftest cl-test-more:run-test-all))

(plan 9)

;; check if first argument is true
(ok (eq got expected) "Description")

;; check if "got" equals "expected"
(is got expected "Description")
(isnt got expected "Description")
;; with :test function
(is got expected "Description" :test #'string=)

;; rather than print *standard-output* "# This is just a comment\n"
(diag "This is just a comment")

;; macro expansion
(is-expand (got macro) (expected :like "this") "Description")

;; output
(is-print (write-line "aiueo") "aiueo\n" "Description")

;; functions always pass or fail
(pass "Description")
(fail "Description")

(finalize)

 出力結果はみんな大好きTAP(Test Anything Protocol)です。

ユーティリティ集 - Alexandria

 最後に、Common Lispのユーティリティ集についてです。

 もし「Haskellの○○はCLにはないのか…」とか「こんな関数が標準的にあったらいいのになぁ」と思ったら、自分で実装する前に「Alexandria」を探すといいです。AlexandriaはCLの標準ユーティリティ集的存在を目指したものです。

 欲しいコマンドはaproposで探します(それかac-slime)。

(apropos "plist" :alexandria)
;->  ALEXANDRIA.0.DEV:ALIST-PLIST, Def: FUNCTION
;    ALEXANDRIA.0.DEV:DELETE-FROM-PLIST, Def: FUNCTION
;    ALEXANDRIA.0.DEV:DELETE-FROM-PLISTF, Def: MACRO FUNCTION
;    ALEXANDRIA.0.DEV:DOPLIST, Def: MACRO FUNCTION
;    ALEXANDRIA.0.DEV:HASH-TABLE-PLIST, Def: FUNCTION
;   ALEXANDRIA.0.DEV::MALFORMED-PLIST, Def: FUNCTION
;                     MAPLIST, Def: FUNCTION
;   ALEXANDRIA.0.DEV::PLIST
;    ALEXANDRIA.0.DEV:PLIST-ALIST, Def: FUNCTION
;    ALEXANDRIA.0.DEV:PLIST-HASH-TABLE, Def: FUNCTION
;    ALEXANDRIA.0.DEV:REMOVE-FROM-PLIST, Def: FUNCTION
;    ALEXANDRIA.0.DEV:REMOVE-FROM-PLISTF, Def: MACRO FUNCTION
;                     SYMBOL-PLIST, Def: FUNCTION
;=> nil

 僕はplistが好きなのでplist系の関数群や、with-gensymsなどの汎用的なマクロをよく使っています。

まとめ

 この他にも、こういったことがしたければこのライブラリだろう、というものはいくつもあります。面倒なので紹介はしませんが以下に名前だけ載せておきます。

 また余談ですが、Quicklispのダウンロード数ランキングも公開されています。何かの参考になるかもしれません。

 さて、今回はCommon Lispライブラリを紹介しました。ライブラリの使用に関しては(Quicklispがあれば)特別困ることもないと思います。

 この連載は松山さんと交互に書いているため、今後話題が交互することになるでしょう。次の僕の回ではモダンなCommon Lispライブラリの作り方について説明します。

4/07/2011

第3回 SLIMEの使い方 基礎編

前回はQuicklispによるライブラリ管理について説明しました。今回はSLIMEの 基本的な使い方について説明します。

SLIMEとは

SLIMEは(Common) Lispのための統合開発環境(IDE)です。他のIDEとは異なり、 対話的にプログラムを構築できるのが特徴です。一度ハマれば他の開発環境は 使えなくなるでしょう。詳しい情報は次のURLを参照してください。
http://common-lisp.net/project/slime/

インストールと設定

SLIME

SLIMEのCVSスナップショット(本家推奨)をインストールします。OSは GNU/LinuxやMac OS XなどのUNIX系OSを想定しています。
$ wget http://common-lisp.net/project/slime/snapshots/slime-current.tgz
$ tar xvzf slime-current.tgz
$ mv slime-* ~/.emacs.d/slime
展開したディレクトリはslime-2011-04-07のような名前になりますが、ここ ではslimeという名前で~/.emacs.d/直下に移動します。
次にEmacsの設定を行います。次のコードを~/.emacsに書いてください。
;; Clozure CLをデフォルトのCommon Lisp処理系に設定
(setq inferior-lisp-program "ccl")
;; ~/.emacs.d/slimeをload-pathに追加
(add-to-list 'load-path (expand-file-name "~/.emacs.d/slime"))
;; SLIMEのロード
(require 'slime)
(slime-setup '(slime-repl slime-fancy slime-banner slime-indentation))
これで基本的な設定は完了です。
2011/7/10追記
若干設定項目が足りなかったので補足します。
;; SLIMEからの入力をUTF-8に設定
(setq slime-net-coding-system 'utf-8-unix)
2013/09/20追記
slime-indentationを追加しました。これで後述のcl-indent-patchesの設定は不要です。

popwin.el

使い始めれば気付くと思いますが、SLIMEは事あるごとにEmacsのウィンドウを 分割したり、他のウィンドウのバッファを切り替えたりします。これではせっ かくの優れた開発環境も台無しです。そこで、拙作のpopwin.elをインストール することをお勧めします。popwin.elはウィンドウの分割等を極力抑制し、プロ グラマが快適に作業することを助けてくれる優れものです。
インストール方法や使い方は次のURLを参照してください。
http://d.hatena.ne.jp/m2ym/20110120/1295524932
インストールが完了したら次のコードを~/.emacsに書いてください。
;; Apropos
(push '("*slime-apropos*") popwin:special-display-config)
;; Macroexpand
(push '("*slime-macroexpansion*") popwin:special-display-config)
;; Help
(push '("*slime-description*") popwin:special-display-config)
;; Compilation
(push '("*slime-compilation*" :noselect t) popwin:special-display-config)
;; Cross-reference
(push '("*slime-xref*") popwin:special-display-config)
;; Debugger
(push '(sldb-mode :stick t) popwin:special-display-config)
;; REPL
(push '(slime-repl-mode) popwin:special-display-config)
;; Connections
(push '(slime-connection-list-mode) popwin:special-display-config)

ac-slime

ac-slimeはauto-complete.elのSLIME拡張です。SLIME(SWANKサーバー)の情報を 利用するため、非常に高精度のコード補完が行えます。
ac-slimeは次のURLから入手できます。
https://github.com/purcell/ac-slime
なおauto-complete.elをあらかじめインストールしておく必要があります。イ ンストールしていない方は次のURLを参照してください。
http://cx4a.org/software/auto-complete/index.ja.html
ac-slimeをインストールするには、先のURLからac-slime.elをダウンロードし、 load-pathの通ったディレクトリにコピーします。install-elispや auto-installがある場合は次のコードを評価することでインストールできます。
;; install-elisp
(install-elisp "https://github.com/purcell/ac-slime/raw/master/ac-slime.el")
;; auto-install
(auto-install-from-url "https://github.com/purcell/ac-slime/raw/master/ac-slime.el")
最後に~/.emacsに次のコードを書いてインストール完了です。
(require 'ac-slime)
(add-hook 'slime-mode-hook 'set-up-slime-ac)
(add-hook 'slime-repl-mode-hook 'set-up-slime-ac)
これで.lispファイルやREPLでauto-complete.elを使った自動コード補完が行え るようになりました。

cl-indent-patches.el

↓SLIMEに取り込まれたため、このcl-indent-patches.elの導入は不要です。(2013/09/20)
この節は2011/7/10に追記しました
Emacs標準のインデント機能には若干問題があります。特にloopマクロのイン デントが変です。例えば次のようなインデントになってしまいます。
(loop for x in lst
      if (oddp x)
      collect x)
本来は次のようにインデントされるべきです。
(loop for x in lst
      if (oddp x)
        collect x)
このあたりに気をきかせてくれるのがcl-indent-patches.elです。次のURLから 入手できます。
http://boinkor.net/lisp/cl-indent-patches.el
このファイルをload-pathの通ったディレクトリに配置してください。後は .emacsに次のような設定を書けばOKです。
(when (require 'cl-indent-patches nil t)
  ;; emacs-lispのインデントと混同しないように
  (setq lisp-indent-function
        (lambda (&rest args)
          (apply (if (memq major-mode '(emacs-lisp-mode lisp-interaction-mode))
                     'lisp-indent-function
                     'common-lisp-indent-function)
                 args))))

SLIMEの使い方

SLIMEの起動

インストールと設定が完了したらM-x slimeとやってみましょう。次のような 表示の*slime-repl ccl*というREPLバッファが表示されれば成功です。
CL-USER> 
SLIMEを使って開発するに際して、まずやることはこのM-x slimeです。これ を行わなければ、後で説明するエディタコマンドやインデント、コード補完、 その他諸々が全く機能しません。まずM-x slime、これだけ覚えておいてくだ さい。
なお表示されたREPLバッファはちょっとした確認などを行ったりする場合を除 いて基本的には使用しません。

SLIMEの操作

SLIMEを起動して次にすることはlispファイルを開くことです。試しに foo.lispなどの適当なファイルを開いてください。
lispファイルのメジャーモードはlisp-modeです。このモードには様々なキー が割り当てられていますが、今回はその中でも覚えておくべきキーバインドを 紹介します。

C-c C-c

C-c C-cは現在ポイントしているトップレベルフォームをコンパイルします。 例えば次のような関数を編集しているとします。
(defun f (a)
  "Hello, World")
ここでC-c C-cすると関数fがコンパイルされます。実際にやってみれば分 かると思いますが、上の関数をコンパイルすると、SLIMEは変数aが未使用で あると警告してくれます。もし警告があればそれを修正してC-c C-c、という のが基本的な開発サイクルになります。

C-c C-k

C-c C-kは現在のファイルをコンパイルしてロードします。トップレベルフォー ムを一つずつC-c C-cするのが面倒なときに重宝します。また、C-c C-c同 様、ソースコードに問題があれば警告してくれるので、ファイルの最終的な確 認にも利用できます。

C-c C-z

C-c C-zは現在接続しているサーバーのREPLバッファを表示してくれます。何 か確認したいときや、前回紹介したQuicklispでライブラリをロードしたいとき にC-c C-zします。例えばCL-TEST-MOREというライブラリをロードし忘れてい たとしたら、C-c C-zでREPLバッファを表示して、次のように入力します。
CL-USER> (ql:quickload :cl-test-more)
REPLは使いこなせば非常に強力なので是非活用してください。

困った時

より本格的な開発サイクルは次回で説明しますが、SLIMEにおけるEmacsの C-g的な存在としてM-x slime-restart-inferior-lispを紹介しておきます。
これは現在接続しているサーバーを再起動してクリーンな状態に戻すコマンド です。SLIMEで開発していると、変にシンボルがインターンされたり、おかしな 値の変数ができたりします。また、正しく動作しているとしても、実は古い変 数などが残っていて偶然動作しているだけだったりもします。そういった状態 をリセットするのにM-x slime-restart-inferior-lispは非常に便利です。是 非覚えておいてください。

まとめ

今回は「SLIMEの使い方 基礎編」ということで、SLIMEのインストールと設定、 および非常に基本的な使い方を紹介しました。実際のところ、今回の内容だけ でSLIMEで開発を行うにはかなり情報が不足しています。Common Lisperなら気 付いていると思いますが、パッケージの扱いなど、様々な重要な点を端折って います。次回以降、より実際的なSLIMEの使い方を紹介していきたいと思います。