この記事はNextremer Advent Calendar 2018の5日目の記事です。そして、1日目の記事「業務を改善していこう、Common Lispで。」の続編でもあります。
NextremerのCommon Lisp大好きプログラマ、t-sinです。この記事では1日目の記事では割愛した、GitHubメンション通知Bot、Nikoの内部の話をします。
前回までのあらすじ
GitHubのIssue/Pull Requestの確認依頼メッセージを各個手動でSlackにポストしていた現状を、Common Lispで実装されたGitHubメンション通知Botの力で自動化したt-sin。その結果、社内Slackのコミュニケーションのノイズが減少し、かつレビュー等の依頼漏れもなくなり、快適な開発へ一歩近づくことに成功した。
しかし、そのプログラムNiko (https://github.com/t-sin/niko) の内部についてはとくに言及されていない。いったいどのようなツール・ライブラリを用いて実装されたのか? Common Lispでモダンにウェブアプリケーションを実装するには、どのようなことをすればいいのか!?
ぼくがCommon Lispを用いてウェブアプリケーションをつくるのが初めてだったため、今日においてウェブ開発にどのようなライブラリやツールを利用したのかと、それらの理解不足ゆえのハマりポイント等を解説します。
利用したCommon Lispのツール・ライブラリたち
Nikoをつくる上で、以下のツール・ライブラリを利用しました:
- Common Lisp処理系マネージャ: Roswell
- ライブラリバージョン管理ツール: Qlot
- ウェブアプリケーションフレームワーク: Utopian
- O/Rマッパー: Mito
- HTTPクライアント: Dexador
- JSONエンコーダ・デコーダ: Jonathan
これらのうち、RoswellとUtopianは解説しません。Roswellの基本は拙作の記事「いまから始めるCommon Lisp」を参照してください。Utopianについてはよくまとまった記事「Utopian手習い #01」があり、かつNikoでは最新のnext
ブランチではないものを利用しているためです。
Qlot
QlotはCommon Lispのライブラリを、入手先とバージョンを指定した上で固定するためのツールです。導入と利用にはRoswellが必要です。Node.jsのnpm install
のようなものです。
Common Lispのライブラリ定義・管理システムASDF (Another System Definition Facility)でもバージョンを指定することができますが、これと異なるのは、Node.jsのpackage-lock.json
のように入手先やGitのrefを保持した上で、node_modules/
のようにプロジェクトローカルなライブラリ環境を作り出します。これによって、Roswell管理下にあるライブラリ環境とも隔離され、入手先とバージョン (GitのコミットIDなど)を指定して同じライブラリ環境を再構築することも可能です。
依存ライブラリはqlfile
に書きます。これは手書きします。例として、Nikoで指定している依存ライブラリ指定を出しましょう。こんな感じです:
git utopian https://github.com/fukamachi/utopian
git dissect https://github.com/Shinmera/dissect
git mito https://github.com/fukamachi/mito
ql lake :latest
git rove https://github.com/fukamachi/rove
git cl-dbi https://github.com/fukamachi/cl-dbi
ql dexador :latest
ql jonathan :latest
ql local-time :latest
ql cl-date-time-parser :latest
ql string-case :latest
ql
で始まる行はQuicklispというライブラリマネージャを経由してインストールすることを表します。このファイルを定義したときの最新のライブラリを使用しています。git
で始まる行はGitのリポジトリを指定します。こちらはタグやコミットIDを指定することが可能ですが、こちらもその時点のmaster
ブランチを利用しています。
この状態でqlot install
を行うと、ライブラリがローカルにインストールされ、同時に以下のようなqlfile.lock
が生成されます:
("quicklisp" .
(:class qlot.source.ql:source-ql-all
:initargs (:project-name "quicklisp" :%version :latest)
:version "2018-04-30"))
("utopian" .
(:class qlot.source.git:source-git
:initargs (:project-name "utopian" :remote-url "https://github.com/fukamachi/utopian")
:version "git-5f397ccc7593e61314f80b678e40d4fbb45c545c"
:remote-url "https://github.com/fukamachi/utopian"
:ref "5f397ccc7593e61314f80b678e40d4fbb45c545c"))
...
もしqlfile.lock
があった場合はそちらの内容に従ってライブラリが入ります。
Mito
MitoはCommon LispのO/Rマッパーです。Common Lispでのクラス定義からスキーマ定義を行ったり、オブジェクトに対する、あるいはオブジェクトによるRDBMSへのCRUD操作を行うことができます。
Mitoはもちろん便利であるのですが特におもしろい点があり、それは依存しているプロダクトです。Mitoだけではないのですが、Mitoを開発されたfukamachiさんのプロダクトのおもしろいところは、過去のプロダクトのまったくの拡張として上位のライブラリが定義されている点です。言い換えると、コアとなるライブラリの上に必要最小限の層を被せることで新たな抽象化を行っていることです。この点がPaul Grahamがエッセイや書籍等でよく言っている対象領域のための言語をつくり、それを繰り返していくことで目的を達する
というLispらしさをまさに体現していると言えるでしょう。
Mitoは以下のライブラリを利用し、その上でCLOSのメタオブジェクトプロトコルを利用し、素のCommon Lispのクラス定義を拡張しています。
なので、Mitoを通じてクエリを投げるときはSxQLの記法を理解していなければなりません(CL-DBIのほうは見える必要がないので隠されています)。
Nikoではユーザ情報のみをDBに持っているため、次のようにクラスを定義しています:
;; https://github.com/t-sin/niko/blob/d328d5ae8e8416adf6662167f8646d3360807349/models/users.lisp
(defclass users ()
((github-id :col-type :string
:initarg :github-id
:accessor users-github-id)
(github-name :col-type :string
:initarg :github-name
:accessor users-github-name)
(slack-id :col-type :string
:initarg :slack-id
:accessor users-slack-id)
(slack-name :col-type :string
:initarg :slack-name
:accessor users-slack-name))
(:metaclass dao-table-class))
いたってシンプルです。このオブジェクトを、たとえばGitHubユーザ名がわかっている状態でSlackのユーザを引いてくるコードを見てみましょう。
;; https://github.com/t-sin/niko/blob/d328d5ae8e8416adf6662167f8646d3360807349/api/github-webhook.lisp
(defun to-slack-user-id (github-usernames)
(let ((users (mito:select-dao 'users
(sxql:where (:in :github-name github-usernames)))))
(mapcar #'users-slack-id
users)))
ここではmito:select-dao
を用いてusers
テーブルを検索しています。テーブル名の後に検索条件を指定できるのですが、where
の前にsxql
とパッケージ修飾が付いているのがわかります。MitoはSxQLのクエリを検索条件として受け取るため、sxql
パッケージをuse-package
しない場合は、修飾が必要になります。なので、MitoやSxQLのREADMEにある例をそのまま持ってくるときは気をつけましょう。
この点を理解せずに使い小一時間首を捻るというハマり事案が発生したので、共有です。
Dexador
DexadorはHTTPクライアントです。他のサーバにHTTPリクエストを投げたいときなどにとても重宝します。
fukamachi products advent calendar 2016の20日目の記事にて開発経緯が明かされていますが、既存のHTTPクライアントライブラリDrakmaを置き換えるために開発されたそうです。
Drakmaの微妙なところやハマりどころを解消するという目的で開発されているため、さくっと使えてしまいます。以下はCommon LispのREPLでNikoのGitHub用Webhook APIを叩いてみたところです。
CL-USER> (dex:POST "http://niko-no-url/api/github/webhook")
""
200
#<HASH-TABLE :TEST EQUAL :COUNT 8 {10036ECD33}>
#<QURI.URI.HTTP:URI-HTTP http://niko-no-url/api/github/webhook>
NIL
Jonathan
JonathanはJSON文字列のエンコーダ・デコーダです。それも、既存のものより高速なのです。昨今のウェブ開発ではJSON形式でデータをやりとりすることが多いため、とてもお世話になっております。
Jonathanのパッケージのニックネームがjojo
となっており、おそらく奇妙な冒険をするあの人が元ネタなんじゃあないでしょうか?
利用するときは通常次の2つのAPIを知っておけばよいでしょう。JSON文字列をCommon Lispのplistに変換するjojo:parse
と、逆にplistからJSON文字列を得るjojo:to-json
です。以下は文字列からplistを得る例です。
CL-USER> (jojo:parse "{\"key\": \"value\", \"array\": [1, 2, 3, 4]}")
(:|array| (1 2 3 4) :|key| "value")
CL-USER> (jojo:parse "[true, false]")
(T NIL)
plistから文字列を得る例はこんな感じです。
CL-USER> (jojo:to-json (jojo:parse "{\"key\": \"value\", \"array\": [1, 2, 3, 4]}"))
"{\"array\":[1,2,3,4],\"key\":\"value\"}"
ちなみに、nil
はCommon Lispでは空リスト(空のplist)、偽値、Null値など複数の意味がありますが、JSONはそうではありません。たとえばnull
を返したいときにこう書くと期待と異なる値が戻ってきます:
CL-USER> (jojo:to-json '(:|null| nil))
"{\"null\":[]}"
実は、Jonathanではnil
は常にJSONの[]
に対応すると決められています。では空のオブジェクトとかfalse
とかを返すにはどうしたらよいのか。答えはjojo:parse
の説明をちゃんと読むと書いてあります。
CL-USER> (let ((jojo:*null-value* :null)
(jojo:*false-value* :false)
(jojo:*empty-array-value* :[])
(jojo:*empty-object-value* :{}))
(jojo:to-json '(:|null-value| :null
:|false-value| :false
:|empty-array| :[]
:|empty-obj| :{})))
"{\"null-value\":null,\"false-value\":false,\"empty-array\":\"[]\",\"empty-obj\":\"{}\"}"
スペシャル変数として定義された*null-value*
たちに望みの値を束縛してあげることで、好きな値を使うことができます。例だけでなんとなく理解をしているとこういうときに躓くので、READMEはちゃんと読みましょう。
おわりに
GitHub上の通知を補足して通知するプログラムNikoをウェブアプリケーションとして実装するときに利用したライブラリ群を、ハマったポイントと共に紹介してみました。紹介したライブラリを利用するにあたってドキュメントのみならず、ライブラリやその依存ライブラリのソースコードを少し覗く機会も得たので、とても勉強になりました。
なお、Nikoは画面数も少なくAPIとしてのエンドポイントも少ないところにフルスタックフレームワークであるUtopianを利用したので、後々Caveman2やNingleに移行するような気がします。