SunDawning avatar

On ParenScript

sundawning

Published: 29 May 2018 › Updated: 29 May 2018On ParenScript

On ParenScript

[创建时间]:<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
  • 浏览器

过程:

  1. 编写纯粹的ParenScript代码

    (if (>= (new (chain (-date) (get-hours)))
            20)
        "evening"
        "day")
    
  2. 与浏览器建立联系
    M-x run-skewer

  3. 像SLIME一样求值上述ParenScript代码
    M-x trident-eval-last-expression

    "day"
    

用NodeJS来求值ParenScript程序

需要提前准备好:

  • SBCL
    • ParenScript
  • Emacs
    • Trident
    • SLIME
  • NodeJS

过程:

  1. 编写纯粹的ParenScript代码

    (if (>= (new (chain (-date) (get-hours)))
            20)
        "evening"
        "day")
    
  2. 与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)))
    
  3. 像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程序:

  1. 就像使用其它Common Lisp包一样,使用"lscript"
  2. 就像使用外部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程序

使用说明

  1. 引用项目

    1. 在网页应用里引用"day-or-evening.js"

      <script src='day-or-evening.js'></script>
      
    2. 在asdf里依赖项目"day-or-evening"

      (asdf:defsystem #:foo
        :depends-on (#:day-or-evening))
      
  2. 使用外部操作符

    (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)
  1. 字符串导出为文件

    (defun string-to-file (string destination-file)
      (with-open-file (out destination-file
                           :direction :output
                           :if-exists :supersede)
        (princ string out)))
    
  2. 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)))))
    
  3. 给生成的HTML、CSS、JavaScript文件带上版权声明

    (defvar *license*
      "Copyright (c) 2018, SunDawning <dpmeichen@gmail.com> https://github.com/SunDawning
    All rights reserved.")
    
    (defun string-to-html-file (string destination-file)
      (string-to-file (format nil "~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

  1. JavaScript

    (in-package #:common-lisp)
    
    (defun day-or-evening::get-hours ()
      (parenscript:new
       (parenscript:chain (-date)
                          (get-hours))))
    
  2. 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

  1. 这是一个Emacs模式,先通过在SLIME中展开代码,再用Skewer将展开的代码发送到浏览器里,返回计算的结果。
    目标是为了创造一个REPL环境来开发ParenScript程序。

Leave On ParenScript to:

Written by

Read more #common-lisp posts


Best Posts From SunDawning

We have not curated any of sundawning's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.

More Posts From SunDawning