On ParenScriptsteemCreated with Sketch.

in common-lisp •  6 years ago 

[创建时间]:<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 <[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

  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程序。
Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!