Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
346
306

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LispAdvent Calendar 2017

Day 2

いまから始めるCommon Lisp

Last updated at Posted at 2017-12-01

この記事はLisp Advent Calendar 2017の二日目の記事です。

はじめに

この記事は、Common Lispという初めての人には初めましてな言語の入門記事です。

この世には、Common Lispというとってもカッコいい言語が存在します。その言語はANSIで規格が定められており、宇宙空間で動いたり深海で動いたりし、メタプログラミングが可能で、しかもC言語並に速いという、超クールな言語なのです。

歴史あり、逸話ありのLispであって、実用的と言われるLispです。そんな言語、いますぐに始めてみたいと思いますよね?

しかしググってみると、なんだか処理系っていうの (?) がたくさんあったりしてどれを選んでいいのかわからない。rbenv的なものはないの? パッケージマネージャは? アプリケーションのビルドとかどうしたらいいの? ぱっと実用的なプログラムをどう書いたらいいかわからない。おれたちはふんいきでCommon Lispをやっている……。

そんな疑問を解決するべくこの記事では、2017年12月時点におけるCommon Lispへの入門方法を解説します。

まず、処理系を導入したあと、Common Lispの文法を軽く概観し、その後実際にアプリケーション(lsコマンドとJSON APIサーバ)を作って実用性を確認します。

では、まず導入方法からです。

Common Lisp環境の導入

Common Lispの導入はとっても簡単です。Roswellという、Rubyで言うところのrbenvのような処理系マネージャが存在します。なので、まずはroswellを導入します。

参考: Installation - roswell wiki

macOS

有志の手によりhomebrewに登録されているため、簡単に入れることができます。

$ brew install roswell

Windows

こちらのページから、自分のマシンに合ったものをダウンロードして展開し、パスを通してください。

Linux

linuxbrewが入っているならば、以下のコマンドで入ります:

$ brew install roswell

あるいはソースからビルドしましょう(以下ではUbuntuを想定)。

# 依存パッケージのインストール
$ sudo apt update
$ sudo apt install git build-essential automake libcurl4-openssl-dev
# リポジトリの取得
$ git clone https://github.com/roswell/roswell
# ビルド
$ cd roswell
$ ./bootstrap && ./configure && make
# インストール
# /usr/local/bin/にrosコマンドがインストールされる
$ sudo make install

セットアップ

Roswellはそれ自身、コア以外の部分にCommon Lisp処理系を必要とします。以下のコマンドを叩くことで、Common Lisp処理系を取得し、roswellの設定の初期化やパッケージマネージャ (quicklisp) のインストールが行われ、完全に機能するようになります。

$ ros setup

なお、ふつうにrosコマンドを実行しても、処理系が足りていなければ自動でセットアップを行ってくれます。

(Common Lispの処理系事情解説)

Common Lispのインストール方法を調べると、「〜の処理系は〜に特徴があり…」とでてきて混乱するかもしれません。

Common Lispは、Pythonなどと違い、一つの公式の実装が存在するわけではありません。ANSIで規定された規格があるため、規格で定められた機能を実装したものはすべてCommon Lispを名乗ることができます。そのため、Common Lispといっても、複数の実装がありうるのです。

ただ、規格があるとはいえ、スレッドやOSとのやりとりなど規格で規定されていないものは処理系の独自拡張を利用することになります。(処理系を跨いで可搬に書けるライブラリはもちろんあります。)

著名な処理系として、処理が速いオープンソースの処理系Steel Bank Common Lisp (SBCL)や、macのサポートが手厚いオープンソースの処理系Clozure Common Lisp (CCL)、GNUが保守するGNU CLISP (最近開発があまりアクティブでない気がする)、商用処理系でとても速いと噂のAllegro Common Lispなどがあります。

ここでは、roswellのセットアップで導入されるSBCLを用いて説明していきます。

Common Lispの基本

それではまず、Common Lispのインタプリタ (REPLと呼ばれます) を立ち上げてみましょう。

$ ros run
*

SBCLでは*がプロンプトです (カレントパッケージを表示してほしい……)。ここにプログラムを入力すると、プログラムが読み込まれ、評価され、その結果が印字されます。

Hello Worldを出力するにはformat関数を使います。構文は後の項目で説明するとして、コードはこうです:

* (format t "Hello World!~%")
Hello World!
nil

デバッガからの抜けかた (12/02 21時ごろ追記)

typoしたりしてREPLがデバッガモードに移行したとき、抜けかたがわからないと指摘があったため、抜けかたを追記します。

たとえばHello Worldでformatを打ち間違えてforamt (aとmが逆)と入力してしまったとしましょう。きっとこんなエラーがでます:

* (foramt t "Hello world!~%")
; in: FORAMT T
;     (FORAMT T "Hello world!~%")
;
; caught STYLE-WARNING:
;   undefined function: FORAMT
;
; compilation unit finished
;   Undefined function:
;     FORAMT
;   caught 1 STYLE-WARNING condition

debugger invoked on a UNDEFINED-FUNCTION in thread
#<THREAD "main thread" RUNNING {1001928083}>:
  The function COMMON-LISP-USER::FORAMT is undefined.

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [CONTINUE      ] Retry calling FORAMT.
  1: [USE-VALUE     ] Call specified function.
  2: [RETURN-VALUE  ] Return specified values.
  3: [RETURN-NOTHING] Return zero values.
  4: [ABORT         ] Exit debugger, returning to top level.

("undefined function" T "Hello world!~%")
0]

いきなり大量に怒られるしプロンプトが変わってしまうし、だいぶ焦りますよね。このデバッガはけっこう高機能で、値が間違ってる程度であればその場で値を変更してエラーになった時点からプログラムを続行することもできてしまいます。が、ここではさっさと抜けてしまいます。abortと入力してエンターを押すと、デバッガモードを終了して通常のREPLに戻ることができます。

なので、デバッガが出てしまってもおどろかず飛び退かず、落ち着いてabortと打ってください。


以下、ちょっとだけCommon Lispの構文等の紹介が入りますが、ここでは軽い紹介に留めます。詳しくは最下部の”Common Lispをさらに学ぶには"に記載のウェブサイトや書籍を参照してください。

Common Lispのプログラムの書き方

Common Lispのプログラムの書き方はとっても簡単。覚えることは少ししかありません。

  1. とりあえず開き括弧を書く
  2. まずは関数名を書く
  3. 関数に必要なだけ、引数を書く…
  4. えいやっと閉じ括弧を書く

とっても簡単ですよね。これが関数呼び出しです。3.の引数がまた別の関数呼び出しだったりするときは、括弧がネストされていく感じです。

上で見たHello Worldの関数をもう一度見てみましょう:

(format t "Hello World!~%")

開き括弧があり、formatが関数名で、t"Hello World!~%"が引数で、最後に閉じ括弧というわけです。format関数はC言語でいうprintfのように書式化文字列を出力する関数で、出力先ストリーム(例ではt)と書式文字列 (例では"Hello World!~%") を引数にとります (お察しの通り、~%は改行です)。

これからCommon Lispのデータ型の話をします。

そこでリストというデータ型の話をしますが、関数呼び出しのコードがリストとも見做せるという点にはちょっとだけ意識を留めておいてください。この「プログラムがデータとして表現される」ことは、Lispにとってとても大事なことなのです。

データ構造

アトム

アトムとは、リストではないデータのことです。たとえば、文字列や数値です。ほかにもCommon Lispにはシンボルという変わったデータ型が存在します。

例を見ましょう:

;;; 数値
* 42
42
;;; 文字列
* "hello"
"hello"

これらはREPLに打ち込むと、評価された結果として自分自身が印字されます。この点ではシンボルも同じように、自分自身に評価されるデータ型です。

;;; シンボル
* 'symbol

ちょっと待って。シンボルに'がついていますが、これは「変数名ではなくシンボルだよ」とREPLに伝えるための記法です。また、Common Lispではシンボルを読み込む際、勝手に大文字に変換されます (Common Lispに慣れていても、たまにイラッときます…)。

シンボルとは何か。Lisp初心者に聞かれると説明に困る質問ですね。ざっくり言うとシンボルは名前です。そして、変数として用いるときには変数の格納場所の名前になります。ちょっとむずかしいですね。

例を出すと、Hello Worldのコードの先頭にあるformatというのがシンボルです。あのコードでは、「リスト(後ほど説明します)の先頭にあるシンボルはオペレータ名である」というCommon Lispの規則によって、formatというシンボルに結び付く関数そのものが取り出されて実行されていたわけです(「オペレータ名」と言ったのは、関数でない場合もあるためです)。

また、変数の格納場所の名前として、以下のようにして代入できます:

;;; ちなみにここでたくさん警告がでますが無視してください
;;; グローバルな変数を作ったよ、と処理系が警告してくれているのです
* (setf hoge 42)
42
* hoge
42

このときhogeには'を付けていません。これはsetfの場合は、setfが**hogeを特別扱いしてくれる**からです。じつはsetfは関数ではありません(マクロといいます)。一般には、「リストの先頭以外に現れたシンボルは変数である」という規則によって、シンボルが保持する値(シンボルが値を保持している状態をシンボルが値に束縛されているといいます)が取り出されます。REPLにhogeとだけ打ち込んでいるのがまさにそれです。

つぎはシンボルの中でも特殊なシンボルを見てみます。

;;; キーワード
* :keyword
:keyword
;;; 空リスト兼、偽値
* nil
nil
;;; 真値
* t
t
SYMBOL 

:で始まるシンボル、キーワードは特殊なシンボルです。通常のシンボルはパッケージ(後述)の異なるシンボルを別のシンボルであると区別します(CL-USER::HOGEMY-PACKAGE::HOGEは異なる)。しかし:で始まるシンボルは常にKEYWORDパッケージのシンボルとなります。なので名前が同じであれば等しいのです。その性質から、パッケージ名や関数のキーワード引数を表現するのにしばしば使われます。

nilはアトムでもありリストでもあるという、変わった値です。アトムとしてみるとき、偽値として使われます。リストとしてみるときは、空リストを表現する値です。

一方でtは、真値を表す値です。

アトムについてはひととおり説明しました。次はLispの主なデータ構造、リストについてです。

リスト

リストとは、括弧で囲まれたデータを連ねた形式のデータです。その実体はコンスセルというデータ型を用いた一方向線形リストですが、コンスセルについては詳しく説明しません。

ここでは、リストの見た目がLispの関数呼び出し等にクリソツであるという点に留意してください。

;;; 数値のリスト
* '(1 2 3)
(1 2 3)
;;; シンボルのリスト
* '(a b c)
(A B C)

リストにも'を付けます。これはREPLに「関数呼びだしではなく(データとしての)リストだよ」と伝えるためのものです。これがないと、以下の二つを区別することができなくなってしまうためです:

;;; シンボルと文字列の混在リスト (リストとして受け取られる)
* '(format t "hello~%")
(FORMAT T "hello~%")
;;; 関数呼び出し (プログラムとして受け取られる)
* (format t "hello~%")
hello

Common Lispの持つ他のデータ型には、ハッシュテーブル、配列、パス名(pathname)などありますが、使うときに適宜解説します。

つぎは、関数の定義方法を紹介します。

関数定義

関数を定義するにはdefunを使います。再三言いますが、コードがリストになっていてオペレータ名 + 引数...の形式になっていることに注意しましょう。

* (defun hello (name)
    (format t "hello, ~a!~%" name))
HELLO
* (hello "wintermute")
hello, wintermute!

引数は通常の引数以外にも、キーワード引数や可変長引数、オプション引数などといったものがありますが、ここでは解説しません。

つぎは、条件分岐が繰り返しのやり方を見てみます。

フロー制御

Common Lispの条件分岐や繰り返しのオペレータはいくつかあります。ここではFizz Buzzを実装しつつ紹介していきます。何度でも言いますが、コードがリストになっていて…(省略

まずif。Common Lispでは偽がnilでそれ以外の値が真です。

* (setf num 42)
42
* (if (> num 40)
    'over-forty   ; 真のとき
    'under-forty) ; 偽のとき
OVER-FORTY

Common Lispのifは文ではなく式なので、実行後に真偽それぞれの場合実行される式の値が返ってきます。

偽のときはなにもしない場合、whenというマクロが使えます。

* (when (> num 40)
    'over-forty)  ; 真のとき
OVER-FORTY

ループは計数繰り返しができるdotimesやリスト上の繰り返しができるdolistがあります。ちなみにformat関数の書式指定子~aは「後ろのパラメータを一つ表示する」という意味です。

* (dotimes (n 5)
    (format t "~a, " n))
0, 1, 2, 3, 4, 
* (dolist (e '(0 1 2 3 4))
    (format t "~a, " e))
0, 1, 2, 3, 4, 

これでFizz Buzzを実装する準備が整いました。Common LispにおけるFizz Buzzはこうです:

* (defun fizzbuzz (n)
    (if (or (zerop (mod n 3))
            (zerop (mod n 5)))
      (progn
        (when (and (zerop (mod n 3)))
          (format t "fizz"))
        (when (and (zerop (mod n 5)))
          (format t "buzz"))
        (format t " "))
      (format t "~a " n)))
FIZZBUZZ
* (dotimes (n 20)
    (fizzbuzz (1+ n)))
1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz 

わーお。なっがーい。
あんまりカッコよくないやり方な気がしますが、カッコよくするのは読者への課題とする。

ちなみにさらっと使いましたがprognというのはC言語でいうとif {...}{...}に相当するオペレータです。複数の式を実行するときに、特に副作用を目的として使います(実際にformatで副作用している)。

つぎはメタプログラミング! Lispのマクロについて説明します。

マクロ

Lispの力の真髄といえばメタプログラミングを可能にする、マクロです。ちなみにC言語のマクロとはまったく別物なので、そちらを想起している方は忘れてください。ぜんぜん違います。

C言語のマクロは文字列処理です。他方、Common Lispのマクロは構文木変換です。

ここまでの説明で「defunとかifとかsetfがリストに見える」ことを散々強調してきました。ここまででリストに対するオペレータをあまり説明しませんでしたが、プログラムでリストを処理できます。プログラムがリストであるなら、プログラムでプログラムを処理することができるはずです。そして、マクロがまさにそれを実現する機能なのです。

ここではwhenを自分で実装してみましょう。my-whenの使い方(仕様)は以下です:

(my-when 条件式
  真のとき実行される式...)

これは機能的には、以下のifと等価です:

(if 条件式
    (progn
      真のとき実行される式...)
    nil) ; なにもしない

whenとして条件式真のとき実行される式が与えられれば、ifのほうの式を機械的に組み立てられますね。そんなの機械にやってほしいですね。やらせましょう!

(defmacro my-when (cond &body body)
  `(if ,cond
       (progn ,@body)
       nil))

はい、機械にmy-whenの展開を教えました。ここで&body bodyという引数がでてきますが、これは可変長引数で、&bodyの後のシンボルに以降の引数が入ります。

ifの式の前についているクォートはシングルクォート'ではありません。バッククォート`です。これはリストのテンプレートに値を埋め込むことのできるクォートです。値の埋め込みは,で示します。つまり、,condは条件式(アトムかリストがくる)をそのままifの条件部分に埋め込みます。,@bodyは可変長引数として渡ってくるリストの中身を、そこに埋め込みます。

バッククォートは慣れないと分かりづらいので、デバッグ例を出して説明します。

マクロのデバッグにはmacroexpand-1が使えます。式を受け取り、マクロ呼び出しだったらそのマクロを展開した式を返す関数です。こいつにmy-when呼び出しの式を渡してやると…。

* (macroexpand-1 '(my-when (> num 40)
                    (format t "40!~%")
                    'over-forty))
(IF (> NUM 40)
    (PROGN (FORMAT T "40!~%") 'OVER-FORTY)
    NIL)
T

prognの中が可変長引数((format t "40!~%") 'over-forty)の外側の括弧を外したものになっていますね。これが,@bodyの埋め込み方法です。

ここまでCommon Lispの構文をざっと説明しました。つぎはちょっと脇道に逸れて、Common Lispの開発環境のお話です。

SLIMEあるいはslimv.vimのすすめ

Common Lispで開発するときに、エディタの支援は欠かせません。コードフォーマッタや名前補完の面でも利点がありますが、それよりも、処理系との連携がとても容易になるためです。そして、この力はとても強烈です。

ここで、EmacsにおけるSLIME、Vimにおけるslimv.vimを使うとどのようなことが実現するか、例を出しましょう。

以下のような関数書いたファイルをエディタで開き、同時にSLIMEを立ち上げます:

cl-slime1.png

エディタのバッファでhello関数を評価(SLIMEではカーソルを末尾に置いてC-x C-e)してやると、ファイルに書かれた関数定義が即、下のREPLのプロセスに送られます。

これを、まさにプログラムの実行中に行うことができます! ホットデプロイです!

out-1.optimized.gif

この画像ではREPLで、無限ループの中でhello関数の実行と2秒のスリープをひたすら実行しています。その実行中に上の関数定義を変更し(hellohelloooo)、その定義をREPLに送りました(WARNINGが出ている箇所)。

その結果、プログラムの実行中にも関わらずhello関数の出力が変わっています!

Deep Space 1の宇宙機に載ったプログラムの地球-木星間バグフィックスは、まさにこの機能を使って行われたそうです。カッコいい!

この恩恵に与るために、EmacsではSLIME、vimではslimv.vimの利用を強くお勧めします。

2018-01-28更新

今現在の各エディタにおける導入方法はこちらの記事が詳しいです: 「CommonLispの環境構築まとめ」

Common Lispにおけるモジュールとライブラリ

モジュール

Common Lispにおいてモジュールに相当するのは「パッケージ」です。SLIMEやSlimvでREPLを触っているとわかるのですが、REPLを立ち上げたときにはパッケージがCL-USERに設定されています。この中にはCommon Lispの標準関数が含まれているため、標準関数を利用できるというわけです。

パッケージは作ることもでき、カレントパッケージを切り替えることも可能です。これ以降はプロンプトをSLIMEの表示に則りカレントパッケージを表示したものにして説明していきます。

まず、パッケージを作るにはdefpackageを使います。

CL-USER> (defpackage hoge-package
           (:use :cl))
#<PACKAGE "HOGE-PACKAGE">

このとき(:use :cl)を忘れないようにしてください。これは「Common Lispの標準関数が入ったパッケージCLの関数をhoge-packageに全てインポートする」という意味です。これを忘れると、このパッケージ内ではCommon Lispの標準関数を使えなくなり、パッケージ切り替えすらできなくなります。

パッケージの切り替えはin-packageを使います。

CL-USER> (in-package :hoge-package)
#<PACKAGE "HOGE-PACKAGE">
HOGE-PACKAGE> 

ライブラリ

Common Lispの仕様には、プログラムをひとまとまりにしたライブラリを構成する方法が定められていません。そこでASDF (Another System Definition Facillity)というプログラムがライブラリ(システムと呼びます)の定義、ロードやコンパイルの仕方の制御を行ってくれます。ASDFは処理系に始めからバンドルされているものを使えるほか、roswellが最新のASDFを導入してくれたりします。

だいたい以下のようなディレクトリ構造が、Common LispのASDFを利用したライブラリでは一般的です。

hoge/
+ hoge.asd       ; システムhogeの定義(バージョン等情報、依存ライブラリ、ファイル定義)
+ hoge-test.asd  ; システムhogeのテスト用システムの定義
+ src/
  + hoge.lisp    ; システムhogeのプログラム
+ t/
  + hoge.lisp    ; hoge.lispのテストコード

システムの定義の流儀には、古いものは2000年ごろのコードで使われているものから、ここ二年ほどで提案されたものなど、いくつかがありますが、この記事では最近提案されたpackage-inferred-systemの流儀で説明します。

システム定義にはどのようなことを書けばいいのか(hoge.asdの中身)については、実際にプログラムを作成するときに解説します。

パッケージマネージャ

ライブラリのインストールについても、規格で規定はされていませんが、有志の手でPythonのpipやnode.jsのnpmのようなものが作られています。Quicklispです。じつは、roswellが処理系をインストールした際に一緒に導入してくれています。

基本的にはREPL上で操作します。たとえばCommon LispのPerl互換正規表現ライブラリcl-ppcreを検索して、ロードしてみます:

;;; 名前で検索
CL-USER> (ql:system-apropos "ppcre")
#<SYSTEM arnesi/cl-ppcre-extras / arnesi-20170403-git / quicklisp 2017-10-23>
#<SYSTEM cl-ppcre / cl-ppcre-2.0.11 / quicklisp 2017-10-23>
#<SYSTEM cl-ppcre-template / cl-unification-20170630-git / quicklisp 2017-10-23>
#<SYSTEM cl-ppcre-test / cl-ppcre-2.0.11 / quicklisp 2017-10-23>
#<SYSTEM cl-ppcre-unicode / cl-ppcre-2.0.11 / quicklisp 2017-10-23>
#<SYSTEM cl-ppcre-unicode-test / cl-ppcre-2.0.11 / quicklisp 2017-10-23>
#<SYSTEM optima.ppcre / optima-20150709-git / quicklisp 2017-10-23>
#<SYSTEM parser-combinators-cl-ppcre / cl-parser-combinators-20131111-git / quicklisp 2017-10-23>
#<SYSTEM trivia.ppcre / trivia-20170830-git / quicklisp 2017-10-23>
#<SYSTEM trivia.ppcre.test / trivia-20170830-git / quicklisp 2017-10-23>

;;; ロード
CL-USER> (ql:quickload :cl-ppcre)
To load "cl-ppcre":
  Load 1 ASDF system:
    cl-ppcre
; Loading "cl-ppcre"
.
(:CL-PPCRE)

;;; 利用
CL-USER> (cl-ppcre:regex-replace-all "い" "おじいさんのとけい" "イェー!")
"おじイェー!さんのとけイェー!"
T

(Quicklispの問題点とその解決)

Quicklispには二つの問題点があります。Quicklispのライブラリリポジトリは月に一回しか更新されないこと、ライブラリバージョンを指定できないことです。そのため、環境によってquicklispでの導入タイミングが異なると、ライブラリのバージョンに差が発生し、プログラムが動かないといった事態が発生します。

また、roswellのquicklispでインストールされたライブラリは~/.roswell/lisp/quicklispに置かれ、すべてのアプリケーションがここを見ます。アプリケーション毎に依存ライブラリのバージョンを変えたい場合、これは不便です。

ここでは、それらの問題点を解決するライブラリqlotがあることを紹介しておくに留めます。Qlotでは、プロジェクトローカルにquicklispを導入することで、上記の問題を解決しています(作者によるqlot説明記事)。

Common Lispを実用する

Common Lispは実用的なことができる言語であることを説明するために、実際に実用的なプログラムを作ってみます。ここでは二つの実用的なプログラムを作ります:

  1. catコマンド - コマンドラインアプリケーション
  2. JSON APIサーバ - Webアプリケーション

実用1: catコマンドを作る

実用的なアプリケーションその1は、catコマンドです。ここでは簡単に、以下の仕様で作ります。

  • catという名前の実行可能ファイル
  • cat FILEでテキストファイルの中身を標準出力に出力
  • catとだけ打ったときは入力を標準入力とする

なお、解説が目的のためテストは書きませんが(長くなるので)、ちゃんとしたものを作るときはテストを書きましょう。

プロジェクトを用意する

まずはcl-catプロジェクト(trivialな機能のライブラリはcl-*という命名が多い)を構成するファイルをtouchコマンド等で作成します。場所は~/.roswell/local-projects/の中にしてください。ここがroswellにおけるPATH的な場所になります。

ただしcat.roscl-cat/roswellディレクトリに移動してからros init catで作成してください。そうすることでroswellスクリプトのテンプレートからファイルが作られます。

cl-cat/
+ roswell/     ; roswellスクリプト用のディレクトリ
  + cat.ros     ; catコマンドのエントリポイントを持つroswellスクリプト
+ cl-cat.asd    ; ASDFのシステム定義ファイル
+ main.lisp    ; コア機能の入ったソースファイル

ASDFのシステムを定義する

まずはASDFのシステム定義を書きます。cl-cat.asdの中身はこのような感じです:

;;; cl-cat.asd
(in-package :cl-user)    ; どのパッケージにいるかわからないのでCL-USERパッケージにする
(defpackage :cl-cat-asd  ; ASDFのシステム定義用のパッケージをつくる
  (:use :cl :asdf))      ; 標準関数とASDFの関数をパッケージ修飾なしで呼べるようにする
(in-package :cl-cat-asd) ; 作ったパッケージにする

(defsystem :cl-cat
  :class :package-inferred-system   ; システム定義のスタイルをpackage-inferred-systemにする
  :description "cat command implemented with Common Lisp"
  :version "0.1"
  :author "t-sin"
  :license "Public Domain"
  :depends-on ("cl-cat/main"))  ; ソースファイルを相対パスで指定する

ほぼおまじないにはなるのですが、コメントで説明を付けました。システムをpackage-inferred-systemスタイルで定義するよう指定しています。Common Lispはファイルとパッケージが結び付いていなくていい言語ですが、近年「それだとわかりにくいよね」といったような動機でone-package-one-fileが提案されました。Common Lisp界では今後このスタイルがナウでヤングでモダンになりそうです。従来式だと、プロジェクトのソースファイル間の依存関係を手書き(!)していたのですが、このスタイルだと自動で推測してくれるので、利点もあります。

main.lispにもパッケージを定義しておきましょう。なおパッケージ名とシステム名が同じで混乱しそうですが、別のレイヤーのものなので問題はありません:

;;; main.lisp
(in-package :cl-user)
(defpackage :cl-cat
  (:use cl)
  (:export :cat))  ; 今回作成する関数(まだない)をパッケージ外部に公開
(in-package :cl-cat)

この状態で一度プロジェクトをREPLでロードしてみましょう:

CL-USER> (ql:quickload :cl-cat)
To load "cl-cat":
  Load 1 ASDF system:
    cl-cat
; Loading "cl-cat"
[package cl-cat/main]
(:CL-CAT)

ここまでは成功しました! これからmain.lispにcatコマンドの中核である入力を読んで出力を吐く動作を書きます。それはこんな関数です:

(defun cat (input-stream output-stream)
  (loop
     for line = (read-line input-stream nil :eof)
     until (eq line :eof)
     do (write-line line output-stream)))

Common Lispにはストリームがあります。標準入出力だったりファイルを開いたりすると得られるものです。そのストリームを二つ受け取って、入力ストリームの内容を出力ストリームに順次流し込んでやる、というのが上記のコードです。ちなみに:eofとあるのは、通常read-line関数はEOFに達した状態で呼ぶとエラーを投げますが、その代わりに:eofというキーワードを返すよう指定しているものです。

また、コマンド部分を以下のように書きます:

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp (ql:quickload '(:cl-cat) :silent t)  ; cl-catをロード
  )

(defpackage :ros.script.ls.3720612639
  (:use :cl))
(in-package :ros.script.ls.3720612639)

(defun main (&rest argv)  ; roswellスクリプトのエントリポイント
  (declare (ignorable argv))
  (if (>= (length argv) 1)   ; 引数があるとき
      (with-open-file (in (first argv))  ; 一つめの引数をファイル名として入力ストリームを開き
        ; 入力を開いたファイル、出力を標準出力ストリームにしてcatする
        (cl-cat:cat in *standard-output*))
      ; 引数がないので、標準入力と標準出力を指定してcatする
      (cl-cat:cat *standard-input* *standard-output*))) ; 
;;; vim: set ft=lisp lisp:

長いですがほぼテンプレートなのでmain関数に注目しましょう。

コマンドライン引数は、処理系毎にアクセス方法に差があり非常に煩雑な部分なのですが、Roswellが抽象化してargvとしてリストで渡してくれます。これにより、roswell登場以前には難しかったCommon Lispによるスクリプティングが可能になったのです!

上のようにライブラリのロードとmain関数だけ実装すれば、実行可能なスクリプトができあがります。では、試しに実行してみましょう:

$ ./cat.ros ../cl-cat.asd
# 表示されるまでちょっと間がある…
(in-package :cl-user)
(defpackage :cl-cat-asd
  (:use :cl :asdf))
...

はい、できました! でも、起動が遅いですよね。このスクリプト、roswellが起動し、処理系を起動し、ライブラリを読み込み(ライブラリソースを読み込むし、必要ならネットからダウンロードする!)…とオーバーヘッドが大きいんです。なので、これらを行ったあとの状態のメモリイメージをそのまま実行可能バイナリにしてしまい、実行速度を速くしてしまいましょう。こうやります:

$ ros build cat.ros
$ ls
cat  cat.ros
$ ./cat ../cl-cat.asd
(in-package :cl-user)
(defpackage :cl-cat-asd
  (:use :cl :asdf))
...

一瞬で起動しました。やったぜ。すごいぜ。ちなみに実行可能バイナリのファイルサイズは見ないほうが身のためです。

というわけで、コマンドラインアプリケーションをCommon Lispで書くのはすごく簡単です。では、二つめの実例です。

実用2: JSON APIを作る

つぎの実用的な例として、簡単なWebアプリケーションを作ります。ここで作るのはデータをJSONで返すAPIです。以下のような仕様で作ります:

  • /ではHello API!と返す
  • /post/listでは記事のリストをJSON形式で返す
    • ID、タイトル、投稿日時、タグを返す
  • /post/id/xxxxではIDxxxxの記事の情報をJSON形式で返す
  • 記事の情報は変数にリストで持つ (簡略化のため)

ここでは、Common LispのマイクロWebフレームワークであるningleを使って開発します。ningleはプロジェクトスケルトンを必要としない、とても小さなフレームワークなので、ここではその流儀に従って、一つのファイルで開発していきます。

まずはじめに、/に対してHello API!を返すコードを書いてみます。ファイル名はapp.lispとしましょう。こんな感じです:

;;;; app.lisp
;;; ningleをロード (ロード状況を出力しない)
(ql:quickload '(:ningle) :silent t)

;;; ningleのアプリケーションクラスをインスタンス化する
(defparameter *app* (make-instance 'ningle:<app>))

;;; アプリケーションクラスの`/`の戻り値を設定
(setf (ningle:route *app* "/")
      "Hello API!")

;;; clackupするためにアプリケーションインスタンスが返るようにする (後述)
*app*

Common Lispってオブジェクト指向言語でもあるんです。知ってました?

さて、アプリケーションを実行してみる前にひとつ説明を。ningleはサーバのインターフェースを抽象化してくれるCommon Lispのライブラリclackの上に作られています。このライブラリがclackupというアプリケーションを実行するコマンドを提供してくれていて、こいつに最後がclackアプケーションを返すコードで終わるCommon Lispのソースファイルを渡してやるとアプリケーションが実行されるのです。上記のコードの最後が*app*をただ返すだけになっているのは、そのような理由があるからなんです。

では実行してみましょう。

$ clackup app.lisp
Hunchentoot server is going to start.
Listening on localhost:5000.

これでアプリケーションが実行されている状態です。別のターミナルからcurlで叩いてみると

$ curl localhost:5000
Hello API!$

こうなります。ちなみにアプリケーションの実行をREPLの上でやることもできます。REPLの上で実行すると、ソースファイル上の変更を実行中のプロセスに即時送ることができるので、こっちのほうが便利です。

CL-USER> (defparameter app-handler (clack:clackup *app*))
Hunchentoot server is started.
Listening on localhost:5000.
APP-HANDLER
CL-USER>

app-handlerclackupの結果を受け取っているのは、アプリケーションを停止させたいときのためです。

では、まずデータを用意しておきましょう。ここではハッシュテーブル的なデータ構造としてkeyとvalueをリスト中に交互に並べる属性リスト (property list, plist) を使います。

(defparameter *db*
  '((:id 1 :title "記事1" :date "2017-01-01" :tags ("tag1" "tag2" "tag3")
     :body "本文1本文1本文1本文1")
    (:id 2 :title "記事2" :date "2017-01-02" :tags ("tag2" "tag3")
     :body "本文2本文2本文2本文2")
    (:id 3 :title "記事3" :date "2017-01-03" :tags ("tag1" "tag3")
     :body "本文3本文3本文3本文3")))

defparameterは初出ですね。これは、スペシャル変数(ダイナミック変数ともいう)をという、グローバルで上書き可能な変数を定義するためのものです。スペシャル変数は名前を*で囲む、というのがCommon Lispの慣例的な命名規則です。

*db*からデータを取り出すのにmapcarを、取り出したplistをJSON形式にするのにjonathan (Common LispのJSONライブラリ)を使います。まず、冒頭のql:quickloadは以下のように変更します:

(ql:quickload '(:jonathan  ; 追加した
                :ningle)
              :silent t)

次に、レコードを/post/listに含めるデータ(ID、タイトル、投稿日時、タグ)に変換する関数を書きましょう。

(defun to-list (record)
  (list :id (getf record :id)
        :title (getf record :title)
        :date (getf record :date)
        :tags (getf record :tags)))

そして最後に、/post/listにルーティングします

(setf (ningle:route *app* "/post/list")
      (jonathan:to-json (mapcar #'to-list *db*)))

この状態でファイルをclackupしてみてください。REPLで書いている人は、アプリケーションを終了させず、ただ定義をREPLに送るだけです。

$ curl localhost:5000/post/list
[{"ID":1,"TITLE":"記事1","DATE":"2017-01-01","TAGS":["tag1","tag2","tag3"]},{"ID":2,"TITLE":"記事2","DATE":"2017-01-02","TAGS":["tag2","tag3"]},{"ID":3,"TITLE":"記事3","DATE":"2017-01-03","TAGS":["tag1","tag3"]}]

つぎに、IDを与えられたポストの情報を返すAPIを実装します。コードはこんな感じです:

(setf (ningle:route *app* "/post/id/:id")
      #'(lambda (params)
          (let ((record (find (parse-integer (cdr (assoc :id params))) *db*
                              :key #'(lambda (r) (getf r :id)))))
            (if (null record)
                "{}"
                (jonathan:to-json record)))))

いろいろ出てきました。

lambdaは無名関数ですが、ningleではURLに関数をルーティングすることができ、その場合URLから取り出されたパラメータがparamsに連想リスト(association list, alist)として渡されます。基本方針としては、URLで渡ってきたIDを取り出して((cdr (assoc :id params)))、整数に変換(parse-integer)したもので、*db*:idの値を検索(find)します。

たとえばID 2に対してAPIを呼んでみると…

$ curl localhost:5000/post/id/2
{"ID":2,"TITLE":"記事2","DATE":"2017-01-02","TAGS":["tag2","tag3"],"BODY":"本文2本文2本文2本文2"}

とまあ、こんな感じです。

おわりに

Common Lispで実アプリケーションを書くところまで持っていってみました。いかがでしょうか。「LISPは人工知能を書くのに使われた、黴の生えた古臭い言語だ」という都市伝説と異なり、実用的なアプリケーションを書くことのできる言語であることがおわかりいただけたと思います。

Common Lispは、まだまだ死んでいません。そのエコシステムは発展を続けており、現代的な構成でアプリケーションを構築することも可能です。

Common Lispをさらに学ぶには

もしここまで読んで、Common Lispをさらに学習したくなった場合、以下のサイトや書籍が参考になりますよ。

では、Happy Hacking!

書籍

ウェブサイト

346
306
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
346
306

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?