一些适用于Node.js的命名约定

序言

There are only two hard things in Computer Science: cache invalidation and naming things.

– Phil Karlton

乔鲁诺·乔巴拿有一个梦想,便是要成为程序员巨星。但如果你看过我写的代码,便知道我还远远够不上“巨星”二字。我的代码中有许多不一致的命名:

  1. 常量的命名时而是全大写的(如WAIT_CONFIRM),时而是全小写的;
  2. 某个项目大部分命名用的是camel case(如TaskController),但由于数据库中的列名用了snake case(如context_id),导致项目中与数据库列有关的代码混用了camel case和snake case(如restricted_hours[new Date(timestamp * 1000).getHours()] = 1;);
  3. 同样是构造复杂对象的函数,它们的前缀可能会是build、create、make,甚至compute中的任何一个;
  4. 明明是一个数组,却用了单数的order作为变量名。

之所以如此混乱,正是因为我没有遵循一套一致的命名规则。每当我在一个项目中蹦出一些新想法时,便会跃跃欲试——不,我真的就用上了。我不曾整理过自己的命名规则(天哪我已经写了三年的JavaScript了),以至于无从判断“新想法”是否真的新——也许它是一个已经被我抛弃的规则。

为了不再深陷不一致命名的泥潭,我定下了本篇的命名规则,期望它们为以后的我指点迷津。

变量名

通用规则

  • 变量名使用camel case的命名风格。例如,使用namingConvention,而不是naming_convention
  • 在尽量遵循规则的基础上随机应变。

变量名的单复数规则

  • 如果一个变量存储的值的类型为数组(即该变量作为Array.isArray方法的参数时结果为真),那么变量的名称就使用复数形式。例如,使用fruits = []而不是fruit = []
  • 如果一个变量存储的值的类型为集合(即Set这个类型),那么变量的名称应当使用单词unique为前缀。例如,使用uniqueUserIds而不是userIdSet

布尔变量的命名规则

如果一个变量的值的类型为boolean,那么变量的名称应当以下列单词为前缀:

  • is。当变量表达一个二元状态的时候,例如isFullisEmpty。在is后面的应当是一个形容词;
  • has。当变量表达历史上是否发生过某个事件的时候,例如hasPaidhasArrived。在has后面的应当是一个动词,并且采用过去分词;
  • can。当变量表达某种权限的时候,例如canWritecanExecute。在can后面的应当是一个动词,并且采用现在分词。

数值变量的命名规则

  • 如果变量中存储的是一系列数字中的最大值,那么变量的名称应当以max为前缀,例如maxScore。如果是最小值,则是以min为前缀,例如minScore
  • 如果变量中存储的是一系列数字的和,那么变量的名称应当以total为前缀,例如totalIncome
  • 如果变量中存储的是数组的长度,那么变量的名称可以用numberOf为前缀,例如numberOfUsers

字符串变量的命名规则

  • 如果表达的是人名、品牌名、公司名、数据库中的表名,那么变量的名称可以用单词name结尾,例如customerNamebrandNamecompanyName
  • 如果表达的是按键或按钮上刻着的文字、纸质表格或电子表单上输入框左侧的简短说明,那么变量的名称可以用单词label结尾,例如buttonLabel
  • 不允许使用单词content

函数名

  • 函数的名称应当由动词和名词组成,例如readFilewriteFile
  • 如果函数的功能是将参数转换为另一种形式的输出(比如进制转换、币种转换),那么函数的名称应当以单词to为前缀,例如toDollartoHexadecimal
  • 如果函数的功能是检验参数并返回一个布尔值,那么函数的名称应当以单词check为前缀,例如checkIsDirectorycheckIsExecutable
  • 如果函数的功能是“计数”,那么函数的名称应当以单词count为前缀,并且其中被计数的对象应当为复数形式,例如countPaidOrders

构造型函数的命名规则

  • 如果表达的是从无到有地创造一个对象,那么函数名可以用create作为前缀,例如createObject
  • 如果表达的是将一些输入原封不动地放在一起(可能输入之间添加了其它东西)创造出一个对象,那么函数名可以用make作为前缀,例如makeFloor。进一步地,如果函数不改变输入的相对顺序,那么函数名可以用concat,例如concatString
  • 如果函数会将根据输入创造出具有不止一个层级的对象,那么函数名可以用build作为前缀,例如buildBinarySearchTree

修改型函数的命名规则

  • 如果函数负责更新数据库中的记录,那么函数名应当以单词update为前缀;
  • 如果函数修改的是一些可枚举的状态,那么函数名应当以单词change为前缀。

类名

  • 类的名称应当采用capital case的命名风格,例如DatabaseConnection
  • 类的名称应当以一个名词结尾;
  • 如果类的存在是为了使用某种设计模式,那么类名应当可以反映在设计模式中所处的位置,例如在State模式中,代表具体状态的类的名称可以是InitialStateUnpaidState

参考资料

带你见识一下传说中的Emacs

本文通过与VSCode作对比,来简单地介绍Emacs的基本功能、特点,以及一些插件。本文所说的Emacs指的是GNU Emacs,下文简称Emacs。

基本概念

  • Emacs是一个文本编辑器,就像VSCode那样。它可以用来写代码、Markdown,以及其它任何纯文本;
  • 在Emacs中打开的每个文件都有一个“主模式”(major mode),就像在VSCode中每个文件都可以设置一种语言模式;
  • 除了主模式,在Emacs中还可以同时启用多个“次模式”(minor mode)。每一个次模式都可以提供自己的个性化功能;
  • 可以用一门叫ELisp的编程语言为Emacs开发插件,扩展新功能。有许多现成的插件可以安装使用。

与VSCode对比

接下来通过与VSCode作对比,来直观地感受一下Emacs的基本功能。

外观

Emacs的官网上有一张应用截图

Emacs的官网截图

第一眼看起来和VSCode还是非常不一样的

VSCode的官网截图

默认的Emacs界面上也会有菜单栏、工具栏,以及底下的状态栏(在Emacs中其实这一行叫做mode line),这些元素在VSCode上也可以找到。VSCode一般给人的印象还有侧边栏、资源管理器视图,以及minimap视图。Emacs的默认底色是白色,而VSCode则是黑色。

键盘操作

Emacs有着丰富的快捷键(在Emacs中称之为key binding),但一些常见的功能的快捷键与VSCode等其它软件并不相同,如下表所示

Emacs VSCode
新建文件 C-x C-f ⌘n
打开文件或目录 C-x C-f ⌘o
保存文件 C-x C-s ⌘s
另存为 C-x C-w ⇧⌘s
撤销 C-x u ⌘z
剪切 C-w ⌘x
复制 M-w ⌘c
粘贴 C-y ⌘v
查找 C-s ⌘f

在上表中,C-x表示先按住control键再按x键,M-w表示先按住alt键再按w键;表示Mac上的command键、表示shift键。此外,在Emacs中还可以使用C-pC-nnC-b,以及C-f键来往上、下、左、右四个方向移动光标,不需要移动手臂便可以在编辑的文件中到处移动,提高效率。VSCode也可以使用Emacs风格的快捷键,如下图所示

文件管理

VSCode自带美观大方的标签页功能,此外还可以将编辑器横向及纵向拆分,如下图所示

Emacs默认是没有标签页的功能的,但也支持切割编辑器,比如在下图中,就将编辑器分为左右两部分,并且左侧还被分为了上下两部分,这三个区域可以展示相同或不同的三个文件。

默认的编程语言支持

VSCode支持非常多的编程语言,点击窗口右下角的语言模式便可以看到这份清单

Emacs虽然没有这么一份清单,但支持的语言的数量也是不遑多让的。不过仅仅是默认的Emacs的话,对编程语言的支持没有VSCode那么开箱即用。例如,比起Emacs,VSCode默认对JavaScript的支持就很好,不仅仅有语法高亮、自动补全,并且还有基于变量类型、函数定义,以及导入的模块等信息实现的智能补全,而Emacs只有平凡的基于文本的语法高亮和自动补全罢了。

搜索功能

除了按下⌘f在文件内搜索之外,VSCode左侧工具栏中有一个名为搜索的入口,可以实现在打开的项目的所有文件中搜索特定内容的功能,并且还能用于替换。Emacs尽管没有这么一个GUI入口,但提供了grep命令来做到同样的事情。

在Emacs中按下M-x后输入grep并按回车,便会在minibuffer中等待使用者的进一步输入

显然,这是直接调用了同名的命令行程序grep来实现搜索的,控制选项比VSCode的搜索功能要丰富许多。

集成git

git可以说已经成为了日常开发中不可或缺的一个工具,如果能够在编辑器内方便地调用git的话会大大提高效率。VSCode通过左侧工具栏的源代码管理入口提供了这个功能,在Emacs中则是通过一个叫做VC dir的主模式提供这个功能。

在Emacs中按下快捷键C-x v d,然后输入使用git管理的项目的目录地址,便会打开一个新的编辑区域

当处于这个模式下时,会有一些新的快捷键可以用,比如将光标移动到显示edited的行上按下等号键,可以打开另一个编辑区域查看该文件的修改内容;按下m键可以选中光标所在行的文件,然后按v键打开一个新的编辑区域来填写commit message,写完之后按下C-c C-c提交(即调用git commit命令);最后按下q键可以退出VC dir模式的编辑区域。

集成终端

在VSCode中通过选中顶部菜单的“查看”,再点击“集成终端”,便可以打开命令行,然后像平时在其它的终端模拟器中那样使用命令。Emacs中也可以打开终端,方法是按下快捷键M-x然后输入eshell并回车,然后Emacs便会在当前窗口中打开一个名为*eshell*的编辑区域,显示命令行提示符和闪烁的光标,并等待使用者的进一步交互。

有意思的是,这不是一个真正的命令行程序。比如在*eshell*中输入which pwd,输出结果是eshell/pwd is a compiled Lisp function in ‘em-dirs.el’.。也就是说,eshell中的某一些命令是Emacs重新实现的;此外,在*eshell*中可以运行Emacs的扩展语言——ELisp。比如输入(+ 1 1)并按下回车,会输出2。

扩展能力

VSCode有一个插件市场

Emacs也有一个,不过得承认这个网页确实没有VSCode阵营的吸引人

在Emacs中按下M-x后输入package-install回车,Emacs会等待用户输入要安装的插件的名字——可以按下键(Mac上的tab键)让Emacs尝试自动补全。确认名字无误后再按回车便可以安装使用了——但一般还需要做一些微调。

炫酷的特性和插件

除了上面与VSCode对比的一些基本操作以外,Emacs还有自身的一些特色功能,更有世上的众多优秀程序员为Emacs贡献了不胜枚举的优秀插件,它们极大地扩展了Emacs的能力,提升了文字编辑这项活动的效率,甚至超越了文字编辑。

内置的功能

颜色主题

Emacs也可以安装颜色主题来改变外观。我比较喜欢的一款主题是gruvbox-light-soft,只需要在Emacs的配置文件中写上(load-theme 'gruvbox-light-soft t)即可启用。在这个主题的仓库主页可以看到一些效果图。

将当前行居中或置顶

Emacs可以用纯键盘的操作,将光标当前所在的行移到窗口的中间来显示。我特别喜欢这个功能,之前用VSCode时候一直想找这个功能的等价物,可惜没找着。在Emacs中,按下一次control+l,光标所在的行就会移动到窗口的中间;按下第二次,则移动到窗口的顶部展示;再按一次,会去到窗口的底部。如果再按一次,那么就跟第一次一样回到窗口的中间。每当Emacs正在编辑的内容已经去到屏幕上较低的位置时,我便会用这个功能校正一下。

比起用鼠标拖动滚动条或者用滚轮来滚动,我更喜欢这种表意更清晰的方式。

纯键盘选中一片区域

在Emacs中可以用单击鼠标右键的方式来选中一片区域。当按下鼠标右键的时候,从光标所在的位置开始,到鼠标点击的位置结束的内容便会被选中。但我个人更喜欢纯键盘的操作,首先是在要选中的内容的起点按下快捷键C-@,这时候Emacs会在minibuffer中打印一条Mark set的消息。然后移动光标——用方向键还是用一系列的快捷键,甚至直接跳转到某一行也可以——到待选中的内容的终点。这时候加在起点和终点间的内容便会被选中,它们会有特殊的背景色,如下动图所示

因为按C-@实在是太别扭了,所以我将这个快捷键修改为了M-SPC(先按住alt键再按下空格键)。如果更喜欢鼠标操作的话,也可以像在VSCode中那样,在终点按住shift键再单击鼠标左键。

矩形编辑

在VSCode中按住shiftoption键,再单击鼠标右键并拖动就可以选中一片矩形的区域,在Emacs中也支持这样的矩形编辑的功能。像下图这样,先定位到要选中为矩形区域的左上角按下C-@,再移动光标到目标矩形区域的右下角,最后按下快捷键C-x r k即可。

惊艳的插件

快速跳转

Emacs本身支持跳转到指定的某一行,只需要按下快捷键M-g M-g然后输入行号并回车即可。但我一般是不显示行号的,所以这个功能其实比较少用。我使用一个名为avy的插件来增强跳转功能,主要用的是它提供的avy-goto-line函数。依照这个插件的文档,我为这个函数配置了快捷键M-g f,因此当我想要快速地跳转到屏幕上可见的区域中、离光标所在位置稍微有点远的行时,我便按下这个快捷键,然后按照提示按下相应的英文字母键既可,具体效果参见下面的演示

发出HTTP请求

VSCode中有一个叫做REST Client的插件,Emacs中则是有一个叫做restclient.el的主模式。借助于restclient.el便可以直接在一个文本文件中写好自己要发出的HTTP请求的内容,然后一键触发。之前我是用Insomnia这个工具的,尽管它很强大,但很多时候我不需要那么强大的功能,而且Insomnia消耗内存比较多,于是我便回到restclient.el上了。restclient.el的效果大致如下图所示

增强的git集成功能

尽管Emacs自带了VC dir这个主模式,但我更喜欢用magit这个插件,尤其是它的magit-discard功能,可以在查看代码的差异的过程中方便地舍弃一些不必要的修改(例如添加一行console.log的调用)。例如下图,通过按下n将光标移动到某一片修改上再按下k键,Emacs便会询问使用者是否要“丢弃”这一块改动。如果按下y,那么这一块被选中的区域的内容便会恢复到git当中未修改的状态。

增强的文件内搜索功能

Emacs默认的搜索使用的是search-forward函数,插件swiper提供的功能更强大——不仅可以是字符串的完全匹配,也可以基于正则表达式来搜索,并且展示效果更直观,如下图所示

代码片段工具

VSCode自带了一个“用户代码片段”的功能(通过顶部菜单“Code”,再选中“首选项”可以看到),可以用来定义一些短语,这些短语会在被选中的时候展开为完整的内容。Emacs有一个名为yasnippet的插件也提供了类似的功能,但定义代码片段的语法不同。并且,yasnippet支持在短语的定义中嵌入ELisp代码,扩展性远远高于只能使用字符串及占位符的VSCode的等价功能。

其它

Emacs还有许多有意思的插件,比如dimmer.el,可以让当前没有获得焦点的窗口显示得黯淡一点;ledger-mode,可以用Emacs来记账。这里就不一一列举了,各位有兴趣的话可以自己摸索Emacs,相信会遇到自己喜欢的插件的。

总结

比起家大业大的VSCode,Emacs算不上是开箱即用。它没有VSCode那么友好的界面和平易近人的操作方式,人气也没有VSCode那么旺,当遇到问题的时候可能没那么好求助到人,并且学习曲线(小众的预设快捷键、冷门的扩展语言)也比较高。但Emacs的扩展能力很强,现有的插件已经很丰富了,可以满足大部分的需求,遇到问题也可以到有模有样的论坛求助。如果喜欢折腾的话,Emacs会是一个不错的选择,至少我自己用得很开心。

将二叉树写到磁盘上

背景

有一阵子很好奇一个问题:MySQL到底是如何将内存中的B+树写入到磁盘文件中的。明明是一棵树,要怎样才能存储成线性的字节流呢?干脆自己动手,试着实现一个简单的版本,来帮助自己摸点门道。虽然想法很不错,不过一上来就面对噩梦级别的B+树也太为难人了,因此就先从简单的二叉树入手吧。

出来吧,二叉搜索树

本文使用Common Lisp进行开发。

首先定义这棵二叉搜索树的节点的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defclass <node> ()
((data
:accessor node-data
:initarg :data
:documentation "节点中的数据")
(left
:accessor node-left
:initarg :left
:documentation "左子树")
(right
:accessor node-right
:initarg :right
:documentation "右子树"))
(:documentation "二叉搜索树的节点"))

基于节点进一步定义二叉树的类型

1
(deftype <bst> () '(or <node> null))

如此一来,要创建节点和空树都是浑然天成的事情了

1
2
3
4
5
6
7
8
9
10
11
12
13
(defun make-node (data left right)
"创建一个二叉搜索树的节点"
(check-type data integer)
(check-type left <bst>)
(check-type right <bst>)
(make-instance '<node>
:data data
:left left
:right right))

(defun make-empty-bst ()
"创建一颗空树"
nil)

要判断一颗二叉树是否为空树只需要简单包装一下cl:null函数即可

1
2
3
(defun empty-bst-p (bst)
"检查BST是否为一个空的二叉搜索树"
(null bst))

为了生成必要的测试数据,需要提供一个往二叉树中添加数据的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(defun insert-node (bst data)
"往一颗现有的二叉搜索树BST中加入一个数据,并返回这颗新的二叉搜索树"
(check-type bst <bst>)
(check-type data integer)
(when (empty-bst-p bst)
(return-from insert-node
(make-node data
(make-empty-bst)
(make-empty-bst))))

(cond ((< data (node-data bst))
(setf (node-left bst)
(insert-node (node-left bst) data))
bst)
(t
(setf (node-right bst)
(insert-node (node-right bst) data))
bst)))

有了insert-node便可以从空树开始构筑起一棵二叉搜索树

1
2
3
4
5
6
7
(defun create-bst (numbers)
"根据NUMBERS中的数值构造一棵二叉搜索树。相当于NUMBERS中的数字从左往右地插入到一棵空的二叉搜索树中"
(check-type numbers list)
(reduce #'(lambda (bst data)
(insert-node bst data))
numbers
:initial-value (make-empty-bst)))

现在来生成稍后测试用的二叉树

1
(defvar *bst* (create-bst '(2 1 3)))

模仿命令行工具tree的格式,提供一个打印二叉树的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(defun print-spaces (n)
"打印N个空格"
(dotimes (i n)
(declare (ignorable i))
(format t " ")))

(defun print-bst (bst)
"打印二叉树BST到标准输出"
(check-type bst <bst>)
(labels ((aux (bst depth)
(cond ((empty-bst-p bst)
(format t "^~%"))
(t
(format t "~D~%" (node-data bst))
(print-spaces (* 2 depth))
(format t "|-")
(aux (node-left bst) (1+ depth))
(print-spaces (* 2 depth))
(format t "`-")
(aux (node-right bst) (1+ depth))))))
(aux bst 0)))

二叉树*bst*的打印结果如下

1
2
3
4
5
6
7
2
|-1
|-^
`-^
`-3
|-^
`-^

接下来终于要写盘了

总算要开始实现将二叉树写入磁盘文件的功能了。将内存中的二叉树写入到文件中,相当于将树形的数据结构转换为线性的存储结构——毕竟磁盘上的文件可以认为就是线性的字节流。在这块字节流中,除了要保存每一个节点的数据之外,同样重要的还有节点间的父子关系。

有很多种写盘的方法。比如说,可以模仿树的顺序存储结构将二叉树序列化到磁盘上。以上面的二叉树*bst*为例,它是一棵满二叉树,如果采用顺序存储,那么首先分配一个长度为3的数组,在下标为0的位置存储根节点的数字2,在下标为1的位置存储左孩子的数字1,在下标为2的位置存储右孩子的数字3,如下图所示

推广到高度为h的二叉树,则需要长度为$2^h-1$的数组来存储所有节点的数据。假设每一个节点的数据都是32位整数类型,那么一棵高度为h的二叉树在磁盘上便需要占据$4·(2^h-1)$个字节。这个做法虽然可行,但比较浪费存储空间。它将节点间的父子关系用隐式的下标关系来代替,节省了存储左右子树的“指针”所需的空间,比较适合存储满二叉树或接近满的二叉树。

对于稀疏的二叉树,如果在序列化后的字节流中显式地记录节点间的父子关系,便可以节省很多不存在的节点所占据的存储空间。比如说,对于每一个节点,都序列化为磁盘上的12个字节:

  1. 下标为0到3的4个字节,存储的是节点中的数据;
  2. 下标为4到7的4个字节,存储的是节点的左子树在文件中的偏移;
  3. 下标为8到11的4个字节,存储的是节点的右子树在文件中的偏移

如下图所示

上面的数组表示磁盘上的一个文件,每一个方格为一个字节,每个方格在文件内的偏移从左往右依次增大。由于采用后序遍历的方式依次序列化二叉树中的节点数据和指针,因此左孩子首先被写入文件,然后是右孩子,最后才是根节点。推广到所有的二叉树,便是先将左右子树追加写入磁盘文件,再将根节点的数据、左子树根节点在文件内的偏移,以及右子树根节点在文件内的偏移追加到文件末尾;如果左右子树是空的,那么以偏移0表示。

这是一个递归的过程,而每一次递归调用应当返回两个值:

  1. 写入的总字节数bytes
  2. 根节点所占据的字节数root-bytes

bytes便是右子树开始写入时的文件偏移,必须依靠这个信息确定右子树的每一个节点在文件内的偏移;使用bytes减去root-bytes,再加上左子树开始写入时的偏移量,便可以得知左子树的根节点在文件内的位置。最终实现写盘功能的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
;;; 定义序列化二叉树的函数
(defun write-fixnum/32 (n stream)
"将定长数字N输出为32位的比特流"
(check-type n fixnum)
(check-type stream stream)
(let ((octets (bit-smasher:octets<- n)))
(setf octets (coerce octets 'list))
(dotimes (i (- 4 (length octets)))
(declare (ignorable i))
(push 0 octets))
(dolist (n octets)
(write-byte n stream))))

;;; 这是一个递归的函数,写入一棵二叉树的逻辑,就是先写入左子树,再写入右子树,最后写入根节点,也就是后序遍历
;;; 由于要序列化为字节流,因此需要用字节流中的偏移的形式代替内存中的指针,实现从根节点指向左右子树
;;; offset是开始序列化bst的时候,在字节流中所处的偏移,同时也是这颗树第一个被写入的节点在字节流中的偏移
;;; 每次调用write-bst-bytes后的返回值有两个,分别为二叉树一共写入的字节数,以及根节点所占的字节数
(defun write-bst-bytes (bst stream offset)
"将二叉树BST序列化为字节写入到流STREAM中。OFFSET表示BST的第一个字节距离文件头的偏移"
(check-type bst <bst>)
(check-type stream stream)
(check-type offset integer)
(when (empty-bst-p bst)
(return-from write-bst-bytes
(values 0 0)))

;; 以后序遍历的方式处理整棵二叉树
(multiple-value-bind (left-bytes left-root-bytes)
(write-bst-bytes (node-left bst) stream offset)

(multiple-value-bind (right-bytes right-root-bytes)
(write-bst-bytes (node-right bst) stream (+ offset left-bytes))

(write-fixnum/32 (node-data bst) stream)
(if (zerop left-bytes)
(write-fixnum/32 0 stream)
(write-fixnum/32 (- (+ offset left-bytes) left-root-bytes) stream))
(if (zerop right-bytes)
(write-fixnum/32 0 stream)
(write-fixnum/32 (- (+ offset left-bytes right-bytes) right-root-bytes) stream))
;; 之所以要加上12个字节,是因为在写完了左右子树之后,就紧邻着写根节点了。因此,根节点就是在从right-node-offset的位置,接着写完右子树的根节点后的位置,而右子树的根节点占12个字节
(let ((root-bytes (* 3 4)))
(values (+ left-bytes right-bytes root-bytes)
root-bytes)))))

(defun write-bst-to-file (bst filespec)
"将二叉树BST序列化为字节流并写入到文件中"
(check-type bst <bst>)
(with-open-file (stream filespec
:direction :output
:element-type '(unsigned-byte 8)
:if-exists :supersede)
(write-fixnum/32 (char-code #\m) stream)
(write-fixnum/32 (char-code #\y) stream)
(write-fixnum/32 (char-code #\b) stream)
(write-fixnum/32 (char-code #\s) stream)
(write-fixnum/32 (char-code #\t) stream)
(write-bst-bytes bst stream (* 5 4))))

现在可以将*bst*写入文件了

1
(write-bst-to-file *bst* "/tmp/bst.dat")

使用hexdump验证写入的效果

文件最开始的五个字节依次存储着字符串"mybst"的ASCII码,为的就是让最早被写入文件中的根节点——也就是二叉树最左下角的节点——的偏移不为0,以免在后续反序列化的时候,从该节点的父节点中读到左子树的偏移为0——这样会被误认为是一棵空树的。

有哪里写得不好的还请各位读者不吝赐教。

在Emacs中搭建笔记查阅系统的尝试

给Emacs写插件有种痛并快乐着的感觉。虽然这个发挥创意的过程很有趣,但是Elisp写起来总有种别扭的感觉。一方面,我把它当成是Common Lisp,写的时候没有觉得“这个用法可能会有问题”;另一方面,它又不是普通的写lisp代码,还要一边写一边摸索Emacs中的一些概念。不过总体而言,还是挺好玩的,除了没有一个像模像样的REPL之外。

来龙去脉

我用Emacs记录了不少的“笔记”。虽说我自己将其称为笔记,但是它们更像是我把遇到的一些问题和解决方法给记录下来,而没有太多自己的感悟。它们的外观倒是高度的一致,见下图

(第一次尝试给自己的图片打水印,有点好玩)每一个一级条目都是一个问题,并且这个文件中只有一级条目。而条目下的内容则是对标题的问题的回答。其中还有代码块——也就是写着BEGIN_SRC和END_SRC的那部分。用org-mode来记录笔记有几个好处,其中一个便是可以在笔记中插入任何Emacs支持的编程语言代码片段并具备语法高亮。当然了,还有一个巨大的优势,便是org-mode尽管看似花里胡哨,骨子里却是正统的纯文本文件,它可以很方便地在其它工具中处理。

而我用来处理的其中一个工具便是ElasticSearch。比如说,上图的第一条笔记,在ElasticSearch中存成了下面这样的结构

本来我是写了一个Alfred的Workflow来查询ElasticSearch的,但是奈何Workflow那种一行行的方式展示org-mode格式的笔记不太友好,因此便打算直接在Emacs中查询并查看笔记内容。

牛刀小试

为了可以在Emacs中查看笔记内容,我打算借助于Helm的力量。Helm是Emacs的一个补全的框架,可以用来呈现一系列的候选项,然后选中后触发一些什么动作。我期望的形式,是在Emacs中按下某种快捷键或者输入某个命令行,可以在minibuffer中输入自己要查询的内容,然后Emacs查询ElasticSearch并最终通过Helm来呈现这些查询内容匹配的笔记条目。目前的成果是下面这样子的

具体的做法其实也很简单。首先,要知道Helm是如何被使用的。通过这篇文档,初步了解到只需要定一个变量,并通过:sources关键字参数传递给helm这个函数即可。我所定义的传递给helm函数的“source”如下

1
2
3
4
5
6
(setq faq-helm-sources
`((name . "FAQ at Emacs")
(candidates . faq-candidates)
(action . (lambda (candidate)
(let ((url (format "http://localhost:9200/faq/_doc/%s" candidate)))
(browse-url url))))))

其中faq-candidates的作用便是根据minibuffer中的关键字查询ElasticSearch并组织好一个结构返回给helm。需要注意的是,faq-candidates必须是一个无参的函数才行,但输入的数据又偏偏需要从minibuffer中获取。因此,我的做法是约定一个变量faq-query,在调用helm之前首先调用read-from-minibuffer函数读取输入,然后将输入的字符串赋值给faq-query,之后当helm开始使用这个source的时候,faq-candidates函数便不需要参数,而可以直接从faq-query中拿到自己需要的搜索内容向ElasticSearch请求了。当然了,如果有像Common Lisp动态作用域的话,也就不需要定义这么一个全局变量了,对Emacs全局的侵入会更少一点。

目前能够做到的也仅仅是查询ElasticSearch,并在选中某个条目并按下回车的时候打开浏览器来查看而已,之后应该会继续完善。目前的完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
;;; 调用ElasticSearch查询笔记
(require 'request)

(defun faq (query)
"向ElasticSearch查询QUERY匹配的笔记"
(let ((response))
(request
"http://localhost:9200/faq/_search"
:data (encode-coding-string
(json-encode
(list
(cons "query" (list
(cons "multi_match" (list
(cons "fields" (list "answer" "question"))
(cons "query" query)))))))
'utf-8)
:headers '(("Content-Type" . "application/json"))
:parser 'buffer-string
:success (cl-function
(lambda (&key data &allow-other-keys)
(setq data (decode-coding-string data 'utf-8))
(setq response (json-read-from-string data))))
:sync t)
response))

(defun make-faq-candidates (response)
"将查询ElasticSearch的结果构造为helm可以识别的candidates格式"
(let ((hits (cdr (assoc 'hits (cdr (assoc 'hits response))))))
(mapcar (lambda (doc)
(let ((_source (cdr (assoc '_source doc))))
(cons (cdr (assoc 'question _source))
(cdr (assoc '_id doc)))))
hits)))

(defvar faq-query nil)

(defun faq-candidates ()
(make-faq-candidates (faq faq-query)))

(setq faq-helm-sources
`((name . "FAQ at Emacs")
(candidates . faq-candidates)
(action . (lambda (candidate)
(let ((url (format "http://localhost:9200/faq/_doc/%s" candidate)))
(browse-url url))))))

(defun lt-ask ()
"交互式地从minibuffer中读取笔记的关键词并展示选项"
(interactive)
(let ((content (read-from-minibuffer "笔记关键词:")))
(setq faq-query content)
(helm :sources '(faq-helm-sources))))

有不少值得吐槽的地方,不过都先按下不表吧,各位读者有兴趣的话可以留言交流一下XD

如何编译defun

本文讲解如何编译defun。在Common Lisp中,defun用于定义函数。例如,下列的代码定义了函数foo

1
2
3
4
(defun foo (a)
"一个名为FOO的函数"
(declare (ignorable a))
(1+ 1))

defun语法中,第一行的字符串是这个函数的文档,可以用documentation函数获取;第二行是declaration。(不管是documentation还是declaration,也许要等到自举的那一天才能够支持了)目前只打算支持如下这般朴素的defun用法:

Read More

编译return语句

Common Lisp中有一个叫做return的宏,它的作用和平常在C、Java,或者Node.js里面见到的return关键字完全不一样。Common Lisp中的return用于从一个块(block)中返的,而不是从一个函数中返回。用return可以写出下面这样的代码,符号YOU-WILL-NOT-SEE-ME永远不会被打印

1
2
3
4
(defun foo ()
(block nil
(return 123)
(print 'you-will-not-see-me)))

求值return,就将123作为block的返回值从中返回了,后面的print并没有机会执行——在SBCL中编译上面这段defun的时候,编译器甚至已经给出了提醒

Read More

一个有用的org-agenda-custom-commands的例子

好久没有写了,文章的开头,照例还是要吹吹水的。

自从更新了基于org-mode的待办事项的管理模式后,感觉整个人都日益神清气爽起来。究其原因,大概是因为现在处理inbox.org和安排第二天的行程的时候,有一个相对可行的操作方法可以参考了,所以每次处理这两件事情的时候,也就没有那么纠结了。同时,还因为采取了一种尽量goal-oriented的TODO管理方法,最大程度上杜绝了一些不必要的TODO被收集起来,从而也减轻了内心的焦虑感。

言归正传,这篇文章是要讲一个我自定义的org-mode的Agenda视图的command的。这条命令是下面这样子的

1
2
3
(setq org-agenda-custom-commands
'(("f" "查看TODO条目(按创建时间排序)" todo "TODO"
((org-agenda-sorting-strategy '(priority-down time-up))))))

俗话说的好,要检验自己是不是懂得某个东西,只要看看自己能不能把这个东西给别人讲清楚就可以了。如果讲清楚了,没有哪里需要emmmm的地方,那么就可以认为这个东西基本上自己是真的懂得了。

那么org-agenda-custom-commands是干嘛用的呢。官方文档的链接在这里:https://orgmode.org/worg/org-tutorials/org-custom-agenda-commands.html ——哎哟喂,其实如果你们身处在Emacs之中来看这篇文章的话,只要按下C-h v,然后在minibuffer中输入org-agenda-custom-commands的话,也就可以看到关于这个变量的说明了啦。不过上面这个文档的好处是它有附赠一些例子。

总而言之,org-agenda-custom-commands是一个变量,通过给这个变量赋值,可以在org-mode的Agenda视图中,添加一些自定义的功能及对应的快捷键。例如,如果在Emacs中求值我上面所给的Elisp代码,然后按下C-c a,便会看到类似于下面这样的提示

这时候,如果按下f键,Emacs就会按照上面代码中描述的那样找出所有关键字为TODO的条目,然后按照【先优先级降序,然后时间戳升序】的方式来排列它们。在我的电脑上的效果如下图所示

可以看到,首先出现的条目是标注为最高的A优先级的两条,然后是在heading内容的开头含有时间戳的条目。

对我来说,这样的一个好处是可以将未处理过的TODO按照时间顺序列出来,从而避免了所有的TODO条目先是按照文件的名称集中起来,然后又按照它们在文件中的顺序从上往下地排列起来。毕竟文件内的TODO都是聚合在各自不同的更高级的heading之下的,它们之间的上下关系体现不出什么东西。

调用C标准库的exit函数

上一篇文章中,实现了对大于号(>)的处理,那么对if表达式的编译也就是信手拈来的事了,不解释太多。在本篇中,将会讲述一下如何产生可以调用来自于C语言标准库的exit(3)函数的汇编代码。

在Common Lisp中并没有一个叫做EXIT的内置函数,所以如同之前实现的_exit一样,我会新增一种需要识别的(first expr),即符号exit。为了可以调用C语言标准库中的exit函数,需要遵循调用约定。对于exit这种只有一个参数的函数而言,情形比较简单,只需要跟对_exit一样处理即可。刚开始,我写下的代码是这样的

Read More

编译大于运算符

原定的计划中这一篇应当是要讲如何编译if表达式的,但是我发现没什么东西可以作为if的test-form的部分的表达式,所以觉得,要不还是先实现一下比较两个数字这样子的功能吧。说干就干,我决定用大于运算符来作为例子——大于运算符就是指>啦。所以,我的目标是要编译下面这样的代码

1
(> 1 2)

Read More