本文是《实战Common Lisp》系列的第一篇文章。本系列主要讲述在使用Common Lisp时能派上用场的小函数,希望能为Common Lisp的复兴做一些微小的贡献。
序言
众所周知,Common Lisp没有内置多少处理字符串的函数,下面的代码便能看到所有以STRING开头的函数名:
1 | (do-external-symbols (s :cl) |
屈指可数,而且大部分是比较两个字符串的!很多其它语言中的“标配”是不存在的,比如想要将多个字符串连接起来这么简单的功能都没有!要么自己实现,要么依赖第三方库,比如cl-str
提供的join
函数。
标准库也没有内置正则表达式,好在有一个优秀的第三方库可以用:cl-ppcre
。一种使用正则的常见需求是提取符合某种模式的内容,比如提取一篇Markdown文章中所有的图片链接。图片链接可以由下列正则括号的部分匹配:
1 | "!\\[.*\\]\\((.*)\\)" |
在cl-ppcre中这个括号匹配的部分叫做第一个register。我想要写一个能够提取出字符串中所有符合这段正则的第一个register,分两步走:
- 先实现一个
all-first-registers-to-strings
函数,实现通用的、提取一个字符串中所有符合某段正则的子串的第一个register的内容; - 基于
all-first-registers-to-strings
实现一个extract-image-paths
。
all-first-registers-to-strings
要实现这个函数,需要借助cl-ppcre的scan
函数。根据scan函数的文档,当正则匹配成功时,它的第三、第四个返回值表示正则中register的起点和终点在字符串中的偏移——它们是两个数组,起点和终点一一对应。有了起点和终点的偏移,再使用CL:SUBSEQ
便能提取出register对应的子串。如果要把字符串中所有匹配register的内容都拿出来,就反复调用scan函数,直到再也没有匹配成功为止。
all-first-registers-to-strings
的定义如下:
- 为了不停地在
target-string
中前进,用变量pos
存储scan
函数的第二个返回值,并作为下一次调用scan
时的start
参数——显然,pos
的初始值为0; - 为了跳出
loop
,使用了return-from
直接从函数返回; - 用
push
收集最终结果,再用nreverse
处理成最终的返回值——印象中《On Lisp》也说过这是一种比较常见的手法。
后记
有了all-first-registers-to-strings
,就可以轻松实现extract-image-paths
了:
1 | (defun extract-image-paths (content) |
显然,这个all-first-registers-to-strings
函数实现得很糟糕,尤其是loop
的用法实在是太不Lispy了。Common Lisp的loop
的用法纷繁复杂如天上的星星,多半可以用更优雅的方法来重写一遍,这个就留给各位读者作为私下的乐趣吧。