迷之Common Lisp

两(多)个不同的命名空间

在 CL 中,符号扮演了一个特殊的角色,这个角色在其它语言中不存在。

首先,符号扮演了其它语言中“标识符”的作用,它可以用来命名变量和函数;另一方面,符号本身是一种独立的数据类型,就如同数字、字符串一样。也就是符号本身也可以是“值”。例如:Tnil就是两个特殊的符号,它们的值就是它们自己。所有的关键字符号(以冒号开头的符号)都是自求值的,它们不指向内存中的某个值,它们的值就是它自己。

符号背后的真相是复杂的。事实上符号也许只是一个“引用”,它指向内存中的某块数据。CL 的复杂性在于,内存中的这一块数据有可能分成了多个格子,可以同时存放多个值,并且根据上下文需求来访问其中的某个格子。

  • 其中一个格子存放“值”(value),和其它语言中的变量一样,就是普通意义上的值。

  • 另外一个格子里可能存放着一个函数(function)!!

其它还有一些格子,不过这里暂时不讨论那些东西。只从前面两种可能性就可以理解这种复杂性。

换句话说,同一个符号,它有可能是一个普通变量,也可能同时又是一个函数。那么,解释器(编译器)如何知道在什么时候要去访问内存中的哪个格子呢?

很简单,当符号出现在参数位置上的时候,系统从 value 格子中取值并返回;当符号出现在函数调用位置(紧跟在括号后面)的时候,系统认为这是一个函数调用,于是去function格子中取出函数并进行调用。

在代码中显式或隐式地进行变量绑定时,值会放在 value 格子中,当 defun 时,函数体(代码)会放在function格子中。

还是实际操作一下吧,这样容易理解

;; 隐式地创建一个变量,赋值为32
(setq foo 32)
=> 32

;; 在 REPL 中验证一个,foo 的值确实是32
foo
=> 32

;; 再定义一个函数 foo, 它总是返回 nil
(defun foo () nil)
=> FOO

;; 现在,在 foo 这个符号后面已经有两个格子了,
;; 分别存了整数 32 和一个函数,互不干扰
foo
=> 32

(foo)
=> nil

;; 如果把函数放到 value 的位置上会怎么样?
;; 我们知道 lambda 表达式返回一个函数对象
(setf foo #'(lambda () t))
=> #<FUNCTION (LAMBDA ()) {1004E601EB}>
foo
=> #<FUNCTION (LAMBDA ()) {1004E601EB}>

(foo)
=> nil

;; 有趣的来了
(funcall foo)
=> T

同一个变量,保存了两个函数,调用其中一个返回 nil,调用另一个返回T。为什么会这样?

当使用显式的函数调用(紧跟在括号后面)去调用 foo 的时候,系统从 function中取得函数,并进行调用

当使用funcall去调用的时候,foo处在参数位置上,于是,系统去value格子中取出了另一个函数,传递给 funcall,于是得到了另一种结果。

(symbol-value 'foo)
=> #<FUNCTION (LAMBDA ()) {1004E601EB}>

foo
=> #<FUNCTION (LAMBDA ()) {1004E601EB}>

(symbol-function 'foo)
=> #<FUNCTION FOO>

#'foo
=> #<FUNCTION FOO>

(function foo)
=> #<FUNCTION FOO>

我们知道,#'前缀不过是 function的语法糖,现在看起来,function也不过是 symbol-function的语法糖而以。它们共同的作用,就是从一个符号背后专门放函数的格子里取出东西来。

把这一点搞清楚是有好处的。把函数当作参数去传递时要不要加 #'前缀?funcall的第一个参数为什么不能是一个函数名?

我是从 Scheme 入的坑,转到 CL 时四处碰壁。比如高阶函数的使用上,Scheme 直接把一个函数名作用参数传递给另外一个函数,在另外一个函数中可以直接调用。现在搞明白了,正确的姿势是,先用#'把一个函数的函数体(代码)取出来,再传递给另外一个函数,在被调函数内部,可以使用funcall直接调用这一坨代码。如果直接给funcall传递一个函数名,并让它调用会怎么样?系统会认为这个函数名是一个普通变量,并且试图去value格子里读取它的值,结果找不到就报错。

还有个好玩的事,除了nilT、关键字符号是自求值符号以外,还可以把普通符号也变成自求值的

(setf abc 'abc)
=> ABC
abc
=> ABC
(symbol-value 'abc)
=> ABC
(symbol-value
  (symbol-value
    (symbol-value
      (symbol-value
       (symbol-value 'abc)))))
=> ABC

不管使用嵌套多少层的symbol-value去读取一个自求值符号的值,它永远是它自己。为什么会这样?因为在系统内部,符号是唯一的,两个相同的符号,其实都指向内存中的同一个地址。

通过看文档,发现有几个访问器可以分别访问一个符号背后的几个格子:

symbol-name
symbol-value
symbol-function
symbol-plist
symbol-package

也就是说,在一个符号背后,有多达5个格子。

  • name 存放符号的字符串表示
  • value 存放具体的值
  • function 存放函数
  • plist 每个符号都自动地带有一个属性列表,尽管一开始它是空的。也就是说,可以在不需要声明一个变量的情况下,直接给某个符号赋值,过后再访问其中的值
  • package 标识了符号属于哪个包(命名空间)

    (symbol-name 'bar)
    => "BAR"

    ;; 尝试从一个从未出现过的符号中取出属性值,返回 nil。然而该符号已经悄悄地被创建了
    ;; 要注意,符号自带的 plist 的 key 也是用符号表示的,而不是冒号开关的关键字符号
    (get 'alizarin 'color)
    => NIL
    ;; 直接赋值
    (setf (get 'alizarin 'color) 'red)
    ;; 然后再读取
    (get 'alizarin 'color)
    => RED

按常理,当一个变量未被声明(隐式或显式),内存中是不应该有它的位置的,也就是它背后的格子还没有 malloc。真相是,一个符号从第一次出现,就自动在内存中被创建了(尽管可能会被GC立即回收掉)。所以,CL的变量声明与其它语言的变量声明和初始化是不一样的,显得相当类。

两种作用域

  1. 词法作用域 没有声明为 (declare (special var)) 的局部变量都是词法变量。词法变量的作用域限制在词法范围内。在调用时,它的值不会被新的 env 中的同名变量所覆盖。反过来,如果一个局部变量被声明为 special ,那么它的作用域也是动态的(在运行期,从 env 一级一级往上找,直到找到一个同名符号)

  2. 动态作用域 CL 的全局变量几乎都是用 defparameterdefvar 来声明的。使用这两者声明的变量都是“动态作用域”的(自由变量)。作态作用域的变量会被新 env 中的同名变量所覆盖。这就会造成一种错觉,似乎 CL 的全局变量都是动态作用域的自由变量。其实,如果用 setf 或者 setq 来隐式地创建全局变量,它的作用域就不是动态的了。

动态变量是把双刃剑,带来方便的同时也会带来难以调试的BUG。需要注意的是,用defparameterdefvar声明的全局变量统统都是自由变量。所以,传统上,LISP 程序员会使用前后加星号的方式来命名全局变量,多少能起到点强调作用。

CL 的这种复杂性在我看来是不必要的,CL 在标准化过程中带了太多的历史包袱,于是就变成了现在这个奇怪的样子。长得奇怪,但是却很强大。说句宽慰的话,这几乎是所有得到了广泛应用的工业语言的共性。比如 C,比如 perl,比如 Javascript, 再比如 UNIX SHELL(以及UNIX本身), 它们共同的特点是:被人骂了几十年了......

从某种角度讲,Scheme 是比 Common Lisp 更纯粹的 LISP,然而 Scheme 界的牛人们似乎忙于为了语言标准而争吵,对于语言的应用并不怎么上心。Scheme 作为一个活了几十年的语言,得到了太多的赞美。然而对比工业界每几年就要产生一门新语言,新语言一面世标准库和第三方库就迅速扩大。在这方面 Scheme 确实混得挺惨的。除了已经分道扬镳的 Racket 以外,几乎都是些玩具,从未真正地走出课堂。

转载于:https://www.cnblogs.com/zh-geek/p/8504070.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值