eLisp : Embedded Lisp
Railsをやったことがある人ならお馴染みのeRuby(もどき)を、common lispで実装してみた。
所要時間およそ90分、行数はコメント込みで70行足らずという簡単なもの。
文法
eRubyの文法は次のように紹介されている(Wikipediaで)。
HTMLファイルの中に<%...%> (もしくは、<%=...%>。こちらは、<%print ...%>の省略形である)の記号で囲った空間があれば、そこをRubyが書かれた部分として認識する。
eRuby - Wikipedia
- 記事編集日時: 2009年4月26日 (日) 01:58
対するeLispの文法。
入力ストリームの中に<%...%>の記号で囲った空間があれば、そこをLispが書かれた部分として認識する。
コード
eLispのコード。特別なことは何もしていないし、ごく短いので説明は省略。
補足を二点。
- !expは、(princ exp)に展開される。そのため、<%!...%>は、eRubyの<%=...%>とほぼ等しい。
- (下で定義する)read関数は入力ストリームを引数にとり、評価結果をオプション引数の出力ストリームに書き出す。文字列を返したりはしない。
;;;;;;;;;;;;;; ;;;; パッケージ (defpackage :embedded-lisp (:use :common-lisp) (:nicknames :elp) (:shadow read) (:export read)) (in-package :embedded-lisp) ;;;;;;;;;;;;;;;;; ;;;; 埋込み用リードテーブル (defvar *elp-readtable* (copy-readtable nil)) ;;;;;;;;; ;;;; 述語 (defun do-eval? (c in) ; <% ... (and (char= #\< c) (char= #\% (cl:peek-char nil in)) (cl:read-char in))) ; 末尾の#\%を読み捨てる (defun elp-string-end? (c in) ; ... <% (do-eval? c in)) (defun elp-end? (in) ; ... %> (and (char= #\% (cl:peek-char t in)) (cl:read-char in) (if (char= #\> (cl:peek-char nil in)) (cl:read-char in) ; 末尾の#\>を読み捨てる (progn (cl:unread-char #\% in) nil)))) ; #\%の後に#\>が続かなかった場合、#\%をストリームに戻す (defun read-elp-string (in) ; %>...elp-string...<% (coerce (loop FOR c = (cl:read-char in) UNTIL (elp-string-end? c in) COLLECT c) 'string)) ;;;;;;;;;;;;;;;;; ;;;; リードテーブル設定 (make-dispatch-macro-character #\% nil *elp-readtable*) (set-dispatch-macro-character #\% #\> ; S式が途中で("%>"により)中断された場合の処理: <% ( ... %>...elp-string...<% ... ) %> (lambda (in subchar arg) (declare (ignore subchar arg)) `(princ ,(read-elp-string in))) *elp-readtable*) (set-macro-character #\! ; !exp => (princ exp) (lambda (stream char) (declare (ignore char)) `(princ ,(cl:read stream t t t))) nil *elp-readtable*) ;;;;;;;;; ;;;; read (defun read-embedded-lisp (in) ; <% s-exp1 s-exp2 ... %> (let ((*readtable* *elp-readtable*)) `(progn ,@(loop UNTIL (elp-end? in) COLLECT (cl:read in))))) (defun read (in &optional (out *standard-output*)) (let ((*standard-output* out)) (handler-case (loop FOR c = (read-char in) DO (if (do-eval? c in) (eval (read-embedded-lisp in)) (write-char c))) (END-OF-FILE ()))))
例
使用例。
まずは、サンプルファイルを作成する(名前はsample.elp)。
<html> <body> <% (dotimes (i 5) %> Hello-<%! (random 100) %> <% ) %> <div> <% ! 1 ; 複数のS式を使った場合 (princ 2) ! 3 %> </div> <% (defun plus(a b) (+ a b)) %> 12+4=<%!(plus 12 4)%> </body> </html>
eLisp実行。
> (with-open-file (in "sample.elp") (elp:read in)) <html> <body> Hello-29 Hello-23 Hello-49 Hello-77 Hello-57 <div> 123 </div> 12+4=16 </body> </html> --> NIL
作り込みは全然足りない。それでも最低限必要な機能は満たしているように思う。
common lispは、こういったものが簡単に実装出来るので良い。