首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
105 阅读
2
nvim番外之将配置的插件管理器更新为lazy
78 阅读
3
2018总结与2019规划
62 阅读
4
PDF标准详解(五)——图形状态
40 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从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
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
314
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
23
篇与
的结果
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日
12 阅读
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日
12 阅读
0 评论
0 点赞
2024-12-11
Emacs折腾日记(三)——简单的elisp 入门
Emacs本身的使用并不复杂,利用帮助文档,差不多半小时左右就能把一些常见的操作方式和快捷键过一遍,剩下的就是慢慢使用并且熟悉了。Emacs真正有价值的是它高度的客制化。任何人都可以利用elisp代码将Emacs改造成只属于自己的编辑器。会elisp 的不一定是高手,但是高手没有一个是不会elisp的。学习Emacs也绕不开elisp。下面我们就来简单的学点elisp一个简单的 Hello Word(message "hello world")这是一个简单的elisp版本的hello world程序。麻雀虽小但是五脏俱全。从这个简单的程序来说我们可以看出lisp的最大特点,就是以括号作为作为一个完整的表达式。曾今有一个段子说苏联的特工冒死偷到了美国阿波罗计划代码的最后一页,结果回来一看全是括号。这也鲜明的表达了一个lisp的特点,那就是大量的括号我们简单的解析一下上面的代码,上面的代码调用了一个函数,函数的参数就是一个字符串的 hello world。我们可以打开Emacs,进入 scratch buffer,输入上述代码。之后可以将光标移动到代码尾部,按下快捷键 C-x C-e 来执行代码,或者使用 M-x 输入命令 eval-buffer 来看效果。我们可以看到,mini-buffer 位置出现了 "hello world" 的字符串变量我们可以使用 setq 来定义变量,它类似于C/C++ 中的= ,用来给变量赋值或者定义并初始化一个变量。例如我们可以改一下上述的代码(setq name "Emacs") (message "hello, %s" name)将上述代码输入到scratch buffer之后就需要依次在每行的最后执行 C-x C-e 或者直接执行eval-buffer 命令,这样就可以看到效果了上面我们说,elisp的表达式是使用括号来表示的,但是如果在上面代码的基础上加上一句(name)此时就会报错,显示的错误为 void-function name 。我们的本意是想让解释器返回name的值,但是解释器将它作为了一个函数。通过这个错误我们能了解到,elisp基本的表达式中函数需要用括号括起来,但是变量自己本身被作为一个完整的表达式。除了使用setq我们还可以使用 defvar 来定义变量,defvar 的使用如下(defvar variable-name value "variable document")例如(defvar name "Emacs" "a defvar demo name") name ;; ==> "Emacs"我们将光标放到name上,按下 C-h v 可以看到关于name的说明文档。需要注意的是,defvar与setq 除了defvar可以指定变量的说明文档外,还有一个区别就是defvar在定义变量前,这个变量已经有值的话,defvar不会改变变量的值例如下面的例子(setq foo "foo") (defvar foo "this is foo" "document for variable foo") (defvar bar "this is bar" "document for variable bar") foo ;; =>"foo" bar ;; =>"this is bar"C-x C-e(eval-last-sexp) treatsdefvarexpressions specially. Normally, evaluating adefvarexpression does nothing if the variable it defines already has a value. But this command unconditionally resets the variable to the initial value specified by thedefvar; this is convenient for debugging Emacs Lisp programs.defcustomanddeffaceexpressions are treated similarly. Note the other commands documented in this section, excepteval-defun, do not have this special feature.上述英文翻译过来就是 eval-last-sexp 对 defvar 做了特殊处理,默认情况下 defvar 在变量有值的情况下不做任何操作,但是在这个命令中,defvar 会无条件的将变量值重置为它指定的值。主要是为了方便调试代码。同时 defcustom 和 deface 做了同样的操作。请注意在本节中记录的其他命令(eval-defun 除外)没有做这样的处理这就解释了为什么我们使用 C-x C-e 执行的时候 foo 的值发生了改变函数函数的定义与使用作为一门函数式编程语言,函数是elisp的一等公民。如何使用一个函数我们已经在前面的hello world程序中见识过了,那么如何定义一个函数呢?定义一个函数使用 defun 关键字。它的语法如下(defun func-name(args) "document string" body)第一行代表一个函数名和函数的参数列表,第二行表示可以使用一个双引号包含函数的说明文档,Emacs是一个自文档的系统,后续我们可以查看这里写的文档。最后一行是函数的主体内容,例如下面的例子(defun say-hello(name) "say hello to define user" (message "hello, %s" name)) (say-hello "emacs") ;; => "hello, emacs"执行将会输出对应的信息。我们将光标移动到say-hello这个函数上,执行 C-h f 默认回车将会得到我们针对函数写的文档lambda 表达式其实像C++、Java、Python 之类的语言也有lambda表达式,它就是一个匿名函数。一般我们使用函数都是先定义同时给函数取一个名字,但是有时候我们仅仅需要一个临时函数作为参数或者仅仅只会在某些地方调用一次,这个时候就可以使用匿名函数。lambda表达式的形式与defun类似,它的使用规则如下(lambda (args) "document string" body)除了关键字变了,就是不用写函数名称了。我们使用 funcall 来调用一个lambda表达式(funcall (lambda (name) (message "hello, %s" name)) "emacs")我们执行它将会在mini-buffer中看到显示的字符串信息另外我们也可以将一个lambda表达式赋值给一个变量,最后通过funcall 来调用(setq say-hello (lambda (name) (message "hello, %s" name))) (funcall say-hello "emacs") (say-hello "emacs") ;; error, void-function say-hello知乎的大牛指出,这里原本有一个错误,在这里更正:defun 会把函数值绑定到符号的 function-cell 上,setq 会绑定到符号的 value-cell 上上述的说法,我查过 funcall 、value-cell 以及 function-cell 相关的文档,里面涉及到的一些知识点比较复杂,目前我还没有完全搞明白,而且把它贴出来作为入门来讲有点过于复杂了。变量作用域elisp默认使用 setq 定义的变量不管是在函数内还是函数外全都是全局变量,它们的作用域是全局作用域,例如(defun say-hello () (setq name "Emacs") (message "hello, %s" name)) (say-hello) ;;需要执行一下函数解释器才能执行到定义`name`变量的位置 (message name)一般来说代码如果都是全局变量的话,会给代码的编写和维护带来很大的不便。elisp中同样支持定义局部变量,我们可以使用 let 和 let* 它们的用法类似(let (bindings) body)其中的 bindings 可以是单个值,也可以是括号包裹的键值对。如果是单个值,则默认赋值nil,也就是空。如果是键值对,则将值赋值给对应的键。let定义的变量只能作用在let语句块内,例如下面一个计算圆面积的函数,这里知乎的大牛告诉我,pi 是emacs中的内置变量,我采用PI来定义圆周率(defun circle-area (radix) (let ((PI 3.1415926) area) (setq area (* PI radix radix)) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3) (message "%f" area) ;; error void-variable area其中我们在let语句块中定义了两个变量,pi初始化为3.1415926,area 初始化为 nil同样的,可以使用 let* 改写上面的程序(defun circle-area (radix) (let* ((PI 3.1415926) area) (setq area (* PI radix radix)) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3)let* 与 let 的区别在于,let* 可以在binding时候使用前面已经定义过的变量。例如上面的代码可以改写成(defun circle-area (radix) (let* ((PI 3.1415926) (area (* PI radix radix))) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3)我们使用 let 来改写一下这个程序,发现它会报错(defun circle-area (radix) (let ((PI 3.1415926) (area (* PI radix radix))) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3) ;; error void-variable PI好了本篇也该结束了,本篇主要了解了基本的elisp语法,下面进行一下总结:使用 defvar 和 setq 来定义全局变量,其中defvar可以给变量设置一个说明文档,我们使用 C-h v 来查看这个文档使用 let 和 let 来定义变量,其中 let 可以在变量定义的时候使用前面定义过的变量使用 defun 来定义函数使用lambda 来定义一个lambda表达式,使用funcall 来调用lambda,lambda可以赋值给变量,后续使用funcall来调用
2024年12月11日
7 阅读
0 评论
0 点赞
1
2
3