首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
124 阅读
2
nvim番外之将配置的插件管理器更新为lazy
90 阅读
3
2018总结与2019规划
67 阅读
4
PDF标准详解(五)——图形状态
44 阅读
5
为 MariaDB 配置远程访问权限
35 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
elisp
linux
文本编辑器
Java
反汇编
读书笔记
OLEDB
数据库编程
数据结构
Masimaro
累计撰写
321
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
24
篇与
的结果
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日
16 阅读
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日
12 阅读
0 评论
1 点赞
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日
14 阅读
0 评论
1 点赞
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日
13 阅读
0 评论
0 点赞
1
2
3