风格指南

简介

这是一份关于编写可维护的优质 Common Lisp 代码的主观指南。

此页面主要基于 Google 的 Common Lisp 风格指南 和 Ariel Networks 自己编写的 指南

内容

总体指导原则

使用库

在开始一个项目之前,请寻找可以解决你要解决问题的库。创建一个不依赖于其他库的项目并不是某种美德。它不会帮助可移植性,也不会帮助将 Lisp 程序转换为可执行文件。

编写与其他库解决相同问题的库会损害 合并。通常,只有在打算让它物有所值时才应该这么做:优秀的完整文档、示例和精心设计的网站(综合考虑)是编写替代现有库的备选库的充分理由。

和往常一样,检查你使用的库的许可信息,寻找是否不兼容。

编写库

在开始一个项目之前,请考虑它的结构:所有组件都必须在项目中实现吗?还是有供其他人使用的部分?如果是,请将项目拆分为库。

如果你开始编写一个庞大的单块代码,然后失去兴趣,你会得到一个雄心勃勃的项目 30% 的成果,由于它绑定到未完成代码的其他部分而对其他人而言完全不可用。

如果你将自己的项目视为由一层薄的特定于领域的特性绑定的独立库的集合,那么如果你对项目失去兴趣,你会为其他人留下一系列有用的库,以便他们使用和进行构建。

简而言之:编写多个小库

命名

总体风格

名称采用小写,使用单破折号 (-) 分隔,且它们是完整的单词。也就是说,你应编写 user-count 而不是 user-cnt,并且 make-text-node 优于 mk-txt-node

变量

特殊变量(可变全局变量)应由星号包围。这些称为耳罩。

例如

(defparameter *positions* (make-array ...))

(defparameter *db* (make-hash-table))

常量应由加号包围。例如

(defconstant +golden-ratio+ 1.6180339)

(defconstant +allowed-operators+ '(+ - * / expt))

谓词

谓词是一个函数,它基于一些输入,返回 tnil

谓词应使用以下后缀

p
如果函数名称的其余部分是一个单词,例如:abstractpbluepevenp
-p
如果函数名称的其余部分有多个单词,例如 largest-planet-prequest-throttled-p

不要添加包名前缀

这是包的作用。包 myapp.parser 中的函数名称不应以 parser- 开头。

如果您正在绑定一个 C 库,其中每个函数名称都采用 library_name_function_name 格式,请使用库的名称(或者更确切地说,使用名称的合理 Common Lisp 翻译,即 libGUI 应为 lib-gui)作为包名称,并使用函数的实际名称作为函数名称。

格式设置

缩进

针对每个形式缩进两行,例如

(defun f ()
  (let ((x 1)
        (y 2))
    (format t "X=~A, Y=~A" x y)
    (terpri)
    t))

但是,也有一些例外。在 if 特殊形式中,两个分支都必须在同一行上

(if (> x 5)
    (format t "Greater than five")
    (format t "Less than or equal to five"))

行长

行长不得超过 100 列。其他语言中使用的 80 列的下限对于 Common Lisp 不太合适,Common Lisp 鼓励使用描述性变量名称和完整的而非缩写的函数名称。

文件标题

文件顶部应包含 四分号注释 注释,其中描述文件的用途。

不要在文件级别注释中包含版权或作者信息。许可证不应在系统定义和 README 外部提及。

文档

无处不在的文档字符串

Common Lisp 允许您向函数、包、类和各个时隙添加文档字符串,您应该这样做。

注释等级

以四个分号开头的注释 ;;;; 应显示在文件顶部,解释其用途。

以三个分号开头的注释 ;;; 应用于分隔代码的不同区域。

以两个分号开头的注释 ;; 应描述函数或其他顶级形式中的代码区域,而以单个分号开头的注释 ; 应只是对单行的简短说明。

不要使用注释来删除代码

在测试过程中,使用注释(尤其是多行注释)是很好的,可以用来实验性地删除大段代码。然而,所有项目都应使用版本控制,因此应慷慨地删除代码,而不是将其隐藏在注释内。

CLOS

理想的类定义如下所示

(defclass request ()
  ((url :reader request-url
        :initarg :url
        :type string
        :documentation "Request URL.")
   (method :reader request-method
           :initarg :method
           :initform :get
           :type keyword
           :documentation "Request method, e.g :get, :post.")
   (parameters :reader request-parameters
               :initarg :parameters
               :initform nil
               :type association-list
               :documentation "The request parameters, as an association list."))
  (:documentation "A general HTTP request."))

时隙选项

以下时隙选项应按此顺序使用

:accessor:reader:writer
访问器方法的名称。
:initarg
用于初始化值的关键字参数。
:initform(如果有)
如果不显式给定,则为槽的初始值。
:type(如果可能的话)
该槽的类型。
:documentation
说明槽的字符串。

MOP 定义的槽选项应在所有其他槽选项之后、在 :documentation 选项之前添加。

使用 :TYPE 槽选项

类型即说明,而用 Common Lisp 允许声明类槽的类型。

(defclass person ()
  ((name :accessor person-name
         :initarg :name
         :type string
         :documentation "The person's name.")
   (age :accessor person-age
        :initarg :age
        :initform 0
        :type integer
        :documentation "The person's age."))
  (:documentation "A person."))

在此,trivial-types 库会派上用场。

检查类型是否符合初始值在标准中未定义,而是留给实现者来实现。Clozure CL 和 SBCL 1.5.9 及以上版本(或者安全级别高时的所有版本)在编译时执行类型检查。

流程控制

简而言之

使用 WHEN、UNLESS

如果有一个不带 else 部分的 if 表达式,则你应使用 when;如果有一个不带 else 部分的类似 (if (not <condition>) ...) 的表达式,则应使用 unless

例如

(if (engine-running-p car)
    (drive car))

(if (not (seatbelts-fastened-p car))
    (warn-passengers car))

应该是

(when (engine-running-p car)
  (drive car))

(unless (seatbelts-fastened-p car)
  (warn-passengers car))

请注意缩进的区别。

保持条件简短

较大的条件表达式更难阅读,因此应提炼为函数。

例如,此代码

(if (and (fuelledp rocket)
         (every #'strapped-in-p
               (crew rocket))
         (sensors-working-p rocket))
    (launch rocket)
    (error "Aborting launch."))

应写为

(defun rocket-ready-p (rocket)
  (and (fuelledp rocket)
       (every #'strapped-in-p
              (crew rocket))
       (sensors-working-p rocket)))

(if (rocket-ready-p rocket)
    (launch rocket)
    (error "Aborting launch."))

每个文件一个包

除非一个包需要包含多个文件。

避免 :USE

除非你确实需要使用包中的所有(或大多数)符号,否则强烈建议你编写一个手动 :import-from 列表,而不是使用 :use

例如,如果你正在编写一个使用来自 Alexandria 的几个符号的包,请不要这样做

(defpackage my-package
  (:use :cl :alexandria))
(in-package :my-package)

而是这样

(defpackage my-package
  (:use :cl)
  (:import-from :alexandria
                :with-gensyms
                :curry))

层次结构的包名称

例如,以下是游戏中一个示例包层次结构

title
  title.graphics
    title.graphics.mesh
    title.graphics.obj
    title.graphics.gl
  title.config
  title.logging
  title.db

项目结构

目录结构

一般的、小型项目看上去是这样

cl-sqlite3/
  src/
    cl-sqlite3.lisp
  t/
    cl-sqlite3.lisp
  .gitignore
  README.md
  cl-sqlite3.asd
  cl-sqlite3-test.asd

作为一个较大的、使用 持续集成、多个包和可选贡献系统的项目的示例,请看一个假设的网络爬取框架是什么样的

spider/
  src/
    http/              # The code here would be under the package `spider.http`
      request.lisp
      response.lisp
    downloader/        # The code here would be under the package `spider.downloader`
      downloader.lisp
      middleware.lisp
    condition.lisp     # Web scraping-related conditions
    util.lisp          # Utility functions
    settings.lisp      # Settings for the web scraper
  t/
    http.lisp          # Request/response test suite
    downloader.lisp    # Downloader test suite
    final.lisp         # Code to run the test suites, and set up/tear down any test fixtures
  contrib/
    run-js/            # A contrib module to run JavaScript in the scraper
      README.md        # A description of the module
      run-js.lisp      # The source code
  .gitignore
  .travis.yml          # The .travis.yml file to enable continuous integration
  README.md
  spider.asd
  spider-test.asd
  spider-run-js.asd

系统定义

应定义以下系统定义文件

project.asd
库或应用程序本身的系统定义。包含大多数元数据。
project-test.asd
用于项目的单元测试。

提供的模块各自应该有自己的 .asd 文件。

选项

应指定以下系统定义选项

:author, :maintainer
项目的原始作者和当前负责人。如果您是首次撰写系统定义,这两部分内容应相等。
:license
项目的许可证。应使用一个短字符串来描述许可证的名称。
:homepage
指向项目主站的链接。可以是实际的主站,也可以是指向 GitHub 或 Bitbucket 资料库的链接。
:version
项目的版本字符串。

以下小节展示了 .asd 文件的示例内容。请注意,这些示例适用于包含单个文件的小型库的最简单情况。对于较大的项目,您应当将代码拆分至多个文件。

主系统定义

主系统定义是项目的第一个“入口点”,因此,应当包含所有相关元数据。其类似于以下内容

(defsystem my-project
  :author "John Q. Lisper <jql@example.com>"
  :maintainer "John Q. Lisper <jql@example.com>"
  :license "MIT"
  :homepage "https://github.com/johnqlisp/my-project"
  :version "0.1"
  :depends-on (:local-time
               :clack)
  :components ((:module "src"
                :serial t
                :components
                ((:file "my-project"))))
  :description "A description of the project."
  :long-description
  #.(uiop:read-file-string
     (uiop:subpathname *load-pathname* "README.md"))
  :in-order-to ((test-op (test-op my-project-test))))

测试系统定义

测试系统的系统定义文件所需元数据并不像主系统定义文件那么多。其类似于以下内容

(defsystem my-project-test
  :author "John Q. Lisper <jql@example.com>"
  :license "MIT"
  :depends-on (:my-project
               :some-test-framework)
  :components ((:module "t"
                :serial t
                :components
                ((:file "my-project")))))

其他代码

摘自 ASDF 手册

不过,建议将此类形式精简至最低限度,而转而使用 :defsystem-depends-on 定义您将用到的 defsystem 扩展。

系统定义文件应当包含系统定义,并且不应包含其他过多内容。它们不应当包含任何代码或任何会修改 ASDF 设置的内容。如果您需要执行此操作,请在 初始化文件 中执行操作。

在运行中调用以将 README 文件的内容读入系统定义的 :long-description 选项是允许的。

README

您应当针对 README 文件使用 Markdown,原因有以下两个

普通的 README.md 文件应类似于以下内容

# [Project title]

[A short, one-line description of the project]

# Overview

[A longer description of the project, optionally with sub-sections like
'Features', 'History', 'Motivation', etc.]

# Usage

[Examples of usage]

# License

Copyright (c) [Year] [Authors]

Licensed under the [Your license here] License.

如果项目足够小,您应当在“使用”之后包含一个“文档”部分,阐述其 API。但是,较大的项目应当有单独的文档。

使用持续集成服务

持续集成 允许您将项目的测试转移到外部服务,并且让潜在用户能够看到其测试通过,而无需亲自下载并进行测试

CI 服务特别适用于项目依赖于必须安装外部依赖项(例如数据库)进行测试的情况。ORM 或数据库界面库,或与用 C 编写的库的绑定,需要对外部内容进行特定的配置和设置,才能进行测试。CI 服务允许你安装所有必需的软件包,并配置虚拟机/容器,以便测试不需要用户付出任何努力。