読者です 読者をやめる 読者になる 読者になる

eLisp : Embedded Lisp

common lisp read macro

Railsをやったことがある人ならお馴染みのeRuby(もどき)を、common lispで実装してみた。
所要時間およそ90分、行数はコメント込みで70行足らずという簡単なもの。

文法

eRubyの文法は次のように紹介されている(Wikipediaで)

HTMLファイルの中に<%...%> (もしくは、<%=...%>。こちらは、<%print ...%>の省略形である)の記号で囲った空間があれば、そこをRubyが書かれた部分として認識する。
- 記事編集日時: 2009年4月26日 (日) 01:58

eRuby - Wikipedia

対する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は、こういったものが簡単に実装出来るので良い。

蛇足: 2010/01/22

まとめ的なもの。

eLispは入力文字列(ストリーム)を次のように処理する。

  • 普通の文字なら、そのまま出力する
  • 文字列<%...%>を検出したら、...の部分をlispの式として読み込み、評価する
    • 読み込み時には、!および%>...<%が特別なシンタックスとして解釈される
      • !expは、(princ exp)シンタックスシュガー
      • %>...<%は、(princ "...")シンタックスシュガー ※ ネストした<%は、lisp式(読み込み/評価)の開始ではなく、別の形式で表記された文字列の終端記号として解釈される