变量

引子

基本程序构造单元——变量。

Common Lisp支持两种类型的变量:词法(lexical)变量和动态(dynamic)变量。(动态变量有时也叫做特殊变量 special variable)这两中变量对应类型分别对应其他语言的局部变量和全局变量,不过,只能说是大致相似一方面某些语言中的局部变量更像是Common Lisp的动态变量。另一方面,某些语言中的局部变量虽然是词法作用域的,但并没有提供由Common Lisp的词法变量所提供的所有功能,尤其是并非所有语言都提供了支持闭包的词法作用域变量。

许多含有变量表达式都可以同时使用词法变量和动态变量

变量的基础知识

和其他语言一样,Common Lisp中的变量是一些可以保存值得具名位置,但是在Common Lisp中,变量并非像在Java和c++等语言中那样带有确定的类型,也就是不需要为每一个变量声明其可以保存对象的类型,相反,一个变量可以保存任何类型的值,并且这些值带有可用于运行期类检查的类型信息。因此 Common Lisp是动态类型的——类型错误会被动态的检测到。

比如,给+函数传递个字符串,Common Lisp就会报类型错误。而另一方面,Common Lisp是一种强类型语言,因为所有的类型错误都将被检测到——无法将一个对象作为其不属于的类型的实列来对待。

至少从概念上来说,Common Lisp中所有值都是对象的引用,因此,将一个变量赋予新值就会改变变量所指向的对象,而对之前被引用的对象没有影响。(如果一个变量保存了对一个可变对象的引用,那么久可以用该引用来修改此对象,而这种改动将应用于任何带有相同对象引用的代码。)

而另一种已经用到的引入新变量的方式是定义函数形参。在用DEFUN来定义函数时,形参列表定义了当函数被调用时用来保存实参的变量。列如,下列函数定义了三个变量x y和z,用来保存其实参:

(defun foo (x y z) (+ x y z))

每当函数被调用时,Lisp将会创建新的绑定来保存由函数调用者所传递的实参。绑定代表了变量在运行期的存在。单个变量就是可以在程序源代码中所指出的那种东西,在程序运行过程中可以有多个不同的绑定,单个变量甚至可以同时带有多重绑定。例如,一个递归函数的形参会在每一次函数调用中被重新绑定。

和所有Common Lisp变量一样,函数形参也可以保存对象引用。因此,可以在函数体内为一个函数形参赋予新值,而这并不会影响到同样函数的另一个调用所创建的绑定。但如果改变了传递给函数的可变对象,则这些改动将会被调用者看到,因为无论调用者还是被调用者都在引用同一个对象。

引入新变量的另一种方式是使用LET特殊操作符。下面就是一个LET形式的结构:

(let (variable*)
    body-form*)

其中每一个variable都是一耳光变量初始化形式,每一个初始化形式要么是一个含有变量名和初值形式的列表,要么就是一个简单的变量名——作为将变量初始化到NIL得简略写法。列如,下面的LET形式会将三个变量x y z绑定到初始值10 20 和NIL上:

(let ((x 10) (y 20) z) ...)

当这个LET形式被求值时,所有的初始值形式都将首先被求值,然后创建出新的绑定,并在形式体被执行之前这些绑定将初始化到适当的初始值上。在LET形式体中,变量名将引用新创建的绑定。在LET形式体执行结束后,这些变量名将重新引用在执行LET之前它们所引用的内容,如果有的话。

形式体中最后一个表达式的值将作为LET表达式的值返回,和函数形参一眼,由LET所引入的变量将在每次进入LET时被重新绑定。

函数形参和LET变量的作用域(变量名可用来引用该绑定的程序区域)被限定在引入该变量的形式之内,该形式即函数定义或LET,被称为绑定形式。很快将看到,词法变量和动态变量使用两种略有不同的作用域机制,但两者的作用域都被界定在绑定形式之内。

如果嵌套了引入同名变量的绑定形式,那么最内层的变量绑定将覆盖外层的绑定。例如,在调用下面的函数时,将创建一个形参x的绑定来保存函数的实参。第一个LET创建了一个带有初始值2的新绑定,而内层的LET创建了另外一个绑定,其初始值为3。右边的竖线标记出了每一个绑定的作用域。

1
2
3
4
5
6
7
8
9
;;;;第二杭的x为实参,第四行为2 第六行为3
(defun foo (x)
(format t "Parameter: ~a~%" x)
(let ((x 2))
(format t "Outer LET: ~a~%" x)
(let ((x 3))
(format t "Inner LET: ~a~%" x)))
(format t "Outer LET: ~a~%" x))
(foramt t "Parameter: ~a~%" x))

每一次对x的引用都将指向最小封闭作用域中的绑定。一旦程序控制离开了一个绑定形式的作用域,其最近的闭合作用域中的绑定就被解除覆盖,并且x将转而指向它。因此,调用foo将得到这样的输出:

1
2
3
4
5
6
CL-USER> (foo 1)
Parameter: 1
Outer LET: 2
Inner LET: 3
Outer LET: 1
NIL

后面学习其他可以作为绑定形式使用的程序构造,其特点在于所引入的新变量只能用于该构造。

DOTIMES循环和LET*需要后面学习

词法变量和闭包

动态变量

常量

赋值

广义赋值

其他修改位置的方式

函数

书接上文

有了语法和语义规则以后,所有lisp程序的三个最基本组成部分就是函数,变量和宏。一开始数据库建立就全部使用了,现在开始详细学习,首先学习函数,和其它语言一样,函数提供了用于抽象和功能化的基本方法

(宏是用来生成代码,不是用来完成实际操作的)

定义新函数

定义方式

函数一般使用DEFUN宏来定义。DEFUN的结构看起来像这样:

1
2
3
(defun nameparameter*)
"Optional documentating string."
body-form

命名方式

任何符号都可以用作函数名。通常函数名包含字典字符和连字符。但是在特定的命名约定里,其它字符也允许使用。列如,将值得一种类型转换成另一种函数有时会在名字中使用->

一个将字符串转换成微件(widget)的函数可能叫做string->widget 最重要的一个命名约定是用连字符而不是下划线或者内部大小写

因此是a-b而不是a_b或者aB

定义参数

一个函数的形参列表定义了一些变量,将用来保存函数在调用时所传递的实参。如果函数是不带有实参,则该列表就是空得,写成()。不同种类的形参分别负责处理必要的,可选的,多重的以及关键字实参。

参数说明

如果一个字符串紧跟在形参列表之后,那么它应该就是一个用来描述函数用途的文档字符串。当定义函数时,该文档字符串将被关联到函数名上,并且以后可以通过DOCUMENTATION函数来获取

最后总结

一个DEFUN的主体可由任意数量的lisp表达式所构成,他们将在函数被调用时依次求值,而最后一个表达式的值将被作为整个函数的值返回,另外RETURN-FROM特殊操作符可用于从函数的任何位置立即返回。

一开始学习的时候写的hello-world,形式如下

(defun hello-world () (format t "hello, world"))

分析如下

  1. 名字是hello-world
  2. 形式列表为空,因此不接受任何参数
  3. 没有文档字符串
  4. 函数体由一个表达式所构成(format t “hello,world”)

另一个列子

1
2
3
4
(defun verbose-sum (x y)
"Sum any two numbers after printing a message."
(format t "Summing ~d and ~d.~%" x y)
(+ x y))

这个函数称为verbose-sum 接受两个实参分别为形参x和y一一对应并且带有一个文档字符串,以及一个由两个表达式所组成的主体。有“+”调用所返回的值将成为verbose-sum的返回值。

函数形参列表

函数名和文档字符串差不多了,现在了解形参列表

形参列表的基本用途是为了声明一些变量用来接收传递给函数的实参,当形参列表是一个由变量名所组成的简单列表时,如同在verbose-sum里那样,这些形参叫必要形参。当函数调用时候,必须为每个必要形参都提供一个实参。每一个形参被绑定到对应的实参上。

但是,形参列表也给了更灵活的方法将函数调用实参映射到函数形参。除了必要形参以外,一个函数还可以有可选形参,或者也可以用单一形参绑定到含有任意多个额外参数的列表上。最后,参数还可以通过关键字而不是位置来映射到形参上,这样Commom lisp的形参列表对于集中常见的编码问题提供了一种便利的解决方案。

可选形参

定义

为了定义一个可选形参的函数,在必要形参的名字后放置符号&optional后接可选形参的名字

列子

(defun foo (a b &optional c d)(list a b c d))

当函数被调用时,实参被首先绑定到必要形参上,在所有必要形参都被赋值以后,如果还有任何实参剩余,它们的值被赋给可选形参。如果实参在所有可选形参被赋值前用完了,那么其余的可选形参将自动绑定到NIL上。这样,列子定义的函数会给出下面的结果

  • (foo 1 2) -> (1 2 NIL NIL)
  • (foo 1 2 3) -> (1 2 3 NIL)
  • (foo 1 2 3 4) -> (1 2 3 4)

lisp任然可以确保适当的数量的实参被传递给函数——在本列中是2-4个。而如果函数用太少或太多的参数来调用的话,会报错。

当然,我们经常想要一个不同于NIL的默认值,这时可以通过将新参名替换成一个含有名字跟一个表达式的列表来制定该默认值。只有在调用者没有传递足够的实参来为可选形参提供值得时候,这个表达式才会被求值,通常情况只是简单的提供一个值作为表达式:

(defun foo (a &optional(b 10)) (list a b))

上述函数要求将一个实参绑定到形参a上,当存在第二个实参时,第二个形参b将使用其值,否则使用10

  • (foo 1 2) -> (1 2)
  • (foo 1)-> (1 10)

不过,有时可能需要更灵活选择默认值,比如可能想要基于其他形参来计算默认值,默认值表达式可以引用早先出现在形参列表中的形参。如果要编写一个返回矩形的某种表示的函数,并且想要使它可以特别的产生正方形,那么可以使用一个像这样的形参列表。

(defun make-rectangle (width &optional (height width)) ...)
除非明确知道否则这将导致height形参带有和width形参相同的值。

有时,有必要去了解一个可选形参的值究竟是被调用者明确指定还是使用了默认值。除了代码检查是否默认值(假如调用者碰巧显式传递了默认值,那么这样做终归是无效的)以外,还可以通过在形参标示符的默认值表达式之后添加另一个变量名来做到这点。该变量将在调用者实际为该形参提供了一个实参是被绑定到真值,否则为NIL 通常约定,这种变量的名字与对应的真实形参相同,但是带有一个 -supplied-p后缀
列如:

1
2
(defun foo (a b &optional (c 3 c-supplied-p))
(lisp a b c c-supplied-p))

这将给出类似下面结果

  • (foo 1 2) -> (1 2 3 NIL)
  • (foo 1 2 3) -> (1 2 3 T)
  • (foo 1 2 4) -> (1 2 4 T)

剩余形参

可选形参仅适用于一些较为分散并且不能确定调用者是否会提供值得形参。但某些函数需要接收可变数量的实参,比如前面已经出现过的内置函数。FORMAT有两个必要实参,即流合控制串。单在这两个之后,它还需要一组可变数量的实参,这取决于控制串需要插入多少个值。

+函数也接受可变数量的实参——没有特别的理由限制它只能在两个数之间相加,它可以对任意数量的值做加法运算(它甚至可以没有实参,此时返回0——加法的底数)。下面这些都是这两个函数的合法调用:

1
2
3
4
5
6
7
(format t "hello, world")
(format t "hello, ~a" name)
(format t "x: ~d y: ~d" x y)
(+)
(+ 1)
(+ 1 2)
(+ 1 2 3)

很明显,也可以通过简单给它一些可选形参来写出接受可变数量实参的函数,但这样将会非常麻烦,光是写形参列表就已经够麻烦了,何况还要在函数体中处理所有这些形参。为了做好这件事,还不得不使用一个合法函数调用所能够传递的那么多的可选形参。这一具体数量与具体实现相关,但可以保证至少有50个。在当前所有实现中,它的最大值范围从4096到536870911

相反,Lisp允许在符合&rest之后包括一揽子形参。如果函数带有&rest形参,那么任何满足了必要可选形参之后其余所有实参就将被收集到一个列表里成为该&rest形参的值。这样,FORMAT和+的形参列表可能看起来会是这样:

1
2
(defun format (stream string &rest values) ...)
(defun + (&rest nmbers) ...)

关键字形参

如果我想给第四个形参提供值,其余不给,怎么办呢,关键字形参就起作用了。(这个没有描述清楚)

如果用可选形参的话。如果给第四个可选形参传递一个显式值,就会导致前面三个可选形参对于调用者来说变成了必要形参。

定义:在任何必要的&optional和&rest形参之后,可以加上符号&key以及任意数量的关键字形参标示符,后者的格式类似于可选形参标示符。下面就是一个只有关键字形参的函数:

(defun foo (&key a b c) (list a b c))

当调用这个函数时,每一个关键字形参将绑定到紧跟在同名键字后面的那个值上,如前面所说,关键字以冒号开始的名字,并且他们被自动定义为自求值常量。

如果一个给定的关键字没有出现在实参列表中,那么对应的形参将被赋予其默认值,如同可选形参那样。因为关键字实参带有标签,所以他们在必要实参之后可按任意顺序进行传递。例如foo可以用下列形式调用:

1
2
3
4
5
6
7
(foo)  -> (NIL NIL NIL)
(foo :a 1) -> (1 NIL NIL)
(foo :b 1) -> (NIL 1 NIL)
(foo :c 1) -> (NIL NIL 1)
(foo :a 1 :c 3) -> (1 NIL 3)
(foo :a 1 :b 2 :c 3) -> (1 2 3)
(foo :a 1 :c 3 :b 2) -> (1 2 3)

如同可选形参那样,关键字形参也可以提供一个默认值形式以及一个supplied-p变量名。

在关键字形参和可选形参中,这个默认值形式都可以引用那些早先出现在形参列表中的形参。

1
2
3
4
5
6
(defun foo (&key (a 0) (b 0 b-supplied-p) (c (+ a b)))
(list a b c b-supplied-p))

(foo :a 1) ->(1 0 1 NIL)
(foo :b 1) ->(0 1 1 T)
(foo :b 1 :c 4) ->(0 1 4 T)
(foo :a 2 :b 1 :c 4) -> (2 1 4 T)

同样,出于某种原因想让调用者用来指定形参的关键字不同于实际形参名,那么可以将形参名替换成一个列表,令其含有调用函数时使用的关键字以及用作形参的名字,比如说下面这个foo的定义

1
2
(defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p))
(list a b c c-supplied-p))

可以让调用者这样调用它:

(foo :apple 10 :box 20 :charlie 30) -> (10 20 30 T)

这种风格在想要完全将函数的公共API与其内部细节相隔离时特别有用,通常是因为想要在内部使用段变量名,而不是API中的描述性关键字。不过该特性不常被用到。

混合不同的形参类型

四个混合使用,多种混合使用时候:必须以这样的顺序声明:首先是必要形参 其次是可选形参 再次是剩余形参 最后才是关键字形参。但在使用多种类型形参的函数中,一般情况是将必要和另外一种类型形参组合使用,或者可能是组合&optional形参和&rest形参。其它两种组合方式,无论是&optional形参还是&rest形参,当与&key形参组合使用时,都可能导致某种奇怪的行为。

将&optional形参和&key形参组合使用时将产生非常奇怪的结果,因此也许应该避免将他们一起使用,问题出现在如果调用者没有为所有可选形参提供值时,那么没有得到值得可选形参将吃掉原本用于关键字形参的关键字和值。列如,下面这个函数很不明智地混合了&optional形参和&key形参:

(defun foo (x &optional y &key z)(list x y z))

如果像这样调用的话,就没问题:

(foo 1) -> (1 nil nil)

但是这样的话将报错:

(foo 1 :z 3) -> ERROR

这是因为关键字:z被作为一个值填入到可选的y形参中了,只留下了参数3被处理。在这里,Lisp期待一个成对的关键字/值,或者什么也没有,否则就会报错,也许更坏的是,如果该函数带有两个&optional形参,上面最后一个调用将导致值:z和3分别被绑定到两个&optional形参上,而&key形参z将得到默认值NIL 而不声明缺失了东西。

一般而言,如果正在编写一个同时使用&optional形参和&key形参的函数,可能就应该将它变成全部使用&key形参的形式——他们更灵活,并且总会可以在不破坏该函数的已有调用的情况下添加新的关键字形参。也可以移除关键字形参,只要没人在使用他们,一般而言,使用关键字形参就uhui使代码相对易于维护和拓展——如果需要为函数添加一些需要用到新参数的新行为,就可以直接添加关键字形参,而无需修改甚至重新编译任何调用该函数的已有代码。

虽然可以安全的组合使用&rest形参和&key形参 ,但其行为初看起来可能会有一点奇怪。正常的来讲,无论是&rest还是&key出现在形参列表中,都将导致所有出现在必要形参和&optional形参之后的那些值被特别处理——要么作为&rest和&key同时出现在形参列表中,那么两件事都会发生——所有剩余的值,包括关键字本身,都将被收集到一个列表里,然后绑定到&rest形参上,而适当的值,也会同时被绑定到&key形参上,因此给定下列函数:

(defun foo (&rest rest &key a b c) (list rest a b c))

将会得到如下结果:

(foo :a 1 :b 2 :c 3) -> ((:A 1 :B 2 :C 3) 1 2 3)

函数返回值

目前所有函数都使用了默认的返回值行为,既最后一个表达式的值被作为整个函数的返回值。这是从函数中返回值得最常见方式。

有些时候,尤其是想要从嵌套的控制结构中脱身时,如果有办法从函数中间返回,那就是非常便利的。在这种情况下,你可以使用RETURN-FROM特殊操作符,它能够立即以任何职从函数中间返回。RETURN-FROM事实上不只用于函数,还可以用来从一个由BLOCK特殊操作符所定义的代码块中返回。不过DEFUN会自动将其整个函数体包装在一个与其函数同名的代码块中,因此,对一个带有当前函数名和想要返回的值的RETURN-FROM是一个特殊操作符,其第一个“参数”是它想要返回的代码块名。该名字不被求值,因此无需引用。

下面这个函数使用了嵌套循环来发现第一个数对——每个都小于10,并且其成绩大于函数的参数,它使用RETURN-FROM在发现后立即返回该数对;

1
2
3
4
5
(defun foo (n)
(dotimes (i 10)
(dotimes (j 10)
(when (> (* i j) n)
(return-from foo (list i j))))
)
)

必须承认的是,不得不指定正在返回的函数名多少会有些不便——比如改变了函数的名字,就需要同时改变RETURN-FROM中所使用的名字,但是在事实上,显式的RETURN-FROM调用在Lisp中出现的频率远小于return语句在源自C的语言里出现的频率,因为所有的Lisp表达式,包括诸如循环和条件语句这样的控制结构,都会求值得到一个值,因此在实践中这不是什么问题。

作为数据的函数————高阶函数

使用函数主要方式—-通过名字来调用 但有时将函数作为数据看待也是很有用的。列如,可以将一个函数作为参数传递给另一个函数,从而能写出一个通用的排序函数,允许调用者提供一个比较任意两元素的函数,这样通用的底层算法就可以跟许多不同的比较函数配合使用了。

类似的,回调函数(callback)和钩子(hook)也需要能够保存代码引用便于以后运行。由于函数已经是一种对代码比特进行抽象的标准方式,因此允许把函数视为数据也是合理的。

在Lisp中,函数只是另一种类型的对象,在用DEFUN定义一个函数时,实际上做了两件事:创建一个新的函数对象以及赋予其一个名字,前面说过,也可以使用LAMBDA表达式来创建一个函数而无需为其指定一个名字。一个函数对象的实际表示,无论是有名的还是匿名的,都只是一些二进制数据——以原生编译的Lisp形式存在,可能大部分是由机器码构成。只需要知道如何保持它们以及需要时如何调用它们。

特殊操作符FUNCTION提供了用来获取一个函数对象的方法,它接受单一实参并返回与该参数同名的函数。这个名字是不被引用的。因此如果一个函数foo的定义如下。

1
2
3
4
5
? (defun foo (x) (* x x))
FOO
就可以得到如下的函数对象
? (function foo)
#<Compiled-function FOO #x302000F99E4F>

事实上前面已经用过FUNCTION了,但是它以伪装的形式出现的,前面用到的#’语法就是FUNCTION的语法糖,正如’是QUOTE的语法糖一样。因此也可以像这样得到foo的函数对象。

1
2
? #'foo
#<Compiled-function FOO #x302000ECB11F>

一旦得到了函数对象,就只剩下一件事可做了——调用它。Common Lisp提供了两个函数用来调用函数: FUNCALL和APPLY 它们的区别仅在于如何获取传递给函数的实参。

FUNCALL用于在编写代码时确切知道传递给函数多少实参时,FUNCALL的第一个实参是被调用的函数对象,其余的实参被传递到该函数中。因此,下面两个表达式是等价的:

(foo 1 2 3)= (funcall #'foo 1 2 3)

不过,用FUNCALL来调用一个写代码时名字已知的函数毫无意义。事实上,前面的两个表达式很可能被编译成相同的机器指令。

下面这个函数演示了FUNCALL的另一个更有建设性的用法。它接受一个函数对象作为实参,并使用实参函数在min和max之间以step为步长的返回值绘制一个简单的ASCII式柱状图:

1
2
3
4
L-USER> (defun plot (fn min max step)
(loop for i from min to max by step do
(loop repeat (funcall fn i) do (format t "*"))
(format t "~%")))

FUNCALL表达式在每个i值上计算函数的值,内层LOOP循环使用计算得到的值来决定向标准输出打印多少星号。

请注意,不需要使用FUNCTION或#‘来得到fn的函数值,因为它是作为函数对象的变量的值,所以你需要它解释成一个变量。可以用任何接受单一数值实参的函数来调用plot,列如内置的函数EXP,它返回以e为底以其实参为指数的值。

(plot #'exp 0 4 1/2)
*
*
**
****
*******
************
********************
*****************************************
NIL

然而,当实参列表只在运行期已知时,FUNCALL的表现不佳,列如,为了再次调用plot函数,假设你已有一个列表,其包括一个函数对象,一个最小值和一个最大值以及一个步长。换句话说,这个列表包含了你想要作为实参传给plot的所有值,假设这个列表保存在变量plot-data中,可以像这样用列表中的值来调用plot:

(plot (first plot-data) (second plot-data) (third plot-data) (fourth plot-data))

这样固然可以,但仅仅为了将实参传给plot而显式地将其解开,看起来相当不爽。

这样就需要APPLY的原因,和FUNCTION一样,APPLY的第一个参数是一个函数对象,但在这个函数对象之后,它期待一个列表而非单独的实参。它将函数应用在列表中的值上,这就使你可以写出下面的替代版本:

(apply #'plot plot-data)

更方便的是,APPLY还接受“孤立”(loose)的实参,只要最后一个参数是个列表,因此,加入plot-data只含有最小,最大和步长值,那么你仍然可以像这样来使用APPLY在该范围上绘制EXP函数:

(apply #'plot exp plot-data)

APPLY并不关心所用的函数是否接受&optional &rest 或&key实参——由任何孤立实参和最后的列表所组合而成的实参列表必定是一个合法的实参列表,其对于该函数来说带有足够的实参用于所有必要形参和适当的关键字形参。

匿名函数

一开始编写或只是使用那些可以接受其它函数作为实参的函数,你就必然发现,有时不得不去定义和命名一个仅使用一次的函数,尤其是你可能从不用名字来调用它时,这会让人相当恼火。

觉得没必要用DEFUN来定义一个新函数时,可以使用一个LAMBDA表达式创建匿名的函数。前面讨论过,一个LAMBDA表达式形式如下:

(lambda (parameters) body)

可以将LAMBDA表达式视为一种特殊类型的函数名,其名字本身直接描述函数的用途,这就解释了为什么可以使用一个带有#’的LAMBDA表达式来代替一个函数名。

(funcall #'(lambda (x y) (+ x y)) 2 3) ->5

甚至可以在一个函数调用表达式中将LAMBDA表达式用作函数名,由此一来,我们可以在需要时以更简洁方式来书写前面的FUNCALL表达式如下:

((lambda (x y) (+ x y)) 2 3) -> 5

但是几乎没人这样做,它唯一的用途是来强调将LAMBDA表达式用在任何一个正常函数名可以出现的场合是合法的。

在需要传递一个作为实参的函数给另一个函数,并且需要传递的这个函数简单到可以内联表达时,匿名函数特别有用,列如,假设想要绘制函数2x 你可以定义下面的函数:

defun double (x) (* 2 x))

并随后将其传给plot

1
2
3
4
5
6
7
8
9
10
11
12
(plot #'double 0 10 1)
**
****
******
********
**********
************
**************
****************
******************
********************
Nil

但如果写成这样就会更简单和清晰:

1
2
3
4
5
6
7
8
9
10
11
12
(plot #'(lambda (x) (* 2 x)) 0 10 1)
**
****
******
********
**********
************
**************
****************
******************
********************
NIL

LAMBDA表达式的另一项重要用途是制作闭包(closure),即捕捉了其创建时环境信息的函数。前面也使用了一点闭包,但是要想深入了解闭包的工作原理和用途,更多地还是要从变量而非函数的角度去考察,下面在研究吧。

也可以不带前缀#’来使用一个LAMBDA表达式作为FUNCALL的参数

关于这个。。。


语法

括号里都有神马

S-表达式

s-表达式基本元素是列表和原子。列表由括号包围,并可包含任何数量的由空格所分隔的元素。

空列表()也可写成NIL既是原子也是列表

数字的表示方法

  1. 123 整数123
  2. 3/7 比值七分之三
  3. 1.0 默认精度的浮点数1
  4. 1.0e0 同一个浮点数的另一个写法
  5. 1.0d0 双精度的浮点数1
  6. 1.0e-4 等价于万分之一的浮点数
  7. +42 整数42
  8. -42 附属42
  9. -1/4 比值负四分之一
  10. -2/8 负四分之一另一种写法
  11. 246/2 123另一种写法
  12. lisp复数后面第十章开始解说

-2/8和246/2等价于-1/4和123 1.0 等同于1.0e0 1.0、1.0d0 和1是不一样的,因为精度不同,代表的是不同类型的对象

字符串

  1. 字符川由双引号包括,在字符串中,一个反斜杠会转义接下来的任意字符。
  2. 两个在字符串中必须被转移的字符串是双引号和反斜杠本身
字符串 含义
“foo” 含有foo的字符串
“fo\o” 含有foo的字符串
“fo\\o” 含有fo\o的字符串
“fo\”o” 含有fo”和o的字符串
lisp中所使用的名字,诸如FORMAT  hello-world和*db*均由称为符号的对象所表示。空白字符不可以出现在列表里,只有一个句点的名字也不可以。这是个字符不能出现在名字里:开括号,闭括号,双引号和单引号,反引号,逗号,冒号,分号,反斜杠以及竖线。(加上反斜杠转义或者将有需要转义的字符串的字符名字用竖线包起来,还是可以使用的)

读取器将把foo Foo和FOO都读成FOO,但\f\o\o 和 |foo|都被读成foo这是和符号FOO不同的另一个对象,这就是为什么REPL中定义一个函数时,它的名字会被打印成大写形式的原因。

现在标准的编码风格是将代码全部写成小写形式,然后让读取器将名字转换为大写。

为了保证同一个名字总是被读取成相同的符号,读取器保留这个符号,在已读取名字将其全部转化成大写形式后,读取器在一个称为包得表里查询带有相同名字的已有符号,如果没有找到,就创建一个新符号并添加到表里,否则返回已在表中的那个符号。无论在任何地方,同意的名字出现在任何s-表达式里,都会用同一个对象去表示它

全局变量名字开始和结尾处有*

常量名开始结尾处有+

有些程序员将特别低层的函数名前面加%或者%%

语言标准所定义的名字只能使用字母字符表(A-Z)外加* + - / 1 2 < = > 以及&这些符号

目前要求

只用列表,数字,字符串和符号就可以描述很大一部分的lisp程序呢其他的后面说目前关键是要理解怎样用数字,字符串和由符号借助括号所组成的列表式构建s-表达式,从而表示任意的树状对象。下面是一些简单的列子。

  1. x 符号x
  2. () 空列表
  3. (1 2 3) 三个数字组成的列表
  4. (”foo” “bar”) 两个字符串组成的列表
  5. (x y z) 三个符号所组成的列表
  6. (x 1 “foo”) 一个符号,一个数字和一个字符串所组成的列表
  7. (+ (* 2 3)4)一个符号,一个列表,一个数字组成的列表

下面这种四元素列表就稍显复杂了,它含有两个符号,空列表以及另一个列表-其本身又含有两个符号和一个字符串:

1
(defun hello-world () (format t "hello, world"))

作为Lisp形式的S-表达式

  1. 读取器把大量文本转化为s-表达式后,这些s-表达式随后可以作为lisp形式被求值。(有些没必要求值)
    任何一个原子(非列表或空列表)都是一个合法的Lisp形式

  2. 作为最简单的lisp形式,原子可以被分成两个类别:符号和所有其他内容。符号在作为lisp形式被求值时会被视为一个变量名,并且会被求值为该变量的当前值。

  3. 所有其他的原子,包括数字和字符串都是自求值对象。
    比如输入10和helloworld

  4. 把符号变成自求值对象也是可能的,它所命名的变量可以被赋值成符号本身的值。两个以这种方式定义的常量是T和NIL,既所谓的真值和假值

  5. 另一类自求值符号是关键字符号-以名字冒号开始的符号。当读取器保留这样一个名字时,它会自动定义一个以此命名的常值变量并以该符号作为其值。
1
当我们开始考虑列表的求值方式时候,事情变得更加有趣了,所有合法的列表形式均以一个符号开始,但是有三种类型的列表形式,他们会以三种相当不同的方式进行求值,为了确定一个给定的列表式哪种形式,求值器必须检测列表开始处的那个符号是一个函数,宏还是特殊操作符的名字。如果该符号尚未定义,比如说当你正在编译一段含有对尚未定义函数的引用代码时,它会被假设成一个函数的名字。我将把这三种类型的形式称为函数调用形式,宏形式和特殊形式。

common lisp中一个符号可以同时为操作符(函数,宏或特殊操作符)和变量命名

函数调用

函数调用形式的求值规则很简单,对以lisp形式存在的列表其余元素进行求值并将结果传递到命名函数中,这一规则明显有着附加的句法限制在函数调用形式上:除第一个以外,所有的列表元素他们自身必须是一个形态良好的lisp形式,换句话说,函数调用形式的基本语法应如下所示,其中每个参数本身也是lisp形式:
(function-name argument*)

  1. (+ 1 2)首先求值1,再求值2然后将得到的值传给+函数再返回3
  2. (+ 1 2)(- 3 4))先求参数的值为3和-1最后传递到函数里,从而得到-3

特殊操作符

并非所有操作都可以定义成函数,由于一个函数的所有参数在函数调用之前都将被求值,因此无法写出一个累世IF操作符那样的函数

当列表的第一个元素是一个由特殊操作符所命名的符号时,表达式的其余部分将按照该操作符的规则进行求值。

IF操作符

IF的规则相当简单:求值第一个表达式。如果得到非NIL,那么求值下一个表达式并返回它的值,否则,返回第三个表达式的求值,或者如果第三个表达式被省略的话,返回NIL。换句话说,一个IF表达式的基本形式是像下面这样:

(if test-form then-form [else-form])

其中test-form将总是被求值 ,然后要么是then-form要么是else-form

QUOTE操作符

一个更简单的操作符是QUOTE,它接受一个单一表达式作为其“参数”并简单的返回他,不经求值,列如,下面的表达式求值得到列表(+ 1 2),而不是值3:

(quote (+ 1 2))

QUOTE用得非常普遍,以至于读取器内置一个它的特别语法形式,还可以写成 ‘(+ 1 2) 该语法是读取器所理解的s-表达式语法的小扩展

一般来说,特殊操作符所实现的语言特性需要求职器操作出某些特殊处理。列如,有些的操作符修改了其他形式的求值环境,其中之一是LET,下面会详细解说,它用来创建新的变量绑定。下面的形式求值得到10,因为在第二个x的求值环境中,它是由LET赋值为10的变量名:

(let ((x 10)) x)

由于特殊操作符数量在标准语言中是固定的,然而宏却能提供给语言用户另一种语法扩展方式。宏是以一个s-表达式为其参数的函数,并返回一个lisp形式,然后对其求值并用该值取代宏形式。宏形式的求值过程包括两个阶段:首先,宏形式的袁术不经常求值既被传递到宏函数里,其次,由宏函数所返回的形式(展开式)按照正常的求值规则进行求值。

。。。

真 假和等价

真假

  1. 符号NIL式唯一的假值,其他所有都是真值
  2. 符号T式标准的真值,可用于需要返回一个非 NIL值却又没有其他值可用的情况。
  3. 关于NIL唯一一个麻烦的一点是,它是唯一一个即是原子又是列表的对象:除了用来表示假以外,它还用来表示空列表。
  4. nil () ‘nil和’()这四个求值结果一样的
  5. t和‘t求值结果也完全相同:符号T

等价

  • =用来比较数字
  • CHAR=用来比较字符

等价时返回真,否则假

  1. EQ 只有两个对象相同时才是eq等价的(最好不是数字字符的比较时候用)
  2. EQL (eql 1 1)真 (eql 1 1.0)假
  3. EQUAL 将在递归上具有相同结构和内容的表现视为等价,也认为含有相同字符的字符串是等价的,它对于位向量和路径名也定义了比eql更加宽松的等价性
  4. EQUALP 和equal相似,但是比较字符串忽略大小写区别,字母相同就是等价,只要数字表示相同数学意义上的值,他们在equalp下面就是等价的,因此(equalp 1 1.0)是真的。由equalp等价的元素组成的列表也是equalp等价的。同样的,带有equalp元素的数组也是equalp等价的,和equal一样,还有一些其他暂时没讲到的数据类型,equalp可认为两个对象是等价的,但是eql和equal则不会,对于所有其他数据类型,equalp会退到eql水平上。

格式化Lisp代码

代码缩进

格式化代码关键在于正确缩进它,这一缩进应当反映出代码结构,这样就不用数括号了。一般而言,每一个新的嵌套层次都需要多缩进一点儿,并且如果折行是必须的,位于同一个嵌套层次的项应当按行对齐。
这样,一个需要跨越多行的函数调用可能就会被写成这样:

1
2
some-function arg-with-a-long-name
another-arg-with-an-even-longer- name

那些实现控制结构的宏和特殊形式在缩进上稍有不同:“主体”元素相对于整个形式的开括号缩进两个空格。就像这样

1
2
3
(defun print-listlist
(dolist (i list)
(format t "item: ~a~%" i)))

注释

;;;; 四个分号用于文件头注释

;;; 带有三个分号的注释将通常作为段落注释应用到接下来的一大段代码上

;; 两个分号说明该注释应用于接下来的代码上。注意,该注释与其所应用的代码具有相同的缩进

 ;;;这就是一大堆代码
(defun foo (x)
    (dotime (i x)
         ;;这就是两个注释
        (some-function-call
        (another i)
        (add-another)
        (baz)))

!–more–>

iphoneSize

规格 iphone4_ios6 iphone5_ios6 iphone4_ios7 iphone5_ios7
尺寸未除2 640*960 640*1136 640*960 640*1136
head (0,0,width,44) (0,0,width,44) (0,20,width,44) (0,20,width,44)
center (0,44,width,height-88) (0,44,width,height-88) (0,64,width,height-108) (0,64,width,height-108)
foot (0,height-44,width,44) (0,height-44,width,44) (0,height-44,width,44) (0,height-44,width,44)
规格 iphone4_ios6 iphone5_ios6 iphone4_ios7 iphone5_ios7
尺寸 640*960 640*1136 640*960 640*1136
center大小 320*372 320*460 320*372 320*460
iphone4和iphone5只有高度相差88

表格中的height是self.view.frame.size.height,里面的width也是一样

ios6和ios7的self.view.frame.size是不一样的

简单的数据库

CD和记录

为了记录一些CD中MP3的信息,需要做一个数据库。
数据库每条记录包含CD标题和艺术家信息,一个多少用户喜欢它的评级,以及一个表示是否已经转换过的标记。
所以,需要一种方式表示一条数据库的记录(也就是一仗CD)最简单的就是使用列表

列表

(list 1 2 3)会得到(1 2 3)

四元素列表

属性表

以冒号开始的名字为关键字符号

(list :a 1 :b 2 :c 3)

(:A 1 :B 2 :C 3)

GETF函数

getf接受一个plist和一个符号,并返回plist中跟在那个符号后面的值

(getf (list :a 1 :b 2 :c 3):a)得到1

(getf (list :a 1 :b 2 :c 3):c)得到3

make-cd函数

1
2
(defun make-cd (title artist rating ripped)
(list :title title :artist artist :rating rating :ripped ripped))

defun告诉我们正在定义一个函数

函数名是make-cd,函数名都是-在中间命名,这个和其他语言区别啊

跟在名字后面的是形参列表,这个函数有四个形参 title artist rating 和ripped

形参列表后面的都是函数体,上面的函数体只有一个形式,即对list的调用,档make-cd被调用时,传递给调用的参数被绑定到形参列表中的变量上。

列如,为了建立一个关于Kathy Mattea的名为Roses的CD的记录,可以这样调用make-cd

(make-cd "Roses" "Kathy Mattea" 7 t)

回车返回(:TITLE “Roses” :ARTIST “Kathy Mattea” :RATING 7 :RIPPED T)

录入CD

DEFVAR宏定义全局变量*db*

只有单一记录还不能算是一个数据库,需要一些更大的结构来保存记录,出于简化目的,可以使用一个全局变量*db*

命名中的*号是Lisp的全局变量命名约定

(defvar *db* nil)

可以用PUSH宏为*db*添加新的项,稍微抽象一些,因此可以定义一个函数add-record来给数据库增加一条记录

其实就是插入数据

(defun add-record (cd) (push cd *db*))

插入数据

现在可以讲add-record和make-cd一起使用,为数据库添加新CD记录了

(add-record (make-cd "Roses" "Kathy Mattea" 7 t))

(add-record (make-cd "Qoses" "Kathy Mattea" 6 t))

每次都PUSH返回正在修改的变量的最新值

查看数据库的内容

无论如何,输入*db*都可以看到*db*的当前值

但是这个查看方式不爽,不是人类看的,可以用一个dump-db函数讲数据库变成有格式的,符合人类看的

比如

TITLE: home
ARTIST: da
RATING: 5
ROPPED: T

TITLE: homeqe
ARTIST: da
RATING: 6
ROPPED: T

全局变量一个缺点是每时每刻只能有一个数据库

1
2
3
(defun dump-db ()
(dolist (cd *db*)
(format t "~{~a: ~10t~a~%~}~%" cd)))

这个函数的工作原理就是在使用DOLIST宏在*db*的所有元素上循环,依次绑定每个元素到变量cd上,然后使用format函数打印出每个cd的值

format至少接收两个参数,第一个是用来发送输出的流,t是标准输出流的简称。

第二个蚕食是一个格式字符串,内容既包括字面文本,也包括那些告诉format如何插入其余参数等信息的指令,。格式指令以~开始,类似于printf指令以%开始,

~a指令是美化指令,它的意图是消耗一个参数,然后将其输出成人类可读的形式,这将使得关键字被渲染成不带前导冒号的形式,而字符串也不再有引号呢,

比如(format t "~a" "jing yu")

输出

jing yu

 NIL

~t指令用于制表,~10t告诉format产生足够的空格,以确保在处理下一个~a之前将光标移动10列。~t指令不适用任何参数

(format "~a:~10t~a" :artist "Dixie Chicks")

输出

1
2
3
Dixie Chicks

NIL

现在事情复杂了,档format看到~{的时候,下一个被使用的实参必须是一个列表。format在列表上循环操作,处理位于~{和~}之间的指令,同时在每次需要时,从列表上使用尽可能多的元素,在dump-db里,format循环将每次循环时从列表上消耗一个关键字和一个值。

~%指令并不消耗任何实参,而只是告诉format来产生一个执行,然后再~{循环结束以后,最后一个~%告诉format再输出一个额外的换行,以便在每个cd数据间产生一个空行

从技术上来讲,也可以使用format在整个数据库本身循环,从而将dump-db函数变成只有一行

1
2
(defun dump-db ()
(format t "~{~{~a: ~10t~a~%~}~%~}" *db*))

最牛逼的FORMAT指令~R指令

用英语说一个大数字

(format nil "~r" 3489142793724123341)

改进用户交互

这个界面不友好对于普通用户 ,变成提示用户输入的

1
2
3
4
(defun prompt-read (prompt)
(format *query-io* "~a: " prompt)
(force-output *query-io*)
(read-line *query-io*))

1
2
3
4
5
6
(defun prompt-for-cd ()
(make-cd
(prompt-read "Title")
(prompt-read "Artist")
(prompt-read "Rating")
(prompt-read "ripped [y/n]")))

1
2
3
4
5
6
(defun prompt-for-cd ()
(make-cd
(prompt-read "Title")
(prompt-read "Artist")
(or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
(y-or-n-p "Ripped [y/n]: ")))

1
2
3
(defun add-cds ()
(loop (add-record (prompt-for-cd))
(if (not (y-or-n-p "Another? [y/n]: ")) (return))))

保存和加载数据库

1
2
3
4
5
6
(defun save-db (filename)
(with-open-file (out filename
:direction :output
:if-exists :supersede)

(with-standard-io-syntax
(print *db* out)))
)

如果退出了下次数据没了就吊了,所以必须保存

1
2
3
4
(defun load-db (filename)
(with-open-file (in filename)
(with-standard-io-syntax
(setf *db* (read in))))
)

读取数据

load-db之前要确保保存了,如果save-db后又加入了一些数据,这个时候load-db那么这些刚加入的数据就会丢失

查询数据库

1
2
(remove-if-not 
#'(lambda (cd) (equal (getf cd :artist) "Dixie Chicks")) *db*)

1
2
3
(defun select-by-artist (artist)
(remove-if-not
#'(lambda (cd) (equal (getf cd :artist) "sfada")) *db*))

更新已有记录

消除重复,获益良多

总结

我已经不想写了,也没有完全看下去,最后的部分主要是加入数据库的几个功能-增加删除修改查询

昨天晚上今天晚上两个晚上没有看出什么,唯一的感觉就是这个好像真的很牛逼,几个方法就U可以实现增删查改

Common lisp

我为什么学习lisp

至于我为什么学习lisp,这一切都源于Emacs,因为Emacs我知道了lisp,知道它很牛逼,在一些unix论坛也发现很多人说lisp很牛逼。后来学习了几天就没有坚持下去,至于没有坚持下去可能是觉得这个没什么用处吧,这次看完黑客与画家又一次听说lisp很牛逼,实在无法忍受,于是这次开始学习lisp。看看到底有没有外界说的那么牛逼。思想上能有什么启发。

lisp环境的安装

我对lisp的历史,分支什么的不是太感兴趣,只是在书中稍微了解了下,直接开始学习了。
Common lisp学习环境可以下载一个lispbox或者ccl(这个是Mac上面当今最主流的)。

lispbox下载链接
其实lispbox就是Emacs配置的

CCL可以直接从AppStore下载

这两个在Mac上面其实都很简单,不需要怎么配置就可以直接使用,但是我很喜欢CCL,因为它简单而又强大。

Hello World

  1. “hello,world”
  2. (format t "hello,world")
  3. (write-line "hello,world")
  4. (print "hello,world")
    这四种各有含义。

anytime

好久没有写了,今天写一个留下,看看有没有生疏

以前买东西的时候总是在乎涨价了,降价了,比价什么的,这个固然重要,但是只要自己开心就好,

game

#游戏开发
游戏开发最重要的就是选好游戏引擎

##游戏引擎
我所知道的公开的比较红得也就那几个

  1. cocos2d (已经出局)
  2. cocos2d-x
  3. Unity3d
  4. sprite kit

#性能对比

1
2
3
4
5
6
7
表格使用
sprite kit | cocos2d-x |unity3d
-|-|-|

fddfs | df | fds/|
1. 最后一个转义字符/
2. 每个环境对语法的支持不一样,效果不一样,主题也可以决定效果
3. hexo里面第二排必须n个-| -|标书一列

sprite kit cocos2d-x unity3d
优点 fddfs df fds/
缺点 s s

##我的结论
游戏开发只选apple的sprite kit虽然这个不稳定,
后起,如果苹果倒了,就全倒了

#数学公式

数学公式

1
2
3
4
5
6
7
以下是几个三角恒等式:
<script type="math/tex; mode=display">
\begin{align}
\sin \left(x+y\right)=\sin x \cos y + \cos x \sin y\\
\cos \left(x+y\right)=\cos x \cos y - \sin x \sin y
\end{align}
</script>

以下是几个三角恒等式: