Elisp 03:变量

LogicPulseMaster
• 阅读 2151

前言:不知多久能学会 Elisp

上一章:文本解析

上一章实现的解析器程序——当然仅仅是玩具,有几处颇为丑陋,还有一处存在着安全问题。

全局变量

安全第一。先从安全问题开始。观察以下代码:

(defun text-match (src dest)
  (setq n (length dest))
  (if (< (length src) n)
      nil
    (string= (substring src 0 n) dest)))

上述代码定义的这个函数可判断字符串对对象 src 的内容是否以字符串对象 dest 的内容作为开头,例如

(princ\' (text-match "I have a dream!" "I have"))

输出 t。这不是问题。问题在于倘若紧接着执行

(princ\' n)

输出 6

问题是什么呢?在 text-match 这个函数定义的外部,能够访问在函数的定义内部的一个变量,宛若他人的手指可以触及我的内脏……这是不是一个安全问题?

这种匪夷所思的现象之所以出现,是因为 setq 定义的变量是全局变量。在一个程序里,倘若有一个全局变量,那么在这个程序的任何一个角落皆能访问和修改这个变量。

全局变量不可以没有,但不可滥用。对于 text-match 这样的函数,在其定义里使用全局变量,属于滥用。

局部变量

回忆一下 simple-md-parser.el 里的代码里 every-line 函数的定义:

(defun every-line (result in-code-block)
  (if (= (point) (point-max))
      result
    (progn
      (if (text-match (current) "```")
          (progn
            (if in-code-block
                (progn
                  (setq result (cons '代码块结束 result))
                  (setq in-code-block nil))
              (progn
                (setq result (cons '代码块开始 result))
                (setq in-code-block t))))
        (progn
          (if in-code-block
              (setq result (cons '代码块 result))
            (setq result (cons '未知 result)))))
      (forward-line 1)
      (every-line result in-code-blcok))))

在这个函数里,我在多处用 setq 反复定义了两个变量 resultin-code-block,但是倘若调用这个函数之后再执行以下程序

(princ\' result)
(princ\' in-code-block)

Elisp 解释器在对 (princ\' result) 进行求值时会出错,它会抱怨:

Symbol’s value as variable is void: result

意思是,result 这个变量未被定义。为什么会这样呢?

原因是它们也都是函数的参数,在函数定义的内部可以访问和修改它们,而在函数定义的外部却不能。因此,函数的参数是局部变量。

Elisp 语言以及其他 Lisp 方言,正是基于函数的参数构造了局部变量,并且为了简化构造过程,提供了 let 表达式。

let 表达式可以初始化局部变量,并将限定其生存范围。例如

(let ((a 1)
      (b "Hello")
      (c '世界))
  (princ\' a)
  (princ\' b)
  (princ\' c))

可定义三个局部变量 abc,它们仅在 let 表达式内部有效——可以使用,也可以修改。

使用 let 表达式,可以让不安全的 text-match 函数规矩一些:

(defun text-match (src dest)
  (let ((n (length dest)))
    (if (< (length src) n)
        nil
      (string= (substring src 0 n) dest))))

现在,倘若再执行

(princ\' (text-match "I have a dream!" "I have"))
(princ\' n)

Elisp 解释器在对 (princ\' n) 求值时会抱怨变量 n 未定义,然后终止。

let 表达式里,也可以不对局部变量进行初始化。例如

(let (a b c)
  (princ\' a)
  (princ\' b)
  (princ\' c))

结果输出:

nil
nil
nil

未进行初始化的局部变量,Elisp 解释器会认为它们的值是 nil

美颜

局部变量不仅能让函数更为安全,甚至对函数的定义和调用也能产生一些美容效果。

simple-md-parser.el 里定义的 every-line 函数,其调用形式是

(every-line '() nil)

需要给它两个初始的参数值,它方能得以运行。虽然它能正确地解决问题,但是却不美观,犹如一件电器,它能正常工作,只是有两个线头露在了外面。基于 let 表达式,在函数的定义可以去掉这两个参数。例如:

(let ((result '())
      (in-code-block nil))
  (defun every-line ()
    (if (= (point) (point-max))
        result
      (progn
        (if (text-match (current) "```")
            (progn
              (if in-code-block
                  (progn
                    (setq result (cons '代码块结束 result))
                    (setq in-code-block nil))
                (progn
                  (setq result (cons '代码块开始 result))
                  (setq in-code-block t))))
          (progn
            (if in-code-block
                (setq result (cons '代码块 result))
              (setq result (cons '未知 result)))))
        (forward-line 1)
        (every-line))))
  (every-line))

上述代码由于略微复杂,导致程序结构不够清晰,倘若隐去一些代码,便清楚得多。例如

(let ((result '())
      (in-code-block nil))
  (defun every-line ()
    ... 省略的代码 ...)
  (every-line))

所表达的主要含义是:在 let 表达式里定义了函数 every-line,然后调用该函数。注意观察,此时,该函数是没有任何参数。

不过,将函数的定义放到 let 表达式内,这个函数会被 Elisp 就地求值了。倘若依然希望它保持函数的尊严,而不是每次使用它都要背负一个冗长的 let 表达式,只需将整个 let 表达式封装为一个函数即可。例如

(defun every-line\' ()
  (let ((result '())
        (in-code-block nil))
    (defun every-line ()
      ... 省略的代码 ...)
    (every-line)))

上述代码不仅彰显了可以在 let 表达式里定义一个函数,也彰显了可以在一个函数的定义里定义一个函数。不过,我认为内外两个函数的名字最好换一下,即

(defun every-line ()
  (let ((result '())
        (in-code-block nil))
    (defun every-line\' ()
      ... 省略的代码 ...)
    (every-line\')))

现在,我觉得美观多了。因为 simple-md-parser.el 的最后两行代码,现在可以写成

(find-file "foo.md")
(princ\' (every-line))

对于上一章实现的列表反转函数也可以采用类似的办法予以美化。例如

(defun reverse-list (x)
  (let ((y '()))
    (defun reverse-list\' ()
      (if (null x)
          y
        (progn
          (setq y (cons (car x) y))
          (reverse-list\' (cdr x)))))
    (reverse-list\')))

如此,之前的代码

(setq x '(5 4 3 2 1))
(princ\' (reverse-list x '()))

现在可写成

(setq x '(5 4 3 2 1))
(princ\' (reverse-list x))

结语

局部变量可让程序更安全,也更优雅。

下一章:迭代

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
3年前
Python Challenge Level 18
初学Python,挑战一下流行的PythonChallenge,很不幸,卡在了18关~~被字符字节码之间的转换搞得焦头烂额,不过终于搞定了还是很happy的~~~主要的问题就是16进制形式的字符如何转成字节码(注意:不是encoding)如:\'89','50','4e','47','0d','0a','1a','0a','00
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
Uber准备放弃自动驾驶,转手卖给前谷歌无人车CTO,估值曾被孙正义炒到72.5亿美元
!(https://oscimg.oschina.net/oscnet/0fe7cb00a0cf4872b022342d1e21d47e.png)杨净发自凹非寺量子位报道|公众号QbitAI最新消息,Uber要出售无人驾驶部门(ATG)了。据TechCrunch报道,Uber有意向出售,而也有人愿意买。
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这