首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
80 阅读
2
nvim番外之将配置的插件管理器更新为lazy
58 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
Java
emacs
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
308
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
23
篇与
的结果
2025-02-19
Emacs折腾日记(十三)——函数、宏以及命令
之前在开篇介绍简单的elisp时候就提到过函数,后面的一些示例中也用到了一些函数,但是都是一些基本的概念,这篇将深入了解函数的一些特性。首先要判断一个符号是否是函数,可以使用 functionp 来判断。(defun foo() 1) (foo) (functionp 'foo) ;; ==> t (setq var 1) (functionp 'var) ;; ==> nil不光函数,以下几种functionp也返回t函数。这里的函数特指用 lisp 写的函数。原子函数(primitive)。用 C 写的函数,比如 car、append。lambda 表达式特殊表达式宏(macro)。宏是用 lisp 写的一种结构,它可以把一种 lisp 表达式转换成等价的另一个表达式。命令。命令能用 command-execute 调用。函数也可以是命令。参数列表的语法过去我们的所有函数都是定参的函数,也就是说是确定了参数个数的函数。但是实际使用中会大量使用不定参函数,也就是参数不确定的函数。在C/C++ 以及 Python中会大量使用。它的一个使用场景就是某些时候不传就采用默认值,否则就采用用户定义的值。另一个场景就是像printf这样事先无法确定到底要输出多少内容。elisp中的函数完整定义如下(defun func (REQUIRED-VARS... [&optional OPTIONAL-VARS...] [&rest REST-VAR]))前面是确定的参数列表,也就是说前面的参数在调用函数时必须传入,而&optional 之后是可选参数,如果要传入可选参数这个 &optional 关键字是必须写上的。这里的可选参数也是需要在定义时一个个的指定出来,但是&rest 之后定义的只用一个变量来使用,在传入的时候可以传入任意个参数。例如下面的例子(defun foo (var1 var2 &optional op1 op2 &rest rest) (list var1 var2 op1 op2 rest)) (foo 1 2) ;; ==> (1 2 nil nil nil) (foo 1 2 3) ;; ==> (1 2 3 nil nil) (foo 1 2 3 4) ;; ==> (1 2 3 4 nil) (foo 1 2 3 4 5) ;; ==> (1 2 3 4 (5)) (foo 1 2 3 4 5 6) ;; ==> (1 2 3 4 (5 6))从这个例子我们可以得出以下几个结论:当可选参数没有提供时,在函数体里,对应的参数值都是 nil。我们可以通过判断是否为nil来判断用户是否传了参&rest 要捕获后面所有传入的参数,所以它必须在参数列表的最后面,它的值是一个list当 &rest 与 &optional 共存时,优先匹配 &optional 参数,最后如果有剩余的参数则分配给 &rest教程原文中是有关于文档字符串的描述的。但是我想现在我作为一个菜鸟,将来要组织自己的配置也主要依靠拷贝粘贴别人现有的东西再组合,没有多少机会参与那种高大上的开源项目,自己将来弄的配置估计也没什么人用,而且我也会详细记录自己攒配置的过程,所以这里就不需要给函数写过于详细的文档说明。这里我就跳过这块了。如果有读者对这块感兴趣可以看原文。函数调用在编写程序的时候会有这种需求,一个框架负责处理大块的内容,比如数据解析、转发等等,它会预留一些接口来让用户在此基础之上处理自己的业务逻辑,比较典型的就是http server,或者gui程序框架。在C/C++ 中一般会预留一些函数指针类型的参数进行回调或者提供接口供使用方重载实现自己的逻辑。在elisp中也有这样的操作,但是它就没有虚函数、虚基类或者函数指针的概念。在elisp中通过符号调用一个函数使用的方法是 funcall 和 apply。它们都是通过符号来调用函数的,唯一的区别在于如何处理传入参数。我们通过一个例子来看看它们有什么不同。我们还是用上面定义的foo函数来测试(funcall 'foo 1 2 3 4 5 6) ;; ==> (1 2 3 4 (5 6)) (apply 'foo 1 2 3 4 5 6) ;; ==> error (apply 'foo 1 2 3 4 '(5 6)) ;; ==> (1 2 3 4 (5 6)) (apply 'foo 1 2) ;; ==> error (apply 'foo '(1 2)) ;; ==> (1 2 nil nil nil) (apply 'foo 1 2 3 4) ;; ==> error (apply 'foo 1 2 '(3 4)) ;; ==> (1 2 3 4 nil) (apply 'foo 1 2 3 4 5) ;; ==> error (apply 'foo 1 2 3 4 '(5)) ;; ==> (1 2 3 4 (5))从上面的结果可以看出,funcall 直接按照对应函数定义的参数列表进行传参即可。而apply在传参的时候最后一个参数必须是list,并且在嗲用时会自动将list参数给展开并传入各个参数。从上面的区别可以看出,如果在调用函数的时候,参数已经通过list进行了组织的话,那么使用apply更为合适,否则使用funcall。宏宏是lisp家族中一个非常重要,也非常灵活的内容,可以说宏是lisp的灵魂。之前在看到一些lisp相关的教程时都说,宏实现了利用代码生成代码,并且因为宏的存在导致lisp中扩展出了大量的方言。可以说没有宏,lisp就不是lisp了,或者说lisp就没这么灵活了。但是C/C++中也有宏的概念,C/C++中的宏是在预处理阶段进行简单的文本替换,然后编译替换之后的结果。虽然利用宏,C/C++中可以实现很多非常复杂的功能,但是它远远没有lisp的宏灵活。要详细了解宏的相关内容,我们先回忆一下之前介绍的elisp的知识。首先elisp或者lisp的代码本身就是一颗语法树。它被写作一个list。也就是说list既可以作为代码执行,也可以作为数据,例如 (setq x 1) 它是一段代码。而 '(setq x 1) 它是一个列表,列表中有3个元素,分别是 setq 、x、1 这么三个符号和数字。再者elisp特有的符号系统,例如 x 表示一个变量,可以对它进行求值,'x 代表一个符号,根据前面所学的,我们可以通过符号找到符号中记录的值、函数、属性等等。基于这两个内容,我们可以通过操作list来实现生成一段代码。例如下列的例子(defun my-inc (var) (list 'setq var (list '1+ var))) (setq x 0) (eval (my-inc 'x)) ;; ==> 1上面的其实就是返回了一个list, (setq var (1+ var))。后面我们通过 eval 来执行这个返回的list。需要注意的时,函数调用时会首先将变量进行求值,然后将值作为参数传入,但是这里我们希望并不希望传入一个具体的值,而是希望他能操作我们传入的变量值,并改变它,要做到这点需要传入一个符号。这里有点像C++ 中的引用传递定义宏其实跟定义函数非常相似。我们只需要将关键字由 defun 改为 defmacro。(defmacro my-inc(var) (list 'setq var (list '1+ var))) (setq x 0) (my-inc x) ;; ==> 1我们发现宏与函数的一个不同点,函数中代码在函数被调用时执行,并且参数是在调用时进行求值并传入。而宏调用时需要展开它返回的表达式(或者这里直接就是一个list)。然后将参数作为符号传入。宏最后需要返回一段可执行的list数据,如果没有返回,会影响展开执行,最终可能会报错,例如下面的例子(defmacro my-inc (var) (setq var (1+ var))) (setq x 0) (my-inc x) ;; ==> error这里的问题在于这个宏定义的代码是一个直接执行的代码,并不是一个list,所以在调用它的时候会直接执行,但是又需要将参数作为符号绑定,所以它在被调用的时候会执行(setq 'x (1+ 'x)) 这段代码,而这里的x是一个符号,无法直接对符号进行赋值,所以它会报x的类型错误。这里已经显示出了,elisp中的宏与C/C++中宏的不同。首先C/C++中的宏只是简单的字符串替换,可以将它理解为它生成了新的C/C++源码的代码,它在预处理阶段来执行代码的替换。而elisp中并没有简单的进行替换,根据之前介绍lisp表达式的解析,其实宏返回的是一颗抽象语法树。在扩展宏的时候不断的进行抽象语法树的修改和重建,最后在执行的时候将传入的参数作为符号放入到这颗树中的对应节点。我们可以使用 macroexpand 来查看宏展开的样子。(defmacro bad-inc (var) (setq var (1+ var))) (macroexpand '(bad-inc 0)) ;; ==> 1我们发现之前错误的实现并没有生成可执行的代码,而是直接返回一个常数。因为宏中的代码首先在展开的时候就已经执行了。相当于返回了 setq var (1+ 0) 的值,也就是1。(defmacro my-inc (var) (list 'setq var (list '1+ var))) (macroexpand '(my-inc x)) ;; ==> (setq x (1+ x))使用 macroexpand 可以使宏的编写变得容易一些。但是如果不能进行 debug 是很不方便的。在宏定义里可以引入 declare 表达式,它可以增加一些信息。目前只支持两类声明:debug 和 indent。debug 可选择的类型很多,具体参考 info elisp - Edebug 一章,一般情况下用 t 就足够了。indent 的类型比较简单,它可以使用这样几种类型:nil 也就是一般的方式缩进defun 类似 def 的结构,把第二行作为主体,对主体里的表达式使用同样的缩进整数 表示从第 n 个表达式后作为主体。比如 if 设置为 2,而 when 设置为 1符号 这个是最坏情况,你要写一个函数自己处理缩进。从前面的例子就可以看到,如果在定义宏的时候使用list cons 等来构建list是非常麻烦的,一旦要构造非常复杂的程序,可能直接就歇菜了。为了方便,elisp中提供了一些符号来简化操作。 ` 读作backquote,表示被它包裹的表达式都是quote,可以理解为它里面的直接构建了一个list如果希望它里面的某个位置不作为quote的一部分,而是直接作为列表的元素,可以使用 , , 也就是它会对后面的内容进行求值如果要让一个列表作为整个列表的一部分(slice),可以用 ",@",它会将后面的内容作为列表参数依次添加到当前列表中。我想起来了之前接触过的quote,也就是 ' 。它表示后面的内容不进行求值,作为符号,虽然它也可以构造一个list,但是二者还是有些不同,例如'(list x (+ 1 2)) ;; ==> (list x (+ 1 2)) `(list x (+ 1 2)) ;; ==> (list x (+ 1 2)) '(list x ,(+ 1 2)) ;; ==> (list x (\, (+ 1 2))) `(list x ,(+ 1 2)) ;; ==> (list x 3) (setq var '(2 3)) '(list x ,@var) ;; ==> (list x (\,@ var)) `(list x ,@var) ;; ==> (list x 2 3)我们使用上面的方法稍微弄一个复杂一点的宏(defmacro max(a b) `(if (> ,a ,b) ,a ,b)) (max 4 5) (max (1+ 2) (+ 3 6)) '(macroexpand '(max (1+ 2) (+ 3 6))) ;; ==> (if (> (1+ 2) (+ 3 6)) (1+ 2) (+ 3 6))这里是一个经典的C/C++ 中的max宏。虽然实现不严谨,有一些副作用,但是可以从上面看到一些用法。首先使用 ` 表示返回一个列表,以供调用的时候进行展开。再者对于传入的a和b需要使用, 来表示需要求解它们的值,实现参数的绑定,否则将会得到一个错误,例如(defmacro max(a b) `(if (> a b) a b)) (macroexpand '(max 4 5))) ;; ==> (if (> a b) a + b)如果将上述的 , 全部替换成 ,@ 就不太合适了,因为 ,@ 是将列表中的值取出来组成新的列表,并不会想 , 那样进行求值。例如(defmacro max(a b) `(if (> ,@a ,@b) ,@a ,@b)) (macroexpand '(max (1+ 2) (+ 3 6))) ;; ==> (if (> 1+ 2 + 3 6) 1+ 2 + 3 6)命令emacs 运行时就是处于一个命令循环中,不断从用户那得到按键序列,然后调用对应命令来执行。emacs 中的命令可以说就是一个函数,它是一个特殊的函数,是里面包含了 interactive 表达式的函数。这个表达式指明了这个命令的参数。比如下面这个命令(defun say-hello (name) (interactive "swhat's your name:") (message "hello, %s" name))当解释器加载了该函数之后就可以使用 M-x 来调用这个函数。我们根据提示输入一个名字,emacs会在minibuffer中输出一段话。我们发现,在interactive 表达式后面跟的字符串前面多了一个 s 字符。我们可以通过这个多加的字符来控制命令参数的类型和行为,例如使用 s 表示字符串参数,n 表示数字参数,f 代表文件,r 代表区域。interactive 的字符十分复杂,而且繁多。用的时候看 interactive 函数的文档还是很有必要的。但是不是所有时候都参数类型都能使用代码字符,而且一个好的命令,应该尽可能的让提供默认参数以让用户少花时间在输入参数上,这时,就有可能要自己定制参数。首先学习和代码字符等价的几个函数。s 对应的函数是 read-string,n 代表的是 read-file,f代表的是 read-file-name。其实大部分代码字符都是有这样对应的函数或替换的方法。我们可以使用这些方法来替代前面的代码字符,假如传入的是一个表达式,那么对表达式进行计算之后返回的列表元素就是命令的参数,例如我们用 read-string 来代替之前例子中的s。(defun say-hello (name) (interactive (list (read-string "what's your name: "))) (message "hello, %s" name))教程 中还列举了一些常见的字符代表的函数,这里我就不列出来了。各位读者有兴趣的话也可以去看看。
2025年02月19日
4 阅读
0 评论
0 点赞
2025-02-11
Emacs 折腾日记(十二)——变量
本文是依据 emacs lisp 简明教程 而来在此之前我们已经了解了elisp中的全局变量和函数中的局部变量,也了解了elisp中各种数据类型。这一篇主要谈谈elisp中各种变量的生命周期和作用域let 绑定的变量使用let绑定的变量只在let范围内有效,如果是多层嵌套的let,只有最里层的那个变量是有效的,用 setq 改变的也只是最里层的变量,而不影响外层的变量。比如(progn (setq foo "I'm global variable!") (let ((foo 5)) (message "foo value is: %S" foo) ;; ==> "foo value is: 5" (let (foo) (setq foo "I'm local variable!") (message foo)) ;; ==> "i’m local variable!" (message "foo value is still: %S" foo)) ;; ==> "foo value is still: 5" (message foo)) ;; ==> "i’m global variable!"这个有点像C/C++中的{} 定义的语句块中的变量只在当前 {} 内有效,当内部变量与外部变量重名的时候只影响{}内,而不影响外部。我们可以给出这样的c++代码int n = 0; printf("n = %d\n", n); { int n = 10; printf("n = %d\n", n); } printf("n = %d\n", n);elisp中的let两边的括号就有点像这里的 {},出了这个范围定义的变量就无效了。但是定义变量使用的是栈空间,而程序的栈空间是有大小限制的,一旦超过这个范围就会发生栈溢出。在C/C++程序中一般发生在递归层数过大。我们可以在编译时修改这个栈空间的大小。在elisp中也有变量控制递归层数,它就是 max-specpdl-size 但是在最新的29中已经将它弃用,新版的emacs可以使用 max-lisp-eval-depth 来限制specpdl堆栈的大小,该堆栈主要用于 存储动态变量绑定和 unwind-protect 激活等。buffer-local 变量顾名思义,它的值只在当前buffer中生效,在其他buffer中可能是另外的值。它有点像之前介绍的vim中的setlocal变量,仅在当前缓冲区内生效。该特性常用于实现缓冲区特定的配置和行为。声明一个 buffer-local 的变量可以用 make-variable-buffer-local 或用 make-local-variable。这两个函数的区别在于前者是在所有缓冲区都创建一个 buffer-local 的变量。而后者只在声明时所在的缓冲区内产生一个局部变量,而其它缓冲区仍然使用的是全局变量。一般来说推荐使用 make-local-variable。下面来举例说明它们的区别,在举例之前介绍例子中用到的函数或者宏with-current-buffer, 它的使用方式是(with-current-buffer buffer body)其中 buffer 可以是一个缓冲区对象,也可以是缓冲区的名字。它的作用是使其中的 body 表达式在指定的缓冲区里执行。default-value 可以访问符号所对应的全局变量下面是使用 make-local-variable 创建buffer-local变量的例子(setq foo "i'm a global variable!") (make-local-variable 'foo) foo ;; ==> "i'm a global variable!" (setq foo "i'm buffer-local variable in scratch buffer") foo ;; ==> "i'm buffer-local variable in scratch buffer" (with-current-buffer "*Messages*" (progn (message "%s" foo) ;; ==> "i'm a global variable!" (setq foo "i'm buffer-local variable in message buffer") (message "%s" foo))) ;; ==> "i'm buffer-local variable in message buffer" (default-value 'foo) ;; ==> i'm buffer-local variable in message buffer上述代码因为message buffer 中未定义foo的buffer-local 变量,所以它修改的是全局变量的值,我们使用 default-value 发现全局变量的值被修改了。(setq foo "i'm a global variable!") (make-variable-buffer-local 'foo) foo (setq foo "i'm buffer-local variable in scratch buffer") foo (with-current-buffer "*Messages*" (progn (message "%s" foo) (setq foo "i'm buffer-local variable in message buffer") (message "%s" foo))) (default-value 'foo) ;; ==> "i'm a global variable!"前面的结果都一样,但是我们关注一下最后输出的全局的 foo 变量,它的值没有被修改,而使用 make-local-variable 的时候它被修改了。这是因为 make-variable-buffer-local 会在每一个缓冲区内都创建一个 buffer-local 的拷贝,所以后面在 message 缓冲区中修改的是缓冲区内自己的 buffer-local 变量而不影响全局变量的值,而 make-local-variable则不同,它会在 message 缓冲区中为foo也创建一个buffer-local变量。message 缓冲区中修改的是 buffer-local 变量,不影响全局的foo变量。一般来说根据实际情况选择使用哪种,我并不是资深的elisp开发者,以我浅薄的认知,一般使用 make-local-variable情况较多,它影响面较小,仅仅影响当前缓冲区。而make-variable-buffer-local 影响面较大,一旦使用它设置了local-buffer 变量,那么在后面其他缓冲区中想要使用 setq 设置全局的值就没那么简单了。这种一般是需要隐藏起来的核心变量,例如某些功能依靠这个变量来驱动,一旦修改了可能导致后面的代码运行行为不准确。可能会使用 make-variable-buffer-local 定义,不让用户自己随便修改全局的值。我们可以使用 setq-default 来设置全局变量的值。这里的例子就不给出了,各位读者可以根据上面的例子稍加修改就可以了。测试一个变量是不是 buffer-local 可以用 local-variable-p。这里我们再介绍一个新的函数 get-buffer。它需要一个buffer的名称作为参数,返回这个buffer的对象,如果未找到对应的buffer,则返回nil。(setq foo 5) (make-local-variable 'foo) (local-variable-p 'foo) ;; ==> t (local-variable-p 'foo (get-buffer "*Messages*")) ;; ==> nil如果要在当前缓冲区里得到其它缓冲区的 buffer-local 变量的值可以用 buffer-local-value(setq foo "i'm a global variable!") (make-local-variable 'foo) (setq foo "i'm buffer-local variable in scratch buffer") (with-current-buffer "*Messages*" (buffer-local-value 'foo (get-buffer "*scratch*"))) ;; ==> "i'm buffer-local variable in scratch buffer"变量的作用域在之前我们已经介绍过几种变量,分别是使用 setq 或者 defvar 定义的变量,它们是全局变量,即使在一些语法块或者函数中定义的,在外围也可以正常访问使用 let 或者 let* 定义的变量,只在 let 语法块中有效使用 make-local-variable 或者 make-variable-buffer-local 定义的buffer-local 变量,在当前buffer中有效但是函数参数列表的变量生命周期与我们平常在C/C++、Java、Python等语言中有些不同。作用域(scope)是指变量在代码中能够访问的位置。emacs lisp 这种绑定称为 indefinite scope。indefinite scope 也就是说可以在任何位置都可能访问一个变量名。而 lexical scope(词法作用域)指局部变量只能作用在函数中和一个块里(block)。比如 let 绑定和函数参数列表的变量在整个表达式内都是可见的,这有别于其它语言词法作用域的变量。先看下面这个例子(defun foo(x) (getx)) (defun getx() x) (message "%s" (foo "hello,x")) ;; ==> "hello,x"我们可以看到最终成功输出了结果,而根据之前学习C/C++的经验,在C、C++等语言中,这样的代码是无法执行成功的,因为在getx中并未定义x的值。但是在elisp 中,foo函数执行期间,x变量的都是有效的且可以正常访问到的。当然,在elisp中,let也具有这一效果,在let的语法块中,定义的变量总是有效的。例如(let ((x "hello, x")) (message "%s" (getx))) (defun getx() x)在let语句块中 x 是一直有效的,如果脱离let,在最外层调用 getx 将会得到一个x未定义的错误需要注意的是,上面的例子无法再使用 C-x C-e 来一条条的执行了,这个时候需要使用 eval-buffer 来执行整个缓冲区的代码。emacs 从 24.1 版本开始,引入了 lexical binding 这一特性,在代码中如果启用这个属性,那么它将采用C/C++ 等普通编程语言的那种 lexical scope 的作用形式。官方文档中提到使用 lexical binding 特性可以提高代码的运行效率,并且鼓励使用。可以在代码文件最开始的位置添加这么一个注释 -*- lexical-binding: t -*- 来开启这一特性。上面的代码只要加上这一特性就能得到不一样的结果;; -*- lexical-binding: t -*- (let ((x "hello, x")) (message "%s" (getx))) (defun getx() x)这个时候执行将会得到一个错误信息 "Symbol’s value as variable is void: x",x这个符号是一个未定义的变量。生存期是指程序运行过程中,变量什么时候是有效的。全局变量和 buffer-local 变量都是始终存在的,前者只能当关闭emacs 或者用 unintern 从 obarray 里除去时才能消除。而 buffer-local 的变量也只能关闭缓冲区或者用 kill-local-variable 才会消失。而对于局部变量,elisp 使用的方式称为动态生存期:只有当绑定了这个变量的表达式运行时才是有效的。在 emacs lisp 简明教程 中举了一个闭包的例子,在elisp中并不支持闭包,它采用的是与普通编程语言一样的变量生存周期,这里我就不列出来了。JavaScript等语言中是支持闭包的,有兴趣的读者可以去看看JavaScript中的闭包。其他函数一个符号如果值为空,直接使用可能会产生一个错误。可以用 boundp 来测试一个变量是否有定义。这通常用于 elisp 扩展的移植(用于不同版本或 XEmacs)。对于一个 buffer-local 变量,它的缺省值可能是没有定义的,这时用 default-value 函数可能会出错。这时就先用 default-boundp 先进行测试。使一个变量的值重新为空,可以用 makunbound。要消除一个 buffer-local 变量用函数 kill-local-variable。可以用 kill-all-local-variables 消除所有的 buffer-local 变量。但是有属性 permanent-local 的不会消除,带有这些标记的变量一般都是和缓冲区模式无关的,比如输入法。(setq foo "I'm local variable!") foo ; ==> "I'm local variable!" (boundp 'foo) ; ==> t (default-boundp 'foo) ; ==> t (with-current-buffer "*Messages*" (boundp 'foo)) ; ==> t (makunbound 'foo) ; ==> foo foo ; This will signal an error (boundp 'foo) ; ==> t (default-boundp 'foo) ; ==> t (kill-local-variable 'foo) ; ==> foo (with-current-buffer "*Messages*" (boundp 'foo)) ; ==> t上面的例子需要注意以下几点这里只是使变量的值为空,并没有消除这个变量的符号。所以在执行makunbound 之后,关于foo 是否绑定的测试都是tkill-local-variable 只是消除了foo作为buffer-local变量,并没有影响到全局变量,所以在messages-buffer中测试它仍然是有效的变量变量命名习惯对于变量的命名,有一些习惯,这样可以从变量名就能看出变量的用途:hook 一个在特定情况下调用的函数列表,比如关闭缓冲区时,进入某个模式时。function 值为一个函数functions 值为一个函数列表flag 值为 nil 或 non-nilpredicate 值是一个作判断的函数,返回 nil 或 non-nilprogram 或 -command 一个程序或 shell 命令名form 一个表达式forms 一个表达式列表。map 一个按键映射(keymap)
2025年02月11日
14 阅读
0 评论
0 点赞
2025-01-21
Emacs折腾日记(十一)——求值规则
截至到现在,我觉得我自己的elisp水平有了一定的提高,希望各位读者借助之前的文章也能有一些收获。现在已经可以尝试写一点elisp的程序了,但是如果想深入了解一下 lisp 是如何工作的,不妨先花些时间看看 lisp 的求值过程。对于我这样一个日常使用C/C++的程序员来说,习惯了C/C++的语法和写法,初次见到lisp这样使用括号并且主要是S-表达式的语言,开始总会有点不习惯,但是在尝试自己写了这么些文章之后,对lisp有那么一点感觉。这篇我想就着 求值规则 这篇文章以及自己的一些理解来尝试梳理一下自己是如何理解elisp表达式的。S表达式要理解S表达式,我们先从如何解析四则运算开始。在之前我鸽了一个系列就是使用C来实现C语言解析器的系列。在那个系列中提到,一个普通的4则运算最终会生成一个抽象语法树,例如 a * b - (c + d) 最终可以生成如下的抽象语法树 - / \ * + / \ / \ a b c d二叉树的每个节点,或者是叶节点,或者有2个子节点,叶节点可以用来存储数据。而每颗子树的根节点存储操作符,或者说表示要对数据进行的操作,而如果操作符需要一个或者多个操作数,那么可以对抽象语法树进行调整。可以用上面的图来表示树的话有些麻烦了,后来发明了点对表示法, 如果只关心叶子节点,每颗子树的根节点采用.来表示,那么这颗二叉树可以表示为 ((a . b) . (c . d)) 。看到这里各位读者想到了什么呢?cons cell。S表达式是点对表示法的形式定义:原子 -> 数字 | 符号 S表达式 -> 原子 | (S表达式 . S表达式)所以,S表达式或者是原子,或者是递归的由其他S表达式构成的点对。虽然抽象语法树可以使用这种点对来描述,但是语法树大了,点的数量大了,其实也挺麻烦的,所以lisp中有一些简单的写法。回顾一下之前学习列表和cons cell的知识,简化也就得到了列表,例如'((a . b) . (c . d)) ⇒ ((a . b) c . d) '((a . b) . (c . (d . nil))) ⇒ ((a . b) c d)如果我们考虑这颗树的根节点,并且采用先序遍历的方式访问,结果仍然采用点对来表示,那么将得到这样的结果 (- (* a b) (+ c d))。 这样我们得到了计算这个四则运算的lisp代码,这个它可以作为列表,也可以让lisp解释器来执行。到此为止,各位读者应该理解了S表达式。它其实就是对应了一颗语法树。现在看到S表达式也不那么恐惧了,解释器如何执行它似乎也慢慢的清晰起来了呢S表达式的求值理解了S表达式,再回过头来看看它的求值过程。所有的表达式可以分为三种:符号、列表和其它类型。我们来分别说明最简单的就是自求值表达式,前面说过数字、字符串、向量都是自求值表达式。还有两个特殊的符号 t 和 nil 也可以看成是自求值表达式。第二种表达式是符号。符号的求值结果就是符号的值。如果它没有值,就会出现 void-variable 的错误。第三种表达式是列表表达式。而列表表达式又可以根据第一个元素分为函数调用、宏调用和特殊表达式(special form)三种。根据上面对S表达式的理解,这里的第一个元素也就是放在语法树的每颗子树的根节点上,表示对它的子节点进行的操作,例如上面的加减乘除,或者使用car之类的函数。而它的子节点可以是一颗语法树,也可以是简单的值,对应在elisp中的话,就是这个操作可以针对上面两种自求值表达式或者符号值,也可以是另一个S表达式。整个求值过程就是不断的求子树然后使用根节点来对子树进行操作,例如针对上面的二叉树可以写下这么一段伪代码来实现求值float calc-ast(ast* pRoot) { switch(pRoot->eOprType) { case function: //函数调用 return function(calc-ast(pRoot->left), calc-ast(pRoot->right)); case math: // 数学计算 return calc-ast(pRoot->left) + calc-ast(pRoot->right); //这里以加法为例 .... default: calc-ast(pRoot->left); calc-ast(pRoot->right); break } }第一个元素如果是一个特殊表达式时,它的参数可能并不会全求值。这些特殊表达式通常是用于控制结构或者变量绑定。每个特殊表达式都有对应的求值规则。这个就根据具体的语法来定,例如 and 和 or 这些操作符具有短路的特性。本文内容到此就结束了,本文比较简单,算是对之前的一个总结,对lisp有一个大概的了解。最后,本文可能是年前最后一篇文章了,在这里提前祝各位读者新年快乐!
2025年01月21日
8 阅读
0 评论
0 点赞
2025-01-20
Emacs 折腾日记(十)——elisp符号
符号是有名字的对象,这么说可能有点抽象。我们先来回忆一下C/C++中关于符号的内容。C/C++ 最终被编译成机器码直接执行,在机器码中不存在变量名称,函数名称等字符,它只有一串地址。但是在写C/C++代码的时候有变量名,函数名,类名,对象名等等名称。编译器是如何做到将符号和地址关联起来的呢?答案是,编译器在编译阶段会提供一个符号表,符号表如何实现的我也不太清楚,但是它做到了关联一个地址和字符串符号的作用。在Windows平台的 vs下,debug版本一般会生成与exe同名的pdb文件,这个是调试文件,它里面保存了一些调试信息,包括符号表。这里的符号与C/C++中的符号表中的符号类似,可以通过它来找到具体的变量。可以理解成elisp提供了这么一种操作符号表的功能。首先必须知道的是符号的命名规则。符号名字可以含有任何字符。与之对应的,一般的编程语言在定义变量的时候有些特殊符号不能用,而且不能以数字开头,有些关键字也不能作为变量名。而elisp中没有这些限制,只是在使用特殊符号的时候需要使用转义字符。(symbolp '+1) ;; ==> nil (symbolp '\+1) ;; ==> t (symbol-name '+1) ;; error (symbol-name '\+1) ;; ==> "+1"上面的代码 symbolp 是在判断一个对象是否是一个符号的函数,symbol-name 用于取符号的名称,它会返回给定符号的名称的字符串。上面的代码说明了,elisp中符号没有什么特别要求,只是对于特定的字符需要使用转义字符。与c/c++中类似,elisp中的符号名也是区分大小写的。符号创建在C/C++中符号表由编译器来创建和操作。而elisp中则提供了操作符号表的方式。符号名要有唯一性,所以一定会有一个表与名字关联,这个表在 elisp 里称为 obarray。从这个名字可以看出这个表是用数组类型,事实上它是一个向量。对于一个新的符号,解释器会首先取字符串的hash值,根据hash值来放入数组对应的位置。同时我们也将这种保存符号的数据结构称之为obarray。也就是说obarray不仅是一个保存符号的变量,也是一种结构。我们也可以在符号上建立这么一个结构用来保存符号对应的属性。也可以作为参数传入emacs 相关函数中当 elisp 读入一个符号时,通常会先查找这个符号是否在 obarray 里出现过,如果没有则会把这个符号加入到 obarray 里。这样查找并加入一个符号的过程称为是 intern。intern 函数可以查找或加入一个名字到 obarray 里,返回对应的符号。默认是全局的obarray,也可以指定一个 obarray。intern-soft 与 intern 不同的是,当名字不在 obarray 里时,intern-soft 会返回 nil,而 intern 会加入到 obarray里。(setq foo (make-vector 10 0)) ;; ==> [0 0 0 0 0 0 0 0 0 0] (intern-soft "abc" foo) ;; ==> nil (intern "abc" foo) ;; ==> abc (intern-soft "abc") ;; ==> abc (intern-soft "abc") ;; ==> nillisp 每读入一个符号都会 intern 到 obarray 里,如果想避免,可以用在符号名前加上 #:(intern-soft "abc") ;; ==> nil 'abc (intern-soft "abc") ;; ==> abc '#:abcd ;; ==> abcd (intern-soft "abcd") ;; ==> nil可以使用 untern 从obarray中删除对应的符号,如果成功删除,会返回t,如果没有对应的符号,则会返回 nil(intern "abc") ;; ==> abc (intern-soft "abc") ;; ==> abc (unintern "abc") ;; ==> t (intern-soft "abc") ;; ==> nil(setq foo 1) (intern-soft "foo") (unintern "foo") (intern-soft "foo") (1+ foo) ;; error通过setq,我们让elisp将foo这个变量放入到obarray中,后续使用unintern 删除这个变量后再使用foo的时候就会报错,foo是一个空变量与hash-table一样,obarray 也提供一个mapatom 函数来遍历整个obarray,例如下面是一个计算所有符号数量的例子(setq count 0) (defun count-sys(s) (setq count (1+ count))) (mapatoms 'count-sys) count ;; ==> 95733 (length obarray) ;; ==> 15121这里我们看到,数组的长度小于符号的数量。这根hash-table的实现是一样的。各位读者在学习hash-table的时候应该了解过,hash-table 中 hash值不同的元素存储在数组的不同位置,相同的元素通过链表进行串联,一般的hash-table在内存中的结构如下图.符号的组成在计算机中,所有的内容都是使用二进制来进行存储的,我们人为的将二进制数据划分为代码和数据。如果单纯的给出一个内存的地址,如何知道它是数据还是代码呢?例如在C/C++中定义了一个int类型的变量a,为什么在后续使用a(1) 这样的语句会报错呢?又例如一个函数指针 pfn,使用 *pfn = 1 这样也会报错呢?编译器怎么知道哪个地址存的是变量,哪个地址存的是函数指针呢?还是通过符号表来解决,符号表中针对每个符号名称都会给定它的类型,例如这个符号对应的地址是一个整数,或者指针,又或者是函数指针。符号名称类比C/C++ 中的符号表,elisp中每个符号都可以有4个组成部分,一个是符号名称,它可以类比到符号表中的名称,可以用symbol-name 来访问。它返回一个符号的字符串名称,关于使用的例子在最开始已经给出了。符号值第二个组成部分是符号值,可以类比成普通变量的值,可以通过set函数来设置,通过 symbol-value 来获取。(set 'abc "i am abc") ;; ==> "i am abc" (symbol-value 'abc) ;; ==> "i am abc" abc ;; ==> "i am abc"set 以及 symbol-value 需要提供一个符号,表示对哪个符号进行操作。解释器执行第一行代码的时候未发现 abc 这个符号,那么它会将abc放入符号表中,然后这个符号就可以作为普通变量来使用了。最后一行代码我们直接将它作为普通变量那样使用,直接对它进行求值,发现它也可以获取到具体的值我们使用 setq 也可以达到这样的效果(setq val 123) ;; ==> 123 (symbol-value 'val) ;; ==> 123 val ;; ==>123 (set 'val 1234) ;; ==> 1234 (symbol-value 'val) ;; ==> 1234 val ;; ==> 1234从上面的代码中发现,setq 直接使用变量名来对变量进行赋值,而set 则需要对符号进行quote操作。我们可以将setq 看做是一个宏(至于宏是什么,会在后面进行介绍),也就是 set quote,自动将后面的符号进行quote操作。但是setq只能对全局的obarray中的符号进行赋值,如果我们想放到指定的obarray中进行,此时就不能使用setq 了(setq foo (make-vector 10 0)) (set (intern "value" foo) 123) (symbol-value 'value) ;; ==> error (symbol-value (intern-soft "value" foo)) ;; ==> 123set 和 symbol-value 没有直接的参数来指定符号所在的obarray,如果想要使用自定义的obarray,那么就需要借助 intern、intern-soft、这样可以指定obarray 的函数来进行辅助操作。如果一个符号的值已经有设置过的话,则 boundp 测试返回 t,否则为 nil。对于 boundp 测试返回 nil 的符号,使用符号的值会引起一个变量值为 void 的错误(intern "null") (boundp 'null) ;; ==> nil null ;; error (set 'null 123) (boundp 'null) ;; ==> t null ;;==> 123函数第三个组成部分是函数,它可以类比成函数指针, 它可以用 symbol-function 来访问,用 fset 来设置。在之前一篇文章中,有知乎大牛指出我的问题,根据大牛的描述,在绑定lambda表达式时将函数部分绑定到符号的函数部分,使用funcall 调用的时候是在取函数部分的内容执行。这里详细了解符号相关的知识之后上述表达就很容易理解了。(setq foo (make-vector 10 0)) (fset (intern "abc" foo) (lambda (name) message "hello,%s" name)) (funcall (intern-soft "abc" foo) "Emacs") ;; ==> error上述的代码会报告一个错误,因为这里我们使用的obarray 是自定义的foo,它里面没有message这个符号,当然我们可以使用 intern来获取全局的 message 函数,并将它放入到foo中。这里的代码可以这么改(fset (intern "abc" foo) (lambda (name) (funcall (intern-soft "message") "hello,%s" name))) (funcall (intern-soft "abc" foo) "Emacs") ;; ==> "hello,Emacs"类似的,可以用 fboundp 测试一个符号的函数部分是否有设置。(fboundp 'message) (fboundp (intern-soft "message" foo)) ;; ==> nil (fset (intern "message" foo) (symbol-function 'message)) (fboundp (intern-soft "message" foo)) ;; ==>t属性列表第4个组成部分是属性列表,关于这部分我暂时还没想到该怎么用C/C++进行类比,如果非要一个类比的话,可以用这个类比。C/C++的编译器在看待变量的时候是将变量转变成对应的内存地址,操作变量实际上就是在操作变量所对应的内存。从CPU的角度来讲,CPU并没有规定哪些内存是只读的,哪些是数据,哪些是代码。编译器是如何做的呢?答案应该是编译器会在符号表中对各个符号做一些标记,例如const型变量所对应的内存不能修改。具体编译器是如何实现我也不太清楚,先这么生搬硬套吧,至少在了解elisp的符号这块,这么理解可能会稍微具体一点elisp中的属性列表,用于存储和符号相关的信息,比如变量和函数的文档,定义的文件名和位置,语法类型。属性名和值可以是任意的 lisp 对象,但是通常名字是符号,可以用 get 和 put 来访问和修改属性值,用 symbol-plist 得到所有的属性列表:(put (intern "abc" foo) 'doc "this is abc") (get (intern-soft "abc" foo) 'doc) ;; ==> "this is abc" (symbol-plist (intern-soft "abc" foo)) ;; ==> (doc "this is abc")符号的属性列表在内部表示上是用(prop1 value1 prop2 value2 ...) 的形式, 在存取上有点像C/C++中的map,但是在elisp中并不是所谓的map结构。另外还可以用 plist-get 和 plist-put 的方法来访问和设置属性列表,在上一段代码的基础之上(也就是设置了符号 abc 的 doc 属性的前提下),使用如下代码来进行测试(plist-get (symbol-plist (intern-soft "abc" foo)) 'doc) ;; ==> "this is abc" (plist-put (symbol-plist (intern-soft "abc" foo)) 'foo 69) (get (intern-soft "abc" foo) 'foo) ;; ==> 69 (setq my-plist '(doc "this is abc")) ;; ==> "this is abc" (plist-put my-plist 'foo 89) (plist-get my-plist 'doc) ;; ==> "this is abc" (plist-get my-plist 'foo) (get (intern-soft "abc" foo) 'foo) ;; ==> 69从上面的代码来看,plist-get 和 plist-put 需要一个额外的属性列表的操作表示要操作的属性列表,但是它也可以通过传入符号的真实属性列表直接来操作符号的属性列表。
2025年01月20日
7 阅读
0 评论
0 点赞
2025-01-15
Emacs 折腾日记(九)——elisp 数组与序列
elisp 中序列是数组和列表的统称,序列的共性是内部数据有一个先后的顺序,它与C/C++ 中有序列表类似。elisp 中的数组包括向量、字符串、char-table 和布尔向量,它们的关系如下:在之前一章中已经介绍了序列中的一种类型——列表,本篇将介绍序列中的另外一种数据类型——数组数组简介与C/C++ 中的数组类似,elisp中的数组有如下特征在创建之初给定长度之后不允许后期修改长度数组中的每个元素都可以通过索引来获取,并且获取的算法时间复杂度为O(1)数组是自求值的数组中的的元素可以通过 aref 来获得,并且通过aset 来设置值根据上图,向量是数组中的一种。字符串也是特殊的数组,它是内部全部都是字符的数组(虽然elisp中没有字符这种数据类型)。教程中没有介绍 char-table 和 bool-vector,所以这里我也不打算介绍,后面要是真遇到了再看。测试函数测试函数是用同名带p的函数来进行测试,例如 sequencep 来测试是否是一个序列,stringp 测试是否是一个字符串, vectorp 测试是否是一个向量,arrayp 测试是否是一个数组。char-table-p 和 bool-vector-p 分别测试对象是否是 char-table、bool-vector(arrayp [1 2 3]) ;; ==> t (vectorp [1 2 3]) ;; ==> t (stringp [?A ?B ?C]) ;; ==> nil (stringp "ABC") ;; ==>t (vectorp "ABC") ;; ==> nil (arrayp "ABC") ;; ==> t通过上面的测试发现字符串和向量是不同的类型,但是字符串也是一种数组上述代码创建了一个向量,然后判断向量是否是一个数组。在elisp中向量也是数组的一种,所以这里返回t序列的通用函数在字符串中提到过,可以使用 length 来获取字符串的长度。其实它是一个序列的函数,它可以获取序列的长度。对于列表来说,它只能获取真列表的长度,对于点列表它会报错,而对于循环列表则会陷入死循环。它的算法应该是跟C/C++ 中获取链表的长度的算法一样,根据最后一个节点的next指针域是否为空来进行判断。对于点列表和循环列表,可以使用 safe-length 来获取,从名称上看,它是一个安全的获取长度的函数。(length [1 2 3 4]) ;; ==> 4 (safe-length '(1 2 3 4)) ;; ==> 4 (safe-length '(1 2 3 . 4)) ;; ==>3这里不要疑惑为什么第二个参数表达式返回的结果会是3,表达式中列表真正的表达形式应该是'(1 (2 (3 . 4)))虽然写法上使用 (1 2 3 . 4) 比较清爽干净也容易理解,但是要时刻记住它真正的形式应该是多个cons cell组成。插一个题外话,不知道各位读者还记不记得当初在学数据结构时,学到的如何判断环形链表的算法。那个算法被叫做两个指针跑步法,脱胎于小学时学的一道数学题;“在一个环形跑道,小明以每秒1米的速度匀速跑,小华以每秒2米的速度匀速跑,多久之后小明落后小华一圈”。这个算法也是这样的,一个慢指针每次往后移动一个节点,一个快指针一次移动两个节点,下一次两个指针能相遇,那么它就是一个环形列表。根据这个算法我们也可以提供一个lisp版本的判断环形列表的代码(defun circle-list-p (list) (and (consp list) (circle-list-p-1 (cdr list) (cdr (cdr list))))) (defun circle-list-p-1 (slow fast) (if (or (null slow) (null fast)) nil (if (not (consp slow)) nil (if (eq (car fast) (car slow)) t (circle-list-p-1 (cdr slow) (cdr (cdr fast))))))) (circle-list-p '(1 2 3 4)) ;; ==> nil (circle-list-p '#1=(1 2 . #1#)) ;; ==> t获取序列的第n个元素可以使用 elt,但是对于已知数据类型最好使用对应的函数,例如针对列表应该使用 nth,数组使用 aref。一来该对象是何种数据类型更加直观,二来省去了 elt 内部类型判断的操作。copy-sequence 在前面已经提到了。不过同样 copy-sequence 不能用于点列表和环形列表。对于点列表可以用 copy-tree 函数。环形列表就没有办法复制了。 好在这样的数据结构很少用到。数组操作创建向量可以使用 vector 函数,或者使用[], 来包裹一组数据,后者是向量的读入语法(vector 1 2 3) ;; ==> [1 2 3] (setq foo '(a b)) [foo] ;; ==> [foo] (vector 'foo) ;; ==> [foo] (vector foo 1 2 3) ;; ==> [(a b) 1 2 3]上述代码中我们使用两个方式分别构造一个向量。采用vector的时候会对其中的每个符号进行求值。而使用[]来构造时则没有进行求值,等效于使用 quote使用 make-vector 可以生成元素相同的向量(make-vector 9 "foo") ;; ==> ["foo" "foo" "foo" "foo" "foo" "foo" "foo" "foo" "foo"]fillarray 可以将数组的每个元素使用对应值进行填充(fillarray [1 2 3 4] 'foo) ;; ==> [foo foo foo foo]aref 和 aset 可以访问和设置数组中对应索引的元素。但是需要注意数组的长度,如果传入索引超过数组长度则会报错。可以使用 vconcat 可以将多个序列合并成一个向量,这里可以传入非向量,例如传入列表。针对列表仅限真列表。(vconcat [1 2 3] [3 4 5]) ;; ==> [1 2 3 3 4 5] (vconcat [1 2 3] '(3 4 5)) ;; ==> [1 2 3 3 4 5] (vconcat [1 2 3] '(4 . 5)) ;; ==> error将向量转化成列表可以使用 append函数(append [a b]) ;; ==> [a b] (append [a b] nil) ;; ==> (a b) (append [a b] '(c)) ;; ==> (a b c) (append [a b] "cd") ;; ==> (a b . "cd") (append [a b] "cd" nil) ;; ==> (a b "cd")在列表那一章中,append是将列表的最后一个节点的cdr替换为对应参数。在这里它可以将序列的元素转化为列表,但是需要注意,转换时同样需要两个参数。它会将第一个参数转化为列表,然后执行列表中添加元素的操作
2025年01月15日
7 阅读
0 评论
0 点赞
2025-01-13
Emacs 折腾日记(八)——CONS CELL和列表
本篇我们来介绍emacs lisp中的第一种复核结构——列表类型。cons cell从概念上讲 cons cell 非常简单,就是两个有顺序的元素。第一个元素叫 CAR、第二个元素叫 CDR。CAR 和 CDR 名字来自于 Lisp。根据 emacs lisp 简明教程 上的说法:它最初在IBM 704机器上的实现。在这种机器有一种取址模式,使人可以访问一个存储地址中的“地址(address)”部分和“减量(decrement)”部分。CAR 指令用于取出地址部分,表示(Contents of Address part of Register),CDR 指令用于取出地址的减量部分(Contents of the Decrement part of Register)。cons cell 也就是 construction of cells。至于历史出处我们并不需要特别关心,也不用掌握,我们只需要掌握相关用法即可。其实我们可以将它想象成一个有两个抽屉的柜子,有一个抽屉叫 car 另一个叫cdr 。具体里面放什么东西没有限制,可以放基本的数据类型,也可以同样的放入这么一个柜子。首先使用 cons 来构建一个cons cell。例如(setq my-cons (cons 1 "hello")) ;; ⇒ (1 . "hello") (setq my-cons (cons 1 nil)) ;; ⇒ (1)因为一个cons cell 包含 car 和 cdr 两个元素,所以一般我们传入的时候需要两个参数。但是第二个参数可以为nil。根据emacs在mini-buffer上的输出,其实还可以使用另一种方式来构建一个cons cell(setq my-cons '(1 . "hello"))我们看到在前面的代码中我们带了一个单引号,这个单引号用于表示符号(symbol)或字面量(literal)。具体来说,它的作用是防止后面的表达式被求值。Lisp的语句是一个S-表达式,在解释器读到到一个S-表达式的时候会尝试对这个S-表达式求值。在出现括号的表达式的时候,会将括号内第一个元素作为函数进行调用,而将其他元素作为参数。如果不加引号,那么上面的代码就变成了(setq my-cons (1 . "hello"))这个表达式的含义就是调用1 这个函数,传入 "hello" 参数,并将函数的返回值设置成变量 my-cons 的值。因为没有这么一个函数,所以它执行会报错。这里的引号就是 quote 函数,它用来表示对后面的内容不求值,仅仅作为一个符号传入。上面的代码也可以改成(setq my-cons (quote (1 . "hello")))再举一个例子(setq my-cons '(a . b));; error上述的代码会报错,虽然我们指定了 (a . b) 是一个符号,是一个cons cell,但是对于里面的 a 和 b 却没有指定,因此解释器会尝试解释 a和b,然后发现a和b未定义,所以也会报错,我们可以使用单引号单独的指定a、b都是符号,或者给a、b变量设定值。虽然都不报错,但是它们的含义却是不同的。(setq my-cons ('a . 'b)) (let ((a 1) (b 2)) (setq my-cons '(a . b)))cons cell还有一个特殊的值,那就是 nil 它表示一个空的 cons cell。它可以使用如下形式来给出nil '()空表并不是一个真正的 cons cell , 但是为了编程方便,还是可以通过 car 和 cdr 来取值,结果都是空。(car '()) ;; ⇒ nil (cdr '()) ;; ⇒ nil (car nil) ;; ⇒ nil列表lisp的全程是 List Processing ,列表处理,从这点上看列表在lisp中的比重非常重,非常重要。列表可以看作一个特殊形式的cons cell。在上面的介绍中,cons cell有两个元素,car和cdr,列表第一个元素是car,其余的是cdr。以此规律往下递归。我们可以使用 list 函数来构建(list 1 2 3) ;; ⇒ (1 2 3)也可以使用上面的 quote 来构造'(1 2 3) ;; ⇒ (1 2 3)二者定义的时候有什么区别呢?quote 方式是直接将内容作为一个列表,而list 函数则是先解释执行后面的代码,再将结果构建成列表,下面是二者不同的一个例子(list (+ 1 2 ) 3) ;; ⇒ (3 3) '((+ 1 2) 3) ;; ⇒ '((+ 1 2) 3)再来看一个例子'(a b c) ;; ⇒ (a b c) (list a b c) ;; ⇒ error, 因为a b c都未定义,无法解释执行如果要使用 list 来生成类似于 (a b c) 这样的列表,关键点在于要告诉解释器a b c 它们不需要解释执行,可以使用 quote 来做到这点(list 'a 'b 'c) ;; ⇒ (a b c)测试函数可以使用 consp 来判断一个对象是否是cons cell。使用 listp 来判断对象是否是列表,但是我们说列表是特殊的 cons cell 所以使用 consp 来检测列表,也会返回真(consp '(1 2 3)) ⇒ t除此之外,elisp 将cons cell也视为一种特殊的列表,因此下面的代码也返回t(listp (cons 1 2)) ;; ⇒ t但是nil 或者 '() 它们不是cons cell 也不是 list,所以判断它们都会返回 nil(consp nil) (consp '())深入理解 cons cell 和列表上面提到我们可以使用 cons 和 list 来分别构造一个 cons cell 和列表,但是它们构造一个新的,不影响之前的,例如(setq my-cons (cons 1 2)) (cons my-cons my-cons) my-cons ;; ⇒ (1 . 2)同时 cons 也可以在列表前增加一个元素,例如(setq foo '(a b)) (cons 'x foo) ;; ⇒ (x a b) foo ;; ⇒ (a b)从上面返回的结果来看,cons 会创建一个新的列表,并且在新列表的最前面加上指定元素,但是它不会修改原有的列表。cons 会返回新元素,不修改老元素还可以理解,因为它本来就是用来构建新的 cons cell 的。那么还有一个问题需要解释,为什么这样一个用来构建cons cell的函数会用来添加列表元素呢?要回答这个问题,我们可以需要回归到列表的本质了。先看这么一个例子'(1 . (2 . (3 . nil))) ;; ⇒ (1 2 3)我们执行它,发现它会返回一个列表,从这个例子上看,列表本身就是一个cons cell。它是一个特殊的cons cell 。按照列表最后一个 cdr 来区分的话,可以分成三类:第一类就是上述例子这样的,它的最后一个cdr是nil,它也被叫做真列表第二类,既不是cons cell也不是nil,这种被称之为点列表第三类,最后一个cdr 指向之前一个cons cell'(1 . #1=(2 3 . #1#)) ; => (1 2 3 . #1)这个是教程中给出的环形列表的表示形式,它比较复杂。但是它的结构与当初学过的数据结构中的环形链表类似。'(1 . (2 . (3 . 4))) ;; ⇒ (1 2 3 . 4)上述代码是第二类列表的形式,它的最后一个cdr 是 4,既不是cons cell 也不是nil。上述的代码中也可以看出来,并不是说有 . 的都是 cons cell,没有. 的就是列表。还是以前面的抽屉来类比,第二个抽屉里放的是nil或者其他基本数据类型,那么它就是一个 cons cell。如果放的是另外一个同样类型的抽屉,那么它就是一个列表。用数据结构中的概念来类比的话,cons cell是一个不带指针的结构体,而列表就是一个带有指向自身结构体类型的指针域。即使它只有一个这种结构的对象也是一个列表的节点。(cons 1 nil) ;; ⇒ (1)上述代码就是这样的,第二节点域指向空,没有指向下一个节点,虽然只有一个节点,但它也是一个列表。我个人的理解是,不应该严格区分cons cell 和列表,就像C/C++中的struct 和list,struct是组成list的基础,而list中每个节点又都是一个struct, 所以前面使用 consp 和 listp 无法区分cons cell 和 lisp。而. 则可以看作是分隔符,分隔数据域和指针域的数据,指针域同样可以放入其他类型的数据,也可以放入 cons cell列表的操作函数添加列表元素如果希望修改原始列表可以使用 push ,与栈操作类似,它是将当前值添加到列表头,例如(setq foo '(a b)) (push 'x foo) foo ;; ⇒ (x a b)在列表前面添加元素使用 cons ,在列表后面添加元素可以使用 append。(setq foo '(a b)) (append foo '(x)) ;; ⇒ (a b x) foo ;; ⇒ (a b)(setq foo '(a b)) (append foo 'x) ;; ⇒ (a b . x) foo ;; ⇒ (a b)(setq foo '(a . b)) (append foo 'x) ;; error foo与cons 类似,它同样不修改原始列表的值。 用上面C/C++结构体和链表的类比话术来说的话,它的作用是将第一个参数的最后一个节点的指针域的空指针替换成第二个参数。上面的第一个例子,原本列表应该是 (a . (b . nil)) 它的最后一节点的指针域就是 nil,它被替换成了 (x), 可以写成 (x . nil) 。最后的结果就是 (a . (b . (x . nil))) 它是一个真列表,(a b x) 。第二个例子,还是先将列表展开 (a . (b . nil)) ,将nil替换成 x ,最后的结果就是 (a . (b . x)) 第三个例子,使用cdr 取出来的最后一个例子并不是空,所以它会报错与C中链表类似,采用头插法的速度要比使用尾插法快得多。即使用 cons 速度要比使用 append 快获取列表元素列表就是一个个cons cell 串起来组成的,可以使用 car 和 cdr 来获取元素,我们可以自己尝试仿照着C中对链表的操作来写一个函数获取列表中任意位置的元素(defun my-get-list-item(lst index) (let ((i 0)) (while (and (cdr lst) (< i index)) (setq lst (cdr lst)) (setq i (+ i 1))) (if (<= index i) (car lst) nil))) (my-get-list-item '(0 1 2 3 4 5) 2) ;; ⇒ 2当然也可以使用递归来完成(defun my-get-list-item(lst index) (if (or (not lst) (= 0 index)) (car lst) (my-get-list-item (cdr lst) (1- index)))) (my-get-list-item '(0 1 2 3 4 5) 2)递归版本相对于上面的循环来说要简单的多,代码量也少。递归版本中当列表为空或者当前索引为0时,停止递归并返回。利用空列表表的car 和 cdr 都是空这个特性,来将两种不同的情况使用同一操作进行处理。条件不满足时对cdr进行递归处理。虽然可以自己写这样的算法来取列表的第n个元素,但是elisp中也提供的对应的操作函数。使用 nth 来获取第n个元素,使用 nthcdr 来获取第n次调用cdr 的结果,也就是获取包含第n个元素的子列表(nth 2 '(0 1 2 3 4 5)) ;; ⇒ 2 (nthcdr 2 '(0 1 2 3 4 5)) ;; ⇒ (2 3 4 5)同时还提供了 last 来返回从右往左数第n个元素的子列表。和 butlast 来返回last之外的其它列表元素。(last '(0 1 2 3 4 5) 3) ;; ⇒ (3 4 5) (butlast '(0 1 2 3 4 5) 3) ;; ⇒ (0 1 2)利用这些函数可以实现取某一范围的子列表(defun my-get-sub-items (lst start end) (if (nthcdr start lst) (butlast (nthcdr start lst) (- (length lst) end)))) (my-get-sub-items '(0 1 2 3 4 5) 2 5) ;; ⇒ (2 3 4)上面的代码比较简单,首先使用 nthcdr 来取start后面的内容,然后使用 butlast 来去掉 end 后面的内容。不知道各位读者还记不记得 length 这个函数,前面我们用它来获取字符串的长度,这里我们用它来获取列表的长度。设置列表元素一般情况下,我们可以放心的递归和对列表进行操作,因为上述的一些函数都不会修改原列表的值,在递归或者循环的过程中我们使用的是产生的临时列表。但是有时候会希望修改列表的值,例如在将列表作为栈来使用的时候,就需要出栈和压栈的操作。设置元素的值,可以使用 setcar 和 setcdr 这两个函数。如果我想设置任意索引位置的值该怎么办呢?可以配合使用 nthcdr 和 setcar。(setq foo '(a b c)) (setcar foo 'x) foo ;; ⇒ (x b c) (setq foo '(a b c)) (setcdr foo '(x y)) foo ;; ⇒ (a x y) (setq foo '(a b c)) (setcdr foo 'x) foo ;; ⇒ (a . x) (setq foo '(a b c)) (setcar (nthcdr 1 foo) 'x) ;; ⇒ x foo ;; ⇒ (a x c)前面提到使用 push 在表头添加元素,这里再介绍一个 pop 函数,它用来删除表头元素,它们两个配合使用就能组成一个栈的数据结构(setq foo '(a b c)) (push 'x foo) ;; ⇒ foo (pop foo)列表排序将列表从尾到头进行反转可以使用 reverse ,例如(setq foo '(a b c)) (reverse foo) ;; ⇒ (c b a) foo ;; ⇒ (a b c)我们可以看到,reverse也是不修改原始的列表,而是返回一个新的列表。如果想要修改原始列表可以使用 nreverse(setq foo '(a b c)) (reverse foo) ;; ⇒ (c b a) foo ;; ⇒ (a)为什么这里foo 指向了列表的最后一个元素呢?使用当初学习C/C++链表操作时掌握的知识很好解释,原本foo指向的是列表头,但是反转之后,原来的链表头就变成最后一个元素,而没有修改foo指针指向的情况下,它就是指向链表的最后一个元素(这个原因是我猜的,不知道对不对)。我们还可以对列表进行排序,可以使用sort 函数进行排序,它接收一个列表,并且接收一个排序方式的函数。例如(setq foo '(3 4 5 1 2 0)) (sort foo '<) ;; ⇒ (0 1 2 3 4 5) foo ;; ⇒ (0 1 2 3 4 5)这里的 '< 是一个排序函数,有点像C++ 11 标准里面的 sort 函数,它可以传入一个函数用来表示排序时比较大小的一个过程。而且这里我们并不需要在这个时候调用 < 这个函数,所以先使用 quote 。在后续真正执行排序要比较大小的时候会调用它。这里我们可以自己定义比较函数,比如这里我们按照字符串长度进行排序(defun strlen-cmp (str1 str2) (< (length str1) (length str2))) (setq foo '("hello" "emacs" "aaa" "bbbbbb")) (sort foo 'strlen-cmp) ;; ⇒ ("aaa" "hello" "emacs" "bbbbbb") foo ;; ⇒ ("aaa" "hello" "emacs" "bbbbbb") ;; 这里也可以使用lambda表达式 (setq foo '("hello" "emacs" "aaa" "bbbbbb")) (sort foo (lambda (str1 str2) (< (length str1) (length str2)))) ;; ⇒ ("aaa" "hello" "emacs" "bbbbbb") foo ;; ⇒ ("aaa" "hello" "emacs" "bbbbbb")这里我们发现sort 已经将修改了原始列表,如果想要保留原始列表,可以使用 copy-sequence(setq foo '(3 4 5 1 2 0)) (let ((temp (copy-sequence foo))) (sort temp '<)) ;; ⇒ (0 1 2 3 4 5) foo ;; ⇒ (3 4 5 1 2 0)还有像 nconc 和 append 功能相似,但是它会修改除最后一个参数以外的所有的参数,nbutlast 和 butlast 功能相似,也会修改参数。这些函数都是在效率优先时才使用。总而言之,以 n 开头的函数都要慎用遍历列表前面我们已经使用 car 和 cdr 能做到遍历列表,这里再介绍一下专门用来遍历的函数 mapc 和 mapcar 。它们都可以遍历列表中的所有元素,它们的第一个参数是一个函数,每次遍历到一个元素的时候会调用这个函数并将元素作为参数传入这个函数。C++中没有提供这样的函数,但是也有类似的操作。例如使用 foreach 获取每个元素,然后根据元素来执行操作。(setq foo '(0 1 2 3 4)) (mapc '1+ foo) ;; ⇒ (0 1 2 3 4) foo ;; ⇒ (0 1 2 3 4) (setq foo '(0 1 2 3 4)) (mapcar '1+ foo) ;; ⇒ (1 2 3 4 5) foo ;; ⇒ (0 1 2 3 4)这两个遍历函数的区别就是,是否使用返回值来构建新的列表,其中 mapcar 会根据返回值构建新的列表,而 mapc 则返回原列表。我们发现无论是哪个函数都无法修改原始列表,要修改原始列表当然也有方法,我能想到的一个方法就是循环,然后配合 setcar 和 ntdcdr 根据索引来设置。好了,本篇的内容就到此为止了。本篇按照 emacs lisp 简明教程 的内容修改而来的。原教程还有好多其他数据结构的操作,但是我作为初学者还是希望本篇内容专注在列表上,至于教程中涉及的其他操作或者数据结构,等后面学到了再了解也不迟。
2025年01月13日
5 阅读
0 评论
0 点赞
2024-12-30
Emacs折腾日记(七)——布尔变量、逻辑运算符与位运算
通过前面的几节内容我们已经对elisp中基本类型有所了解了。emacs lisp 简明教程 中下一节开始就是讲相关容器。所以这一篇我将它作为基础类型的一个结尾,将平时会用到,但是之前没有涉及到的内容都包含进来。bool类型本篇首先要提到的就是bool类型,我们已经在前面几章中用到过它,但是没有具体提到它。像if,cond、while 中都有它的声影。有其他编程语言相关经验的读者对它应该不会陌生,而且使用起来应该也是手到擒来。elisp中bool变量的真和假分别用 t 和 nil 来表示。它只有 nil 表示假,其余都是真。在其他编程语言中,0表示假,但是elisp中0也是真,我们可以使用下列代码来验证(if 0 (message "0 is t") (message "0 is nil")) ;; ⇒ "0 is t"包括0、空字符串都是真,elisp中只有 nil 本身是假,其余都是真。与其他编程语言类似,bool变量主要使用逻辑运算符来进行运算。elisp中的逻辑运算符也是与或非,对应的操作符为 and、or、not ,它们中间也有短路性质,即and 语句中如果一条语句已经为假,则不执行后一条,而or 中一条语句为真,则不判断后一条语句。elisp中经常利用短路性质来执行一些特殊的操作,例如常常用 or 来设置函数参数的缺省值。例如(defun say-hello (&optional name) (or name (setq name "Emacs")) (message "Hello, %s" name)) (say-hello) ;; ⇒ "Hello, Emacs" (say-hello "Lisp") ;; ⇒ "Hello, Lisp"位运算当初在学习C、C++的时候就觉得它对二进制位的操作实在是比较精妙,例如TCP/IP协议中使用位域来定义相关结构体。或者Win32 API中关于flag的设计就是典型的位运算设计。利用位运算的相关内容,一个字节的数据就能存储8位的标志。虽然教程中没有提及位运算的内容,但是我实在是比较好奇elisp中的位运算,所以我加了这一部分的内容。内容也比较简单,位运算基本也就是那些操作,主要是 and、or、xor、not。以及左移右移的操作。elisp中提供了支持这些操作的一些函数,下面是一些位的逻辑运算的函数logand: 按位与运算logior: 按位或logxor: 按位异或lognot: 按位非;; 3 ⇒ 011 5⇒ 101 (logand 3 5) ;; ⇒ 1 (logior 3 5) ;; ⇒ 7 (logxor 3 5) ;; ⇒ 6 (lognot 5) ;; ⇒ -6下面是位移运算的函数ash: 位移操作elisp 中没有单独提供左移和右移的操作,上面的函数根据第二个参数来决定左移或者右移,正数代表左移,负数代表右移,例如(ash 3 1) ;; ⇒ 6 (ash 3 -1) ;; ⇒ 2本节到此就结束了。本节算是一个针对原来教程的补充,内容不多。
2024年12月30日
11 阅读
0 评论
0 点赞
2024-12-28
Emacs折腾日记(六)——elisp字符与字符串类型
本文相关的知识点主要来自 elisp 简明教程 后续内容可以直接查看这个教程上一节我们了解了elisp中基础数据类型之一的数字类型,相比于C/C++ 来说elisp的数字类型更少,学习起来可能也更加简单。那么这篇我们来学习另一个数据类型——字符串字符串的基本介绍回忆以下在C/C++中学到的关于字符的知识,字符采用char 来表示,它占一个字节,里面存储的是各个字符的编码。当然针对汉字或者其他东亚文字,一个char 可能表达不了,它会用 2个或者3个字节来表示一个汉字。后来又有unicode字符,和wchar_t 类型。而字符串则是以0结尾的字符数组。C/C++中经常会出现这样的经典考题char* pszStr = "Hello, World"; char szBuf[] = "Hello, World";它们分别占几个字节,它的考点主要有两个,第一就是指针类型存储的就是地址,它与具体的机器结构有关x86机器上占4个字节。第二个考点就是字符串里面藏了一个0作为字符串的结尾char szBuf[] = "Hello\0Word";这样的字符串虽然可以表达出来,但是我们通过 strlen 之类的函数,得到的结果却是5。因为遇到0就结束了。elisp中的字符串与C/C++中最大的不同就是elisp中字符串可以有 0。另外一个不同就是elisp中没有字符类型,字符串中每一个字符都是字符的unicode形式,用C/C++类比就是字符串通过GetAt 之类的函数返回的是字符的unicode整数值。当然严格意义上来说C/C++中的字符类型也是一个整数值。elisp中可以使用?A 这样的形式来表示一个A字符。最终得到的结果就是A的ASCII码 65,我们可以使用之前学到的数字类型检测函数来判断它得到的是不是整数类型(integerp ?A) ; ⇒ t对于一些标点符号或者有歧义的字符,可以使用 \ 进行转义,例如?\' ?\" ?\\对于一些没有歧义的标点符号加不加转义字符没有影响,但是为了美观或者同一或者说为了不增加记忆的负担,标点符号统一使用转义字符。另外,我们可以在字符串中使用10进制、八进制、16进制的形式来表示字符,例如(setq msg "\x68\x65\x6C\x6C\x6F\x2C\x20\x77\x6F\x72\x6C\x64") ; ⇒ "hello, world"使用其他进制的写法如下:十进制:使用 \d + 数字(不常用,主要用十六进制和八进制)。十六进制:使用 \x + 两位十六进制数字。八进制:使用 \0 + 三位八进制数字。字符串函数首先我们可以使用 length 来获取长度,它有点类似与 Python中的len 函数,它不光可以获取字符串类型的长度,还可以获取列表、向量等类型的长度例如(setq msg "hello, world") (length msg) ;; ⇒ 12我们在前面说过,字符串可以带上0,我们来测试一下有0的情况下,得到的长度如何(setq msg "hello\x00world") (length msg); ;; ==> 11因为elisp并不以0作为字符串的结尾,实际上elisp字符串是以向量的形式存储的,向量中每个元素都是一个整数,所以这里返回的仍然是字符向量的大小。我们可以使用 stringp 来测试一个变量是否为字符串。例如(setq msg "hello") (stringp msg) ; ⇒ t (stringp ?A) ; ⇒ nil我们也可以使用 string-or-null-p 函数来检测,顾名思义,它主要用来判断当前变量是否为字符串或者是一个nil(char-or-string-p msg) ; ⇒ t (char-or-string-p ?A) ; ⇒ t (char-or-string-p nil) ; ⇒ nil (char-or-string-p "") ; ⇒ t但是遗憾的是 elisp 中没有判断字符串是否为空的方法,我们只能自己写代码来实现(defun string-emptyp (str) (and (stringp str) (zerop (length str))))这个函数判断当前传入对象是否是字符串,并且字符串长度为0。构造函数可以使用make-string 函数来构造一个字符串,它构造一个里面都是同样字符的字符串,例如(make-string 5 ?A) ; ⇒ "AAAAA"如果想要构造一个不同字符构成的字符串,可以使用 string(string ?A ?B ?C) ; ⇒ "ABC"也可以使用 substring 和 concat 来产生一个新的字符串,前者从字符串中取子串,后者连接两个字符串。substring 接受一个字符串和两个整数,表示一个范围,是一个前开后闭的区间,也就是包含前面的范围不包含后面的范围。字符串的索引也是从0开始。(substring "Hello, World" 3 5) ; ⇒ "lo"也可以只包含一个起始位置,表示从这个位置开始往后的字符(substring "Hello, World" 3) ; ⇒ "lo, World"也可以传入负数,与Python中的索引类似,负数表示从右往左数,但是注意,最右边的字符是-1,因为0表示最左边的数(substring "Hello, World" -5 -3) ; ⇒ "Wo"concat 就相对比较容易理解一些,它就是将两个字符串合并成一个新串(concat "hello" ", world") ; ⇒ "hello, world"与C/C++类似的是,字符串定义之后无法更改,需要更改的话,它的做法是创建一个新的字符串,并且舍弃掉原来的字符串。所以这里将 substring、concat 这种取子串和连接字符串的函数也归类到构造函数中,因为它们的的确确构造了一个新的字符串。字符串比较在C/C++中,比较字符串时使用 strcmp 函数。我们根据它的返回值来决定字符串是大于小于或者等于。在比较的时候从左往右,依次比较它们的编码值,直到遇到不一样的值。它仅仅比较编码值,而不关心字符串长度,只有在前面的字符都相等的时候才会根据长度判断。相信各位在学习C/C++的时候都亲手实现过strcmp函数,这里就不展开了。elisp中字符串的比较函数就比较多了。char-equal 比较两个字符是否相等,默认情况下它会忽略大小写,例如(char-equal ?A ?a) ; ⇒ t如果要大小写敏感的话就不能用这个函数了,那么大小写敏感的时候该怎么比较呢?这个时候千万别犯迷糊,字符本身就是一个整数,完全可以使用 = 或者 eql直接判断(eql ?A 65) ; ⇒ t (= ?A 65); ⇒ t判断字符串是否相等我们可以使用 string= 或者 string-equal 。它们二者是等价的,是同一个函数的不同叫法而已。(setq foo "hello") (setq bar "hello") (string= foo bar) ; ⇒ t使用字典顺序来比较字符串大小使用的是 string< 或者 string-lessp 。与前面类似,它们也是等价的。它的判断逻辑与 strcmp 函数相同。(setq foo "Hello") (setq bar "hello world") (string-lessp foo bar) ; ⇒ t (setq foo "Hello") (setq bar "Hello") (string-lessp foo bar) ; ⇒ nil比较遗憾的是没有 string> 这样的比较。如果想要判断是否大于的话,需要判断不相等并且不小于。根据 string< 的比较逻辑来看,空串是最小的字符串,也就是任意非空字符串都比空串大,因此上面判断是否是空串的代码可以使用这一特性实现(defun string-emptyp (str) (and (stringp str) (not (string< "" str))))不知道各位是否还记得Java String类中有 Equal 函数和 == 来比较字符串。其中Equal来比较字符串内容是否相等,而 == 仅仅比较对象的地址是否相等。elisp中同样有这样的操作,我们使用 eq 来代替 == 判断对象本身是否相等,对于简单类型也就是数字类型,我们使用它来判断数字是否相等,而对于字符串、列表、向量这种复杂类型时,判断它们的地址是否相等。(setq foo ?A) (setq bar ?A) (eq foo bar) ; ⇒ t (setq foo "A") (setq bar "A") (eq foo bar) ; ⇒ nil字符串转化下面来介绍一些字符串和数字类型相互转换的函数我们可以使用 char-to-string 来将一个整数转换成字符串,或者使用 string-to-char 来将字符串转化为整数,当然这个函数只会返回第一个字符的整数值。例如(char-to-string 65) ;; ⇒ "A" (string-to-char "Hello world") ;; ⇒ 72使用string-to-char 只能获取字符串中第一个字符的值,如果我们要取字符串中任意位置的字符该怎么办呢?我们可以使用substring 来获取以对应位置开始的一个子串,然后获取这个子串的第一个字符(defun get-string-char (str index) (string-to-char (substring str index))) (get-string-char "Hello World" 5) ; ⇒ 32或者我们可以也使用 aref 函数,该函数用来取数组中任意位置的值,因为字符串也是一个数组,因此我们可以使用该函数来取字符串中任意位置的字符。例如(aref "Hello World" 5) ;; ==> 32另外我们可以将数字转化为对应的字符串或者将数字字符串转化为对应的整数。它们的功能类似于C/C++ 中的 atoi 和 itoa 函数。string-to-number 用来将字符串转化为数字,它可以支持从2到16进制的转化,例如(string-to-number "ff" 16) ;; ⇒ 255 (string-to-number "A1") ;; ⇒ 0, 默认以10进制进行转化 (string-to-number "10" 2) ;; ⇒ 2number-to-string 用于将数字转化为字符串,它只支持以10进制的形式转化(number-to-string 256) ; ⇒ "256" (number-to-string ?A) ;; ⇒ "65"如果想以任意进制来将数字转化为字符串,那么可以使用 format 函数,它类似于C/C++中sprintf。用来格式化字符串,但是它只支持8进制10进制和16进制的转换(format "%d" 256) ;; ⇒ "256" (format "%#o" 256) ;; => "0400" (format "%#x" 256) ;; ⇒ "0x100"要转化成二进制的话,没有现成的函数可以用,不过我们可以自己实现,相信学过C/C++的应该写过类似的算法,不过我记得当初我学的算法是先入栈再出栈,下面的代码也是采用类似的方式。通过取模最先算出来的在最低位,所以我们在连接字符串的时候将计算的结果放到前面,连接上之前计算的结果(defun number-to-binary (num) (if (= num 0) "0" (let ((binary "")) (while (> num 0) (setq binary (concat (number-to-string (mod num 2)) binary)) (setq num (/ num 2))) binary))) (number-to-binary 256) ;; ⇒ 100000000emacs-lisp 简明教程 中还介绍了其他类型的数据结构与字符串互相转化的函数,这里就不介绍了,等后面学到了再说。另外字符串还有一些大小写转换的函数。使用 downcase 将字符串中的字母都转换为小写字母,使用 upcase 转换为大写字母。例如(downcase "Hello World") ;; ⇒ "hello world" (upcase "Hello World") ;; ⇒ "HELLO WORLD" (downcase "你好,世界") ;; ⇒ "你好,世界" (upcase "αβγ") ;; ⇒ ""ΑΒΓ""这种字母文字它可以进行大小写转换,但是对于中文这种没有大小写字母的就不存在转化了。使用 upcase-initials 来将字符串中每个单词的第一个字符大写,其余的字符它会忽略它。(upcase-initials "hellO woRld") ;; ⇒ "HellO WoRd"函数 captialize 会将字符串每个单词首字母大写,其余的转化成小写(capitalize "hellO wORLD") ;; ⇒ "Hello World"查找与替换字符串最重要的操作还是查找和替换。elisp 中查找主要使用 string-match 。它使用正则表达式来进行查找。elisp中没有C/C++中find 那样查询子串的缩进的函数。查询子串其实也可以利用正则表达式来处理(string-match "Emacs Lisp" "This is Emacs Lisp Program") ;; ⇒ 8该函数的第一个参数是一个正则表达式,上面代码中我们直接使用字符串进行匹配,就是在精准的匹配子串。它返回子串开始的索引。它还可以接收一个数字,表示从字符串的第几个字符开始往后进行查找,这个参数的作用有点像 String.Find(int index) 这个重载函数中 index 的含义。(string-match "Emacs Lisp" "This is Emacs Lisp Program" 10) ;; ⇒ nil但是如果查找的子串中有特殊符号的话,就不能这么使用(string-match "2*" "232*3=696") ;; ⇒ 0这里因为 * 被当成正则表达式的模糊匹配符号,它表示任意一个字符。如果想要它单纯的作为普通符号,可以使用regexp-quote 来处理一下,它的作用是将字符串中的所有特殊字符转义,使其可以安全地作为正则表达式使用。这样可以确保字符串的内容被视为字面量,而不是正则表达式中的元字符。(string-match (regexp-quote "2*") "232*3=696") ; => 2不知道各位读者使用过 C/C++ 中的正则表达式没有,正则表达式匹配之后会产生一个结果对象,它包含了所有匹配上的位置。我们可以通过循环或者其他方式来得到这个位置,并且得到具体匹配上的结果。在elisp中,结果被保存在 match-data 中。它允许你获取最近一次正则表达式匹配的位置信息,包括匹配的起始和结束位置。我个人的感觉它有点像Win32 API中的GetLastError 一样,每次调用其他API结果都会被覆盖,每次调用只能得到上一次的错误码。(progn (string-match "3\\(4\\)" "01234567890123456789") (match-data)) ;; ⇒ (3 5 4 5)我们使用 3(4) 这样的正则表达式来进行匹配。这里括号表示一个捕获组,它匹配34或者单独匹配4。match-data中每对数组表示一个匹配组的起始和结束位置,它是一个前闭后开区间。也就是包括前面的,不包括后面的。上面的代码先匹配 34,发现它在第3个字符处出现,所以它返回第一组数据 (3 5),然后匹配4,它出现在第4个字符,所以产生第二组数据 (4 5)。按照这个思路,如果将正则表达式修改一下改为 3\\(4\\)\\(5\\) 将会产生3组数据,第一组匹配 345 ,第二组匹配 4 ,第三组匹配 5。最后的结果就是 (3 6 4 5 5 6)在上面说到 match-data 中每对数组表示一个匹配组的起始和结束位置。我们可以使用 match-beginning 和 match-end 来获取这两个数据,它需要一个整数作为参数,用来表示取第几组的数据,例如下面的代码(progn (string-match "3\\(4\\)" "01234567890123456789") (message "%d" (match-beginning 0)) ;; => 3 (message "%d" (match-end 0)) ;; ⇒ 5 (message "%d" (match-beginning 1)) ;; ⇒ 4 (message "%d" (match-end 1))) ;; ⇒ 5如果我们给出的参数超过了匹配组的大小,那么它们将会返回 nil ,例如如果上述匹配我们使用 (match-beginning 2) 来取第三组的结果的话,将会得到nil每一个匹配组都是一个半开半闭区间,因为 match-end 是匹配到的字符位置的下一个位置,所以使用它很容易进行循环。上述的代码在匹配的时候,只要是匹配到就停止了,我们可以写一个循环用来继续匹配后面的字符串(let ((start 0)) (while (string-match "34" "01234567890123456789" start) (message "find at %d\n" (match-beginning 0)) (setq start (match-end 0))))掌握了查找的相关操作之后,我们继续来学习有关替换的操作。我们可以使用 replace-match 来将匹配到的字符串替换成指定的字符串,它的函数原型如下(replace-match NEWTEXT &optional FIXEDCASE LITERAL STRING SUBEXP)我们主要需要关注的是 NEWTEXT 它代表的是我们希望用哪个字符串来替换匹配上的字符串,STRING 表示希望进行匹配的字符串,例如(let ((str "hello world 123")) (string-match "\\([0-9]\\)" str) (replace-match "#" nil nil str)) ;; ⇒ "hello world #23"我们也可以使用循环来将所有数字都替换成 #(let ((str "hello world 123") (start 0)) (while (string-match "\\([0-9]\\)" str start) (setq str (replace-match "#" nil nil str)) (setq start (match-end 0))) str) ;; ⇒ "hello world ###"需要注意的是,这里每次执行 replace-match 后都返回一个新的字符串,原来老的字符串保持不变,所以我们这里每次替换之后都手动的使用 setq 来对老的字符串进行赋值,然后再重新匹配。这样才能保证最后的结果是我们想要的结果。我们可以使用下面的代码来验证这一点(let* ((str "hello world 123") (start 0) (final-str str)) (while (string-match "\\([0-9]\\)" str start) (setq final-str (replace-match "#" nil nil str)) (setq start (match-end 0))) final-str) ;; ⇒ "hello world 12#"这里只替换了最后一个数字,这是因为执行 replace-match 之后的str 变量并没有被改变,每次都是之前的再进行匹配,唯一变化的只有 start 的值。最后一次我们仍然在使用原始的 str 字符串在进行匹配,它匹配到 3。所以最后一次替换只将3替换成了 #。我们也可以使用捕获组进行替换,例如我们想将 "hello world 123" 替换成 "123 hello world",那么可以使用如下代码(let ((str "hello world 123")) (string-match "\\(hello world\\) \\([0-9]+\\)" str) (replace-match "\\2 \\1" nil nil str)) ;; ⇒ "123 hello world"这里使用 \\2 \\1 来表示替换的新字符串,它表示的含义是匹配组里面的第二组结果+空格+第一组结果。它们组成了一个新串用来替换原来的字符串。replace-match 的最后一个参数表示替换哪一个捕获组,默认是0,例如上述的代码中,第0个捕获组就是整个字符串,所以它替换了整个字符串,我们可以将上述代码做一下修改(let ((str "hello world 123")) (string-match "\\(hello world\\) \\([0-9]+\\)" str) (replace-match "\\2 \\1" nil nil str 1)) ;; ⇒ "123 hello world 123"它将第一匹配组也就是 hello world 使用新字符串 123 hello world 来替换,然后加上原来剩下的字符串,最终也就得到结果 123 hello world 123到这里,已经将字符串替换的常见操作都做了一些说明。本节的内容也就到此结束了。后面将继续按照 教程来学习。敬请期待!当然了,如果各位读者觉得我的这一系列教程有抄袭的嫌疑,或者质量不如原版,又或者更新缓慢,请按照对应的链接来学习相关内容。
2024年12月28日
8 阅读
0 评论
0 点赞
2024-12-21
Emacs 折腾日记(五)——elisp 数字类型
本文是参考 emacs lisp 简明教程 写的,很多东西都是照搬里面的内容,如果各位读者觉得本文没有这篇教程优秀或者有抄袭嫌疑、又或者觉得我更新比较慢、再或者其他什么原因,请直接阅读上述链接中的教程。上一篇我们讲了elisp中的流程控制结构相关的内容,下面就该进入到对应数据结构的学习了。elisp中主要的数据结构有:整数、浮点数、列表、符号、向量、散列表等等类型。下面我们先从最简单的类型——整数和浮点数说起数字类型与C/C++对比起来,elisp数字类型少的多,C/C++ 整数类型就有好几种,包括有符号、无符号、int、short、long之类的。elisp不区分这些,它仅仅有整数和浮点数。而且elisp 中只有浮点数这一种小数类型,不像C/C++还有双精度浮点数和单精度浮点数之分。elisp的整数范围与具体的机器有关,它的范围可以通过变量 most-positive-fixnum 和 most-negative-fixnum 来得到。例如在我的机器上它们的值如下most-positive-fixnum ;; 2305843009213693951 most-negative-fixnum ;; -2305843009213693952在给变量使用数字类型赋值的时候,我们可以使用10进制或者其他任意进制的形式。例如#b101100 => 44 ; 二进制 #o54 => 44 ; 八进制 #x2c => 44 ; 十进制 #24r1k => 44 ; 二十四进制因为26个英文字母+10个数字的原因,我们最大只能使用36进制来表示一个数字,但是基本不用到这么大的。日常最多也就用用10进制、二进制、16进制、8进制都算用的少。浮点数的表达遵循 IEEE 标准,也就是可以使用带小数点的数字来表示,或者带上 e 来使用科学计数法,例如3.14 1.0e-10数字类型的测试作为动态类型的语言,在代码执行阶段,变量的类型是会发生变化的。我们无法仅通过变量名或者变量的初始化值来判断变量类型。emacs的变量在执行阶段都知道自己的类型,但是它无法主动向我们报告,我们需要使用一些函数来进行判断,关于数字类型,提供了下列的函数integerp floatp numberp从字面上能理解它们分别判断是否是整形、浮点数、以及数字类型。elisp 测试函数一般都是用 p 来结尾,p 是 predicate 的第一个字母。如果函数名是一个单词,通常只是在这个单词后加一个 p,如果是多个单词,一般是加 -p数的比较与C/C++ 类似,数字的比较一般有 >、<、>=、<= 。但是也有不同的地方,因为elisp中都是使用 setq 来进行赋值的,所以它采用 = 来表示数学意义上的相等。还有一个不同的地方因为elisp中没有 += 、-=、/= 、*= 这样的运算符,所以它使用 /= 来作为不等的判断符号与其他语言类似的,浮点数直接使用等于或者不等于来判断并不准确,需要在一定范围内忽略误差。在C/C++中,我们常见的写法是给定一个误差值,然后二者差的绝对值在这个误差值范围内则认为它们相等。我们将这个算法使用elisp改写一下就得到下面的代码(defun approx-equal (x y) (let ((fuzz-factor 1.0e-6)) (< (abs (- x y)) fuzz-factor))) (approx-equal 1.000001 1.00000000000000001) ;; => t上述的写法并不严谨,在一定误差范围内,它是对的,但是在某些情况下它就不对了,例如 1.0e-7 和 1.0e-12 。它们本身并不相等,但是它们都超过了这个误差范围,相减之后的值小于这个误差范围。但是我们看到其实它们直接的差距还是挺大的,间隔1.0e5 的数量积。我们可以将上述算法进行一些改进(defun approx-equal(x y) (let ((fuzz-factor 1.0e-6)) (or (and (= x 0) (= y 0)) (< (/ (abs (- x y)) (max (abs x) (abs y))) fuzz-factor)))) (approx-equal 1.0e-7 1.0e-12) ;; => t这段代码采用的是比较相对差距的办法。因为涉及到除法,所以先把二者等于0的情况排除了,避免发生除0的问题。上述代码改造成对应的C代码就是#define FUZZ_FACTOR 1.0e-6 // 定义误差范围 bool approx_equal(double x, double y) { // 处理特殊情况:如果两个数都是 0 if (x == 0 && y == 0) { return true; } // 计算相对差并进行比较 double relative_difference = fabs(x - y) / fmax(fabs(x), fabs(y)); return relative_difference < FUZZ_FACTOR; }另外 elisp 中有 eql 函数来判断两个数是否相等(eql 1 1.0) ;; => nil (eql 1.0e-7 1.0e-12) ;; => nileql 在判断数字时不光判断值,也判断类型。第一条语句,因为二者类型不同,第二条语句二者都是float属于同类型,但是二者的值不同,因此两个结果都是假。数字的转换elisp 中可以进行 整形和float型数字的相互转换。在C/C++ 中,整形可以通过隐式转换自动转换成float,而float转换成int时会丢失小数位,比如哪怕是 1.9 在转换为整数时也会是 1。在elisp中,可以通过float将整数转化为浮点数。例如(floatp 1) ; ⇒ nil (floatp (float 1)) ; ⇒ t (eql (float 1) 1.0) ; ⇒ t而浮点数转化成整数有下面几个函数truncate: 抹除小数位,也就是C/C++语言中float转int的操作floor: 类似于C/C++ 中的floor 函数,返回小于等于该数的最大整数ceiling: 类似于 C/C++ 中的 ceil 函数,返回大于等于该数的最小整数round: 类似于 C/C++ 中的 round 函数,返回四舍五入后的整数数的运算一般的语言,数的运算无外乎 +、-、*、/ 取整、取模。elisp 中同样有这些操作,前面的加减乘除跟其他语言一致,没什么特别的。C/C++ 以及 elisp 中的除法都不是纯粹数学意义上的除法,它会将结果抹掉小数位转换成整数。我们如果将除数或者被除数转换为float类型的话,那么就得到数学意义上的除法结果 (当然也不全是,毕竟float数据有表达数据的限制)但是python 不一样,它就是纯粹数学意义上的除法。这个设计我也不知道算是好还是不好,毕竟它与其他语言不一致增加了记忆的负担。(/ 3 2) ; ⇒ 1 (/ (float 3) 2) ; ⇒ 1.5 (/ 3.0 2) ; ⇒ 1.5C/C++ 中有 ++ 、 -- 操作,而且还分 前++ 和 后++ 。在 elisp 中没有这两个操作,也没有类似于 += 的操作。elisp的赋值一直是用的 setq。而且它提供了 1+ 1- 这两个符号来表示 ++ 和 --。至于是前 ++ 还是 后++ 呢?两个都不是,C/C++中的 ++ 本身具有改变变量值的作用,它们的区别在于是返回值之前改变还是之后改变。而elisp 主要使用 setq 来改变变量的值, 1+ 这个操作无法改变变量的,它仅仅改变这条语句返回的值。例如可以使用下面的代码来测试(defun inc (num) (1+ num)) (setq foo (inc 3)) ;; ⇒ 4这里将传入的参数加了1,但是其实函数中 num 的值并没有变化,我们可以对函数做一下修改来验证这一点(defun inc (num) (progn (1+ num) num)) (setq foo (inc 3)) ; ⇒ 3要改变变量的值需要使用 setq 来进行赋值,这个函数可以做一下修改(defun inc (num) (progn (setq num (1+ num)) num)) (setq foo (inc 3)) ; ⇒ 4取模的操作,elisp 中提供了两个方式 % 和 mod 函数,其中 % 与其他语言类似,它要求除数与被除数都是整数,而 mod 则没有这个要求。我们查看mod函数,发现它是被写在C代码里面的。它虽然也是取余,但是它与数学意义上取余的结果并不一致,例如(mod -10 3) ;; ⇒ 2 (mod 10 -3) ;; => -2 (% -10 3) ;; ⇒ -1 (% 10 -3) ;; ⇒ 1% 单纯的就是数学意义上的取模的操作,首先找到商,然后根据商来决定模而 mod 则不同,mod 中首先一个原则就是余数和除数的符号相同。所以第一个的结果应该是正数 也就是 -3 * 4 + 2 = 10,余数是2。第二个结果应该是 - 3 * (-4) - 2 = 10 mod 还有一个原则,那就是商的结果应该是整数。利用这两个原则我们就可以大概的还原一下计算的过程(mod 3.5 2) ;; ⇒ 1.5 (mod -3.5 2) ;; ⇒ 0.5 (mod 3.5 -2) ;; ⇒ -0.5根据上面两个原则,那么它们分别可以还原为1 * 2 + 1.5 = 3.5-2 * 2 + 0.5 = -3.5-2 * (-2) + 0.5 = 3.5另外还有一些其他数学上的操作,对于学习后面写配置的话,大多数应该是用不到的。后续需要使用的话再查询就好了,这里就不在多啰嗦了。到此为止我们已经介绍完了elisp中数的常见操作。后续将陆续介绍其他数据类型,敬请期待。
2024年12月21日
11 阅读
0 评论
0 点赞
2024-12-17
Emacs折腾日记(四)——elisp控制结构
目前我们接着学习elisp相关语法,这里我是按照 elisp 简明教程 来进行学习。与其说这是我自己写得教程到不如说是在这个上面做得注释。目前我不知道这样是否侵犯相关的知识产权。目前就先这样继续学习,继续写记录吧。闲话少说,进入本篇的正题,关于elisp的控制结构。一般编程语言都有三种控制结构:顺序结构、条件结构、循环结构。elisp同样有这三种控制结构。顺序结构和复合语句一般默认elisp的语句是顺序执行的,例如下面的代码(setq name "Emacs") (message "hello, %s" name)它先执行前面的 setq 语句,先给变量name 定义并赋值为 Emacs 。后面接着执行第二行代码,调用message 函数来输出一段文字。在其他语言一般都有一个复合语句。它是有多个语句共同组成的,例如 C/C++中使用{} 来将多个语句整合成一条复合语句。针对C/C++ 我们在很多地方会用到复合语句。例如如果 if , while 等语句后只需要一条语句,那么可以直接使用一条语句,例如下面的代码// 这么写代码不太正规但是符合语法规范,也能编译过 int main() { int i = 0; while(i++ < 10) printf("%d\n", i); //打印1到10,这么10个数字 return(0); }但是如果在循环或者if条件成立后,执行多条语句,就需要使用复合语句,也就是用大括号括起来。那么在elisp中也有这样的操作,在条件和循环语句中需要执行不止一条语句,也需要使用复合语句。elisp 中符合语句使用 progn 来包含一组语句组成复合语句,它的语法规则是(progn statement1 statement2 ... statement3)例如我们将上面的代码用 progn 包装一下(progn (setq name "Emacs") (message "hello, %s" name)) ;; => "hello, Emacs"使用 progn 包装的复核语句可以使用 C-x C-e 也就是 eval-last-sexp 来同时执行里面的两个子语句。如果我们将它们分开写,则使用 eval-last-sexp 做不到这点,它只能一条条的执行# 条件语句 我们使用 if 和 cond来表示条件分支,if的语法如下(if condition then else)需要注意的是 这里的 then 和 else 并不是关键字,而是对应的语句,也就说紧跟着if条件的语句表示条件成立时执行的代码,下一条则是条件不成立时执行的代码。例如我们使用下面的代码来获取两个数的最大值(defun get-max(a b) (if (> a b) a b)) (get-max 3 4) ; => 4与 C/C++ 的函数不同,elisp 函数的返回值不需要使用 return 或者其他的关键字特意指出,它是将函数最后执行的语句的返回值作为函数的返回值,这里当 a > b 时条件成立,执行 a 然后结束函数,也就是这个时候函数的最后一个语句是 a ,函数返回 a 的值。否则执行 b ,此时函数的最后执行的语句就是 b ,这个时候函数就返回 b 的值而 cond 有点像 C/C++ 中的 switch ,它的语法如下(cond (case1 do-when-case1) (case2 do-when-case2) ... (t do-when-none-meet))它的语法特点是,它与 switch 类似,由一堆 case 和 default 组成。每个case 都使用一对 () 来区分,最后可以使用 t 来表示未匹配到前面的 case 时执行的语句,类似于default语句。这里我们使用当初学习C/C++ switch 语法时的经典代码来作为示例(defun score-report (score) (cond ((>= score 90) "优秀") ((>= score 80) "良好") ((>= score 60) "及格") (t "不及格"))) (score-report 75); => 及格我们可以看到,cond 语句的使用比 switch 更为的灵活,switch case 只能进行整型变量的相等比较,而 cond 可以进行其他变量类型的不同形式的条件判断,它只是在形式上更像 switch,但是在使用的范围上更像 if-else if-else。另外 elisp 简明教程中 提供了一个使用 cond 计算 斐波那契数列的例子(defun fib(n) (cond ((= n 0) 0) ((= n 1) 1) (t (+ (fib (- n 1)) (fib (- n 2)))))) (fib 10) ; => 55因为 elisp 中使用 setq 来进行赋值操作,所以它里面的= 就是数学意义上比较相等的操作符,而 其他语言中的 == 在lisp中无效。这里如果写成 == 将会报错。上面的例子也很好理解 当 n 等于 0时返回0,等于 1 时返回1,否则返回 fib(n - 1) + fib(n - 2) 使用 C/C++ 的话可能更容易理解int fib(int n) { if(i == 0) return 0; else if (i == 1) return 1; else return fib(n - 1) + fib (n - 2) }循环结构循环使用 while 关键字,它的语法结构如下(while condition body)我们可以将上述循环打印的C代码使用 elisp 实现(setq i 0) (while (< i 10) (progn (message "%d" i) (setq i (+ i 1))))我们执行完代码之后使用 switch-buffer,切换到 *message* ,可以看到它打印了从0到9的数据。上面的斐波那契数列的例子我们可以使用 while 来实现(defun fib (n) (cond ((= n 0) 0) ((= n 1) 1) (t (let ((first 1) (second 1) (third 1)) (setq n (- n 2)) (while (> n 0) (progn (setq third (+ first second)) (setq first second) (setq second third) (setq n (- n 1)))) third)))) (fib 10) ; => 55因为 elisp 中没有提供 += ++ 这样算术运算符,所以我们需要使用 setq 来赋值。下面还有一个计算阶乘的例子(defun factorial (n) (let ((res 1)) (while (> n 1) (setq res (* res n)) (setq n (- n 1))) res)) (factorial 10) ; => 3628800我们也可以提供一个递归的版本(defun factorial (n) (if (= n 1) 1 (* (factorial (- n 1)) n))) (factorial 10) ; => 3628800到此为止,本篇就结束了。本篇涉及到的elisp 代码其实也不算复杂,如果能熟练掌握一门编程语言的话,到此为止的代码应该不算太难理解。在编写这些示例代码的时候我觉得还好,主要注意括号的匹配,算法什么的就是照搬C/C++中一些经典写法就差不多了。但是即使上面的代码并不多,代码量并不大,我也能明显感觉到上述代码在阅读上不那么直观。
2024年12月17日
10 阅读
0 评论
0 点赞
1
2
3