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ライブラリの作り方について説明します。

0 件のコメント:

コメントを投稿