[创建时间]:<2018-05-29 Tue 07:50:42 UTC+08:00>
[更新时间]:<2018-05-29 Tue 13:50:44 UTC+08:00>
总结使用Common Lisp包ParenScript的实践过程。
Table of Contents
- 1. 快速上手
- 2. 打造一个属于ParenScript的REPL环境
- 2.1. 用浏览器来求值ParenScript程序
- 2.2. 用NodeJS来求值ParenScript程序
- 3. 命名空间与长长的前缀名
- 4. 令人又爱又恨的随机名称
- 5. 示例:一个完整的ParenScript程序
- 5.1. 使用说明
- 5.2. 系统:day-or-evening
- 5.2.1. day-or-evening.asd
- 5.2.2. package.lisp
- 5.2.3. utility.lisp
- 5.2.4. html.lisp
- 5.2.5. css.lisp
- 5.2.6. js.lisp
- 5.2.7. build.lisp
快速上手
使用Quicklisp提前加载程序,再编写ParenScript语句,用于根据当前的时间来判断是白天还是黑夜:
(ql:quickload :parenscript)
(in-package :parenscript)
(ps (if (>= (new (chain (-date) (get-hours)))
20)
"evening"
"day"))
ParenScript语句的目的是生成下列JavaScript语句:
if (new Date().getHours() >= 20) {
'evening';
} else {
'day';
};
复制到浏览器的终端、NodeJS等任意能执行JavaScript语句的程序里执行,将得到:
'day'
打造一个属于ParenScript的REPL环境
先展开ParenScript程序为JavaScript程序,再执行JavaScript程序。
用浏览器来求值ParenScript程序
需要提前准备好:
- SBCL
- ParenScript
- Emacs
- Trident[1]
- SLIME
- 浏览器
过程:
编写纯粹的ParenScript代码
(if (>= (new (chain (-date) (get-hours))) 20) "evening" "day")
与浏览器建立联系
M-x run-skewer
像SLIME一样求值上述ParenScript代码
M-x trident-eval-last-expression
"day"
用NodeJS来求值ParenScript程序
需要提前准备好:
- SBCL
- ParenScript
- Emacs
- Trident
- SLIME
- NodeJS
过程:
编写纯粹的ParenScript代码
(if (>= (new (chain (-date) (get-hours))) 20) "evening" "day")
与NodeJS建立联系
(defun trident-nodejs-start () (interactive) (unless (js-comint-get-process) (js-comint-start-or-switch-to-repl))) (defun trident-nodejs--eval (string) (unless (js-comint-get-process) (error "Please use `M-x trident-nodejs-start' to start NodeJS first.")) (trident-with-expansion (code string) (js-comint-send-string code)) (js-comint-start-or-switch-to-repl)) (defun trident-nodejs-eval-last-expression () "参考`trident-eval-last-expression'" (interactive) (trident-nodejs--eval (slime-last-expression))) (defun trident-nodejs-eval-defun () (interactive) (trident-nodejs--eval (slime-defun-at-point))) (defun trident-nodejs-eval-region (beg end) (interactive "r") (trident-nodejs--eval (buffer-substring-no-properties beg end))) (defun trident-nodejs-eval-buffer () (interactive) (trident-nodejs-eval-region (point-min) (point-max)))
像SLIME一样求值上述ParenScript代码
M-x trident-nodejs-eval-last-expression
'day'
命名空间与长长的前缀名
定义Common Lisp包的命名空间是"day-or-evening":
(ql:quickload :parenscript)
(in-package :common-lisp)
(defpackage #:day-or-evening
(:use #:common-lisp))
(setf (parenscript:ps-package-prefix :day-or-evening)
"DOE$")
展开后JavaScript包的前缀名是"DOE$"。
譬如下列ParenScript代码:
(in-package #:common-lisp)
(defun day-or-evening::day-or-evening ()
(if (>= (parenscript:new
(parenscript:chain (-date)
(get-hours)))
20)
"evening"
"day"))
或:
(in-package #:day-or-evening)
(defun day-or-evening ()
(if (>= (parenscript:new
(parenscript:chain (#:-date)
(#:get-hours)))
20)
"evening"
"day"))
将展开成:
function DOE$dayOrEvening() {
return new Date().getHours() >= 20 ? 'evening' : 'day';
};
在上述ParenScript代码里,要么在"common-lisp"命名空间里,要么在"day-or-evening"命名空间里定义函数。
在两个不同的命名空间里定义函数时,使用ParenScript函数、JavaScript函数时都需要带上长长的前缀,这不免麻烦。
我采用的做法是分而治之。
只要是JavaScript函数就在"common-lisp"的命名空间里提前定义,带上长长的"day-or-evening"前缀,此为筑基。
譬如,提前定义喜闻乐见的JavaScript方法"getElementById":
(in-package #:common-lisp)
(defun day-or-evening::get-element-by-id (id)
"getElementById"
(document.get-element-by-id id))
展开式为:
function DOE$getElementById(id) {
return document.getElementById(id);
};
在这个基础上,在"day-or-evening"的命令空间里,随意地使用JavaScript函数,就像这些函数是Common Lisp的内部函数一样,此时前缀可免,补全可用,文档可用,JavaScript与Common Lisp合二为一。
譬如,上述定义的"get-element-by-id"函数:
(in-package #:day-or-evening)
(get-element-by-id "element-id")
展开式为:
DOE$getElementById('element-id');
当这些JavaScript函数过于频繁地在多个程序里出现时,可以分离出所有的提前定义过程,创建一个新的包,姑且唤作"lscript",只要是ParenScript程序:
- 就像使用其它Common Lisp包一样,使用"lscript"
- 就像使用外部JavaScript文件一样,使用"lscript"所展开得到的JavaScript文件
令人又爱又恨的随机名称
(let ((list '()))
(dolist (i '(1 2))
(push i list)))
展开式为:
(function () {
var list = [];
for (var i = null, _js_arrvar19 = [1, 2], _js_idx18 = 0; _js_idx18 < _js_arrvar19.length; _js_idx18 += 1) {
i = _js_arrvar19[_js_idx18];
push(i, list);
};
})();
上述"_js_"前缀的随机名称,本来是为了消除名称的冲突,但无疑增加了版本管理的因难,不能直接去除,又非用不可,着实麻烦。
姑且有两步来缓解无用的版本更新问题。
可以在生成JavaScript文件之前,检测git仓库里的ParenScript文件是否发生了变化,在没有任何变化时,会得到"nothing to commit, working tree clean",否则,将ParenScript文件转换为JavaScript文件。
也可以使用预先定义的函数来替换能产生随机名称的函数,让"_js_"前缀的随机名称只出现在一个位置。
譬如对于上述"dolist"过程,创建"cl-mapc"函数:
(defun cl-mapc (f-function list)
(let ((l-list '()))
(dolist (i list)
(funcall f-function
i l-list))))
展开式:
function clMapc(fFunction, list) {
var lList = [];
for (var i = null, _js_idx23 = 0; _js_idx23 < list.length; _js_idx23 += 1) {
i = list[_js_idx23];
fFunction(i, lList);
};
};
而后,只要是类似的过程,直接使用"cl-mapc"函数而不是"dolist":
(cl-mapc (function push)
'(1 2))
展开式:
clMapc(push, [1, 2]);
示例:一个完整的ParenScript程序
使用说明
引用项目
在网页应用里引用"day-or-evening.js"
<script src='day-or-evening.js'></script>
在asdf里依赖项目"day-or-evening"
(asdf:defsystem #:foo :depends-on (#:day-or-evening))
使用外部操作符
(day-or-evening:day-or-evening)
DOE$dayOrEvening();
'day'
系统:day-or-evening
day-or-evening.asd
(asdf:defsystem #:day-or-evening
:depends-on (#:cl-who
#:parenscript
#:cl-css
#:external-program)
:serial t
:components ((:file "package")
(:file "utility")
(:file "html")
(:file "css")
(:file "js")
(:file "build")))
package.lisp
(in-package #:common-lisp)
(defpackage #:day-or-evening
(:use #:common-lisp)
(:export #:get-hours
#:day-or-evening))
(setf (ps:ps-package-prefix :day-or-evening)
"DOE$")
utility.lisp
(in-package #:day-or-evening)
字符串导出为文件
(defun string-to-file (string destination-file) (with-open-file (out destination-file :direction :output :if-exists :supersede) (princ string out)))
git仓库里文件没有更新
(defun nothing-to-commit-p (filespec) (let ((old (sb-posix:getcwd)) (new (make-pathname :directory (pathname-directory filespec))) (filename (concatenate 'string (pathname-name filespec) "." (pathname-type filespec)))) (cl-ppcre:scan-to-strings "nothing to commit, working tree clean" (with-output-to-string (out) (sb-posix:chdir new) (let ((result (external-program:run "git" `("status" ,filename) :output out))) (sb-posix:chdir old) result)))))
给生成的HTML、CSS、JavaScript文件带上版权声明
(defvar *license* "Copyright (c) 2018, SunDawning <[email protected]> https://github.com/SunDawning All rights reserved.") (defun string-to-html-file (string destination-file) (string-to-file (format nil "~A~%(html comment removed: ~%~A~%)" string *license*) destination-file)) (defun string-to-css-file (string destination-file) (string-to-file (format nil "~A~%/*~%~A~%*/" string *license*) destination-file)) (defun lisp-file-to-js-file (source-file destination-file &key do-not-use-strict-p force-export-p) "JavaScript语法支持严格模式:”use strict” - daoyuly - 博客园: http://www.cnblogs.com/daoyuly/archive/2013/03/26/2983614.html" (if (or force-export-p (not (probe-file destination-file))) (with-open-file (out destination-file :direction :output :if-exists :supersede) (unless do-not-use-strict-p (format out "~S;~%" "use strict")) (princ (parenscript:ps-compile-file source-file) out) (format out "/*~%~A~%*/" *license*)) (unless (nothing-to-commit-p source-file) (lisp-file-to-js-file source-file destination-file :force-export-p t))))
html.lisp
(in-package #:day-or-evening)
(defparameter *html*
(cl-who:with-html-output-to-string (*standard-output* nil :prologue t
:indent t)
(:html
(:head
(:meta :http-equiv "Content-Type"
:content "text/html,charset=utf-8")
(:meta :http-equiv "Content-Type"
:name "viewport"
:content "width=device-width")
(:title "Day or Evening")
(:base :target "_blank")
(:link :type "text/css"
:rel "stylesheet"
:href "day-or-evening.css"))
(:body
(:script :src "day-or-evening.js")))))
css.lisp
(in-package #:day-or-evening)
(defparameter *css*
(cl-css:css
`(("body"
:background "black"
:font-size "16px"))))
js.lisp
JavaScript
(in-package #:common-lisp) (defun day-or-evening::get-hours () (parenscript:new (parenscript:chain (-date) (get-hours))))
Common Lisp
(in-package #:day-or-evening) (defun day-or-evening () (if (>= (get-hours) 20) "evening" "day"))
build.lisp
(in-package #:day-or-evening)
(let ((directory (asdf:system-source-directory :day-or-evening)))
(lisp-file-to-js-file (merge-pathnames "js.lisp" directory)
(merge-pathnames "day-or-evening.js" directory))
(string-to-html-file *html*
(merge-pathnames "day-or-evening.html" directory))
(string-to-css-file *css*
(merge-pathnames "day-or-evening.css" directory)))
有了"build.lisp"文件后,只要:
(ql:quickload :day-or-evening)
就能生成JavaScript文件、HTML文件、CSS文件,完成项目搭建、更新、维护过程。
Footnotes
- 这是一个Emacs模式,先通过在SLIME中展开代码,再用Skewer将展开的代码发送到浏览器里,返回计算的结果。
目标是为了创造一个REPL环境来开发ParenScript程序。