django核心处理流程 3. wsgi

wsgi 是 python 服务器与 web 应用间交互的通用接口,实现该接口的应用可以通过任何实现了该接口的服务器部署。

wsgi 诞生的背景

我们知道,接口可以有效地解耦调用方与被调方。

拿 cmq 举例,如果我们实现了它所规定的相关接口,就可以提供给调用方使用,而不必关心它是否是原生的 cmq。

最开始的 web 框架花样繁多,服务的部署多种多样,往往框架的选择就已经限制了服务器的选择。而 wsgi 的目标,就是将框架的选择与Web服务器的选择解耦。

需要做哪些工作

我们先思考一下,如何分离这两者,提供一个统一的界面接口。

首先,在我们的框架里,是不需要关心 http 细节的,这就需要把 http 的相关信息以一种通用的格式打包过来。一种比较直观的结构是一维字典,或者是一个元组列表。

拿到处理的信息后,我们可以对其进行解析,然后执行处理逻辑,返回 http 响应。

响应当然可以直接返回一个 http 协议文本,但是 web 应用不该过多参与 http 细节。这里将返回分解为了三部分,返回状态说明、响应头、响应体。

我们知道,响应头是必须先于响应体的,那么在技术上如何保证呢?

wsgi 给出一个 start_response 参数,这是一个可调用函数,以返回状态说明和响应头为参数。如此我们便可以判断其与响应体的顺序了。

另外还有一个问题,一个请求的响应体可能会分多段返回,比如我们要对一个列表的每个元素做计算,由于计算量很大,我希望每算得一个结果就返回一个结果。所以这里我希望能以 yield 的方式来做返回。

跟据上面一些问题的考量,我们就可以得到一个近似 wsgi 的接口了:

1
2
3
4
5
6
7
8
9
def simple_app(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)

yield 'hello\n'
time.sleep(3)
yield 'world\n'

实际返回的 headers 跟这里的 response_headers 并不完全一致,应用只负责它关心的 header,剩下的就交给 wsgi 服务器了。

wsgiref 模块

python 的 wsgiref 模块实现了一个简单的 wsgi 服务器,我们可以借助它来让我们前面写的一些代码变为运行时。

首先,我们把之前的逻辑包装为一个标准的 wsgi 应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_wsgi_application(root_urlconf):
"""
将ROOT_URLCONF中配置的路由,包装为一个wsgi应用
:param root_urlconf: 字符串,根url模块位置
:return:
"""
def wsgi_application(environ, start_response):
"""标准wsgi程序"""
request = HttpRequest(environ)
response = handle_view(request, root_urlconf)

status = STATUS_TO_DISPLAY[response.status_code]
headers = []
start_response(status, headers)

ret = [response.content.encode('utf-8')]
return ret

return wsgi_application

然后,我们将该应用传递给 wsgi 服务器,让它把这个应用运行起来:

1
2
3
4
5
wsgi_application = get_wsgi_application(settings.ROOT_URLCONF)

with make_server(host, port, wsgi_application) as httpd:
print(f"Serving on port http://{host}:{port}...")
httpd.serve_forever()

此时,我们访问 http://127.0.0.1:5302/welcome/oog 就可以看到下面的提示了:

1
Hello oog, welcome to jango.

发挥 wsgi 的优势

我们前面说,wsgi 可以解耦应用和服务器,也就是说,我们的应用同样可以部署在其它 wsgi 服务器下。

这里我们尝试使用 gunicorn 来验证该服务是否可以在其它 wsgi 服务器下正常运行。

首先我们创建一个脚本来生成一个 wsgi 接口的函数:

1
2
3
# demo/wsgi.py
settings = importlib.import_module('demo.settings')
application = get_wsgi_application(settings.ROOT_URLCONF)

然后,我们在命令行启动 gunicorn,并将该 application 传递给它:

script
1
gunicorn -b 127.0.0.1:5302 -w 4 demo.wsgi:application

这里我们借助 gunicorn 的支持,启动了 4 个 worker,一切运行正常。wsgi 做得很好。

到这里,我们就已经成功把静态代码部署为一个服务器了。相关代码可以在 https://github.com/pysnow530/jango 找到。

虽然这里只有一些核心逻辑,但是已经看到了一些 django 的脉络。

当然,django 的能力不止于此,它实际上是一个框架的框架,中间件、灵活的配置系统、第三方模块,使得它可以在系统层面做进一步的抽象。它的思想远不及此,不过这些已超出本文的范围,就此打住了。

这只是旅途的一个开始,想要的答案,让我们去 django 的源码里找吧。

一些重要细节

上面的响应体使用了 yield 和 list 两种返回形式,本质上所有的可迭代对象都可以作为响应体返回使用。

同样的,这里所说的函数,也可以替换为其它可执行对象,包括实现了 __call__ 方法的类。

扩展资料

wsgi 协议可参考

django核心处理流程 2. 请求响应逻辑

web 框架的设计,将每一个资源地址映射到一个处理逻辑。这篇文章将讲述 django 中对请求的解析处理,及处理结果的返回。

视图函数

资源地址对应的处理逻辑,在 django 中叫做视图(view)。

视图是一个可调用对象,一般为函数。

请求的上下文被打包到一个叫做 HttpRequest 的对象中,并传入视图函数。函数处理后,将结果打包到一个 HttpResponse 对象,并返回给主调方。

这里的主调方,其实就是我们的框架了。

下面是一个比较典型的视图函数:

1
2
3
4
def welcome(request, nickname):
content = f'Hello {nickname}, welcome to jango.'
response = HttpResponse(content=content)
return response

这个示例函数的结构比较清晰。需要注意的是,参数里有一个 nickname,django 会把资源地址中的匹配参数一并作为视图参数传递过来。

与该函数绑定的 url 为:

1
[r'/welcome/(?P<nickname>\w+)', views.welcome]

可以看到 nickname 的来源及解析方式。

请求上下文

那么请求的上下文是如何构造出来的呢,下面就是构造的过程:

1
2
3
4
5
6
class HttpRequest:

def __init__(self, environ):
self.method = environ['REQUEST_METHOD']
self.path_info = environ['PATH_INFO']
self.environ = environ

我们可以看到,HttpRequest 是通过解析一个 environ 字典生成的。environ 字典里包含了请求的上下文信息。至于 environ 从何而来,它实际上是 wsgi 接口定义。我们在下篇文章展开。

environ 里包含了所有请求相关的信息,上例中的 path_info 实际上就是我们的请求地址了,比如前面的 '/welcome/oog'

HttpResponse 也会解析请求参数及 COOKIE 等,这样我们就可以很方便的获取需要的信息了。

http 响应

请求被处理完后,视图函数需要返回一个 HttpResponse 对象。该对象包含了需要返回的信息。

一个 http 请求返回的结果,比较典型的有下面几项:

  1. status code,它定义了请求被处理的结论,比如 200 表示成功,302 表示该资源需要跳转到其它资源地址等
  2. body,比如一段 html 代码,或者是一个故事的文本描述
  3. content type,标识 body 的类型,以方便资源请求方正确理解它。比如 text/html、text/plain 等类型

HttpResponse类的定义如下:

1
2
3
4
5
6
class HttpResponse:

def __init__(self, content, content_type='plain/text', status_code=200):
self.content = content
self.content_type = content_type
self.status_code = status_code

请求、响应流程

现在我们已经有请求解析和响应对象的构造,一个请求过来是如何发生的呢?

我们先来看 8 行代码:

1
2
3
4
5
6
7
8
9
10
11
def wsgi_application(environ, start_response):
"""标准wsgi程序"""
request = HttpRequest(environ)
response = handle_view(request, root_urlconf)

status = STATUS_TO_DISPLAY[response.status_code]
headers = []
start_response(status, headers)

ret = [response.content.encode('utf-8')]
return ret

代码中,得到 request 后,调用函数并获取 response,然后把关键信息按需要的格式返回即可。

其中 handle_view() 是一个 10 行函数,功能是根据 url 找到视图函数并执行。具体参见 jango 的源码 https://github.com/pysnow530/jango

这里的结果渲染部分初看会有点奇怪,这里其实是 wsgi 定义的规范,后面的文章会讲到。

到这里,我们就已经完成了一个 url 从请求匹配,到视图执行,最后到请求响应的完整过程。文章里讲到的 wsgi 将会在下一篇文章细说。

一些重要细节

我们上面看到的视图,都是一些函数的形式。如果看 django 的官方文档,会发现 django 也提供了一种类的方式,类名对应请求名。

1
2
3
4
5
6
7
class Welcome(View):

def get(self):
"""GET方法"""

def post(self):
"""POST方法"""

但是不要被外在形式迷惑,类形式的视图本质也是一个可调用对象,只是通过 Welcome.as_view() 包装的语法糖方便使用。具体可参考 django 源码。

另外一点,http 比例子中讲到的功能要更为丰富,它还涉及了文件的上传下载等功能。具体可参考扩展资源给出的 rfc 文档。

扩展资料

http 协议可参考 https://tools.ietf.org/html/rfc2616

django核心处理流程 1. 路由

路由的设计是每一个 web 框架都必须优先考虑的问题,它决定了资源地址在代码中的组织方式。

数据结构

从本质上讲,路由表维护了 url 地址到业务逻辑代码的映射关系。

在 django 中,为了使 url 规则更为灵活,采用了正则表达式的方式来匹配 url 地址。

正则表达式是一种模糊匹配,同一个 url 可能会对应多个正则表达式。所以 django 中的路由是一个列表配置。列表中的每一项是一个 (pattern, view) 对。

为了便于模块化,django 还提供了第二种配置项 (pattern, conf_module),用于将一组 url 映射到某个前缀下。所以整个路由设计的结构上是一棵树。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
# urls.py
urlpatterns = [
['/welcome', views.welcome],
['/api/suburls', 'suburls'], # 以/api/suburls作为url前缀,suburls模块中的其它项作为剩余部分
]

# suburls.py
urlpatterns = [
['/url1', views.url1], # 实际访问地址为/api/suburls/url1
['/url2', views.url2], # 实际访问地址为/api/suburls/url2
]

假如 suburls 是一个成熟的模块,我们就可以使用这种方式给该模块分配一个前缀,如此就可以投入使用了。

下面是一个很经典的例子,将 django 内置的后台管理模块配置到 /admin 下:

1
2
3
urlpatterns = [
path('admin/', admin.site.urls),
]

查找算法

上面提到,django 中的路由实质上是一棵树的结构,所以查找上也是树的遍历算法。由于我们想借助列表结构来说明优先级,这里必然要使用深度遍历。

我们首先会从 settings 配置找到根 url 模块,然后遍历模块中的 urlpatterns 列表,如果匹配到的是一个模块,就继续匹配模块中的列表。如果最后没有找到可执行的逻辑,就是我们常见的 404 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# demo/settings.py
ROOT_URLCONF = 'demo.urls'

# jango/url.py
def resolve(path_info, urlconf):
"""匹配请求url,返回匹配项"""
urlconf_module = import_module(urlconf)
urlpatterns = urlconf_module.urlpatterns

for pattern in urlpatterns:
regex = pattern[0]
re_matched = re.match(regex, path_info)

# ['hello/', view]
if re_matched and callable(pattern[1]):
callback = pattern[1]
callback_kwargs = re_matched.groupdict()
groups = re_matched.groups()
callback_args = groups[:len(groups) - len(callback_kwargs)]
return callback, callback_args, callback_kwargs

# ['api/', 'demo.urls']
elif re_matched and isinstance(pattern[1], str):
resolve_match = resolve(path_info[re_matched.end():], pattern[1])
if resolve_match:
return resolve_match

return None

其中,参数 path_info 是需要解析的url。解析后,我们可以获取执行的函数,及前端传过来的 url 参数。后面就可以调用具体函数执行了。

代码地址可参考 https://github.com/pysnow530/jango

一些重要细节

当然,上面的模型只是一个简化,这里提一下原生 django 几个比较重要的细节。

第一,我们说匹配规则是一个正则表达式,但是 django 已对正则表达式做了封装,提供了更简单直白的用法,比如 /welcome/<name>。更多的限制,必然是一种发展趋势,它让我们更好的关心逻辑代码而非技术。

第二,为减少封装了解本质,我们把路由中的每一个配置项定义为一个列表,实际 django 中是以一个类来替换的,作用类似,只是抽象程度更高。

第三,django 支持给路由配置项命名,并在需要时支持将名称反转为 url。如在模板中的 { % url 'api_welcome' 'name' % } 或代码中使用 reverse('api_welcome', args=('name',)),这让修改 url 几乎零成本,是一个优秀的设计。

说了这么多,路由的核心简单且简洁,就是一个映射表。通过它,我们能找到资源地址对应的逻辑代码。

下一篇文章,我们来看一下逻辑代码是如何组织及工作的。

读《悟空传》

“””
有人说《悟空传》颠覆西游,其实我一点儿没觉得颠覆,我觉得我写的就是那个最真实的西游,西游就是一个很悲壮的故事,是一个关于一群人在路上想寻找当年失去的理想的故事,而不是我们一些改编作品里面表现的那样,就是打打妖怪说说笑话那样一个平庸的故事。

也正是这样,我十分受不了西游里面就只有打妖怪打妖怪,打你妹啊。西游的主题根本就不是打妖怪。妖怪只分两种:一种是当年跟着孙悟空一起反抗天庭的兄弟,像牛魔王之类的,孙悟空必须把当年和他一起战天斗地的结拜兄弟都干掉,就为了成佛,我觉得这就是最大的悲剧;另一种则是神仙安排下来的,不是这个的坐骑就是那个的宠物。这也太恶心了,一边让人去西天一边安排着九九八十一难,就想把你整死。所以整个西游就是一出悲剧,是一场阴谋,不论你怎么做,都是死路一条。你不服从神,不向西走,整死你;你向西走,一路上九九八十一难,都是神安排的,依然整死你。最后到了西天,你以为成功了,结果给你一部经书还是假的,全是白纸;你拿回去退货,送了礼,给你一部有字的,你以为是真的,是真的吗?其实还是假的,因为本来无一物,何处惹尘埃。所谓道不可道,我们说了别人的答案不是你的答案,如果有人要拿答案灌输给你,那不是为了让你聪明,更可能是想让你变傻。

最后四个人成了佛,成佛以后呢?没有了,什么都没有了。以前活生生的有血有肉有感情有梦想的四个人,一成了佛,就完全消失在这个世界上了。佛是什么?佛就是虚无,四大皆空。什么都没有了,没有感情没有欲望没有思想,当你放弃这些,你就不会痛苦了。但问题是,放弃了这些,人还剩下什么?什么都没了,直接就死了。所以成佛就是消亡,西天就是寂灭,西游就是一场被精心安排成自杀的谋杀。
“””

记一个关于星河的梦

不是真的星河,大家说的星河是很多星星在夜空中汇聚成一条大河的形状。

那时候已经是很晚了,感觉四下昏暗暗的。我路过一座桥,桥面却被月光照的很亮。

不经意间抬起头,发现天上竟然有一条河流在流淌。

河流很宽,在夜空中有点昏暗,能很清晰的看到水波。河流很长,一眼望不到边际,只是闪烁着流淌了去。

我呆呆地站在那里,竟然有种不知道从哪里来的感动。

正当我看得出神时,天空渐渐暗了下来,最后只剩漆黑一片,什么都看不到了。

读《三体》

《三体》是刘慈欣创作的系列长篇科幻小说,一共分三部分。

我在一年前就已经开始了阅读,读到第二部分的开头,就读不下去了。原因可能是场景转变太大,一下子没产生读第一部时的兴趣。

跟一个朋友交流,他也遇到了同样的问题。网上也有人读到放弃,但原因是太多物理知识不好理解。不过我觉得对于一本小说来说,不必要这么较真。

大约过了大半年,接近19年年终,我又拾起了这部小说,在江西的一段时间,彻底读完。

没有太体会到大部头,kindle 给人的感觉飘飘忽忽,对字数和厚度没有太大感觉。

那时坐在三楼的阳台,冬天的阳光有点暖,照得人很舒服,算是比较惬意的一段经历。

科幻小说

这是一本科幻小说,也就是科学幻想小说,它的基础是科学,所以有很多内容是真实的,在科学的基础上,开了一些脑洞。

还有一类小说叫文玄幻小说,这一类就不需要科学基础了。

之前饺子导演的一部动画片《哪吒》,哪吒从结界偷跑出去了,两个把门的小妖对话,”很玄幻”,”不科学”。对仗之工整,令人吧服。

小说发生的背景是宇宙,至于具体的故事概要,没有提及的必要,小说嘛。

优秀小说的一个标准

《得到》上有一个课程,叫做《跟着李新学编剧》,发刊词里讲到神作跟口水剧的区别,里面讲到一个标准我觉得很好,”一部神作可以让你搞懂一个行业的底层逻辑”。

这种底层逻辑是需要大量知识积累和大量精力的,从这一点也可以看出作者是否用心去写作或者创作,还是只是赚个流量敷衍了事。

通过阅读《三体》这部小说,一方面可以跟着作者对人类文明做一些更加深度的思考,同时也可以了解一些宇宙科学知识,是比较值得的一部小说。

通过这一条标准,很容易联想到《疯子在左,天才在右》这本书,不过不太好对其归类,只是做个参考比较吧。

对于人类科学的思考

一个确定的事实是,人类的科学是有局限的,而且是必然不完整的科学。

所以人类定义的时间才会从大爆炸开始,大爆炸之前是什么,没有人会知道。

小说里提到的黑域就是一个很有意思的概念,作者说最开始光的速度是无限的,但是我们可以通过一些技术来将某一区域的光速设置一个上限,甚至可以将某一个区域通过这种方式与外部宇宙隔离。

比如人类现在发现的光速是30万千米,但为什么是30万而不是40万或者50万,如果不是依赖其它物理因素,这在科学上就不是很完美。

人类永远不可能解开所有的疑惑。

读《松本行宏的程序世界》

这本书在一个月前就已经读完了,对于一个科班生来说,并没有太多新的内容。但是里面有一些很有意思的点,这里顺带写下来,以对之前付出的时间精力有个交待。

这是一本偏重编程思想的书,作者是 ruby 之父松本行宏。

作者在书里也说到,这并不是讲 ruby 使用的手册,而更多是使用 ruby 或者其它语言来说明某些思想。

不过这里还是想说一下自己对 ruby 这门语言的看法。在设计 ruby 语言时,松本行宏从 lisp 里借用了很多思想;而它的成功很大程度借助了 ruby on rails。这也说明了 ruby 有某些性质,更容易实现一个更为灵活易用的 web 框架。

书中大部分知识点都是一些最为基础、必知必会的内容,记笔记意义不会很大,所以这里只记录一下自己感觉有些意思的地方。

我为什么开发 ruby

这实际上是第1章的标题。作者讲自己开发 ruby,最开始是出于兴趣爱好,希望自己能够轻松编程,提高开发效率,不想开源后变得流行并变成了自己的一个职业。

计算机语言不少,作者学习其它语言,并根据自己的理解设计及发展了 ruby。从某种程度上说,一门语言的设计表现了作者对某些特性的取舍,实际上也是他对编程语言世界的理解。

作者对自己设计的 ruby 语言提出了 3 个设计原则:

  1. 简洁性
  2. 扩展性
  3. 稳定性

对于简洁性,作者形容为”能直接运行的伪码式编程语言”。应该算是很直观的描述了,单从结构形式上来说,ruby 编写的代码是比较易读的。

ruby 意为红宝石,是七月的诞生石。而 perl 音同 pearl,是六月的诞生石。可见 ruby 也从 perl 里选取了一些特性。

有一个段子说 perl 和 ruby 的作者是语言学家,而 python 的作者是数学家,所以前两者注重一个问题多种解法,而后者注重只提供最优解法。

实际上松本行宏也给出了答案,他希望使用这门语言是一件很有趣的事。当然,有一些语言也标榜自己的优势是乏味,毕竟做工程嘛。

对象并非对具体物体的反映

很多地方说面向对象是对自然界系统的一个模拟,或者说对现实世界物体的模拟。作者说这种说法是错的,我赞同这种观点。

为什么人们会有这种看法?

我觉得跟人们过度的类比有关。比如讲到类,大家习惯举一些现实中的例子以简化理解。比如,老师属于人类。

那么面向对象是什么呢?

最开始,软件的基本控制结构,只有一个跳转,也就是汇编里的 jmp。实际上现在常用的三种结构也都是使用跳转来实现的。

随着软件复杂度的上升,这种控制结构可读性很差,对于后期维护也是一个灾难,所以出现了结构化编程。通过将控制结构限定在顺序、分支、循环,来提高可维护性。

但是对于数据的维护,仍然是裸露在保护之外的,语言层面还没有一个较好的工具手段。这时,面向对象概念就出现了。

可以说,结构化编程是对控制流程的结构化,而面向对象是对数据的结构化。

说到这里,想起另一本书里的观念,计算机的一些概念本身就很美,比如变量地址,我们可以试着去理解它,没必要人为多加一层注解(房间号),试图绕过这种理解。

当然,从对控制结构的限制,我们也不难看出,通过越来越多的限制,我们确实获得了更多的自由。

总结

上面讲了两个我认为很有意思的地方。其它章节大多是技术上的探讨,这里就不缀述了。

从内容用词看,作者是一个很谦虚且喜欢思考的人。不过书里有一些地方存在重复,这本书是不是作者对旧有创作的二次整理就不肯定的。

对于科班生来说,大部分内容都是一些基础内容,之前应该都有涉及,不会有太大感受;如果没有学习过这些内容,或想温顾一下,还是可以一读的。

hexo博客搬迁

写博客的经历

最开始是接触 csdn,偶而记录一些现在看来无关痛痒的技术问题。

后来买了一台腾讯云的服务器,搭建了 wordpress,开始记录博客。

奈何服务器维护比较占用精力,而且于我博客互动较少,主要还是利已的。于是开始寻找类似 github page 这样的静态托管服务。

先是从朋友那里克隆了一份基于 jekyll 的代码,经过了一番折腾,好歹也用起来了,开始记录杂七杂八的东西。其间,也对 jekyll 做了一些扩展性的东西。

但是克隆到的主题看上去还是有些混乱,之前看过 jekyll 官方文档,说实话并不是很直白(比起 hexo)。遂决定迁移到 hexo。

hexo 迁移过程

迁移的整个过程是比较顺利的,hexo 的安装过程极度简单,几行命令就搞定了。

不过碍于网络问题,初始化时主题下载失败,打开空白,重新下载后 OK。整个过程下载比较慢,网络问题有时真的烦。

由于 hexo 文件格式与 jekyll 略有不同,简单写了一个 50 行的 python 脚本做了格式转换,比较顺利。

hexo 体验

hexo 也有主题跟插件机制,出于简单易用的考虑,后面有需要再做迭代。

这里使用了默认的 landscape 主题,整体是比较简洁大气的。

首页默认显示最近的十篇文章,也可以通过 Archive 查看所有按年编排的文章,或者通过 tag 过滤出某一类相关文档。

2020,博客可以继续写起来了。

事后记

记录两个事后发现的问题:

  1. 按照官方提示,接入 travis-ci 后,从 github 不能将分支正常切换到 gh-pages。github 官方在 16年已经 给出说明,2020年更新的 hexo 文档竟然没有修正,英文文档在页底有评论给出了解法。有需要可以参考这个仓库的 ci 文件。
  2. 小金反馈说,在迁移文件时,由于月份和日期在官方格式是双位的,单位会出现问题。这应该是 jekyll 兼容性过高引起的。

lisp论文《recursive》

论文的全称是《Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I》,是人工智能之父John McCarthy在1960年4月发表的。

之前读过一次,感觉很震撼。这次阅读下来,零零散散地加在一起,包括最后写的一个求值表达式的调试过程,总耗时大概有7个小时。

虽然时间已经过去60年,作者John McCarthy也已经离开我们7年,数学总归是数学,所以论文的语法,跟现在也不会有太大的差别。

重要的还是思想,借助文字来了解先驱所处的时代和想法,这也是我读这篇论文的初衷。

读到这样一篇踏实的论文还是很幸运的,虽然在最开始感到吃力。

这让我想起了《how to read a book》里讲到阅读的目的,比如看一则新闻,大多时候我们只是在获取更多的信息,对我们的理解能力并没有太大的提升。但是当阅读一篇在我们理解能力之外的文章时,虽然会有些吃力,但是它的回报是远远超过我们付出的。

系统的介绍论文的内容不是这篇文章的目的,且作者写的比我要详细准确一百万倍有余,我这里记录一下自己比较有感触的地方(意思就是跟这篇论文的内容关系不大 :-p)。

lisp并非无所不能

刚上来就要泼一下冷水了。

lisp很强大,你可以看我最后从论文里摘抄出来的一个 eval 求值函数的实现,一个可以用自己重写自身的语言,已经不能用强大来形容了。

lisp最重要的是思想,当然它也支撑了一些很重要的软件,比如emacs、autocad,但它终归不能作为商用软件的语言。

原因有很多,说一点,它的生态就比一些主流语言差很多,假如出现了一款新型数据库,社区要做语言的第三方库支持,肯定不会优先想到一门非主流语言。

所以即使lisp很好很强大,它的使用场景也很局限。

阅读这篇论文最大的障碍

我们记住一个东西或者学会一个东西很容易,但是要忘记就很难了。知识就是这样一个东西。

这是我最开始阅读时遇到最大的一个障碍,60年,还需要再过12年c语言才会被发明出来,至于java还要再等35年。

举个例子,如果我们熟悉了现在主流的语言设计,一开始可能会对 S-expressionM-expression 感到有些费解。

所以,我做的第一个尝试就是忘记之前所学的(表面)知识。

想起了张三丰跟张无忌的对话,这也算是我对这段对话的一个认识吧。

lisp的基础是数学

最开始有点难,但是当我理解了lisp设计的基础,一些东西就很容易理解了。

lisp的语言基础,是从数学的基本形式归纳出来的。如何将数学的逻辑和表达式抽象成一个统一的表达形式,是lisp要解决的一个问题。

有的人说lisp很美,大多时候指的并不是它的表现形式(当然,可能也有人觉得lisp的括弧很美),而是指它的底层结构。这也是它可以很方便地重写自身的原因。

这种古老的语言(而不是现代的语言),让我更加确信计算机科学是数学的一个分支。

现代语言的设计,基于计算机的比例变得越来越高。

一个原因是,数学是一门语言的核心,核心的东西一般都较小,而且现在语言支持的特性越来越庞大,不可避免会有越来越多的非核心的面向计算机的代码。比如,多核心,多线程,有越来越多的计算机术语被发明出来。

关于波兰表达式

波兰表达式(polish notation)和代数表达式(algebraic notation)是两个很典型的数学表达式(还有逆波兰表达式,如emacs的calc)。

最直观的方式是代数表达式,面向人类。

波兰表达式的优点在于它的结构更加统一,更易于流程化的处理。还有很重要的一点,它可以做到数据和运算分离,这可能是emacs的calc工具使用逆波兰表达式的原因。

比如,我们经常会计算一个月内的开销,有时就是一些小花费的总和,这时如果使用波兰表达式就很清楚明了,(+ 30.0 20.0 30.0 40.0)。

声名式和命令式(declarative and imperative)

这两个概念在60年就已经存在了,不过现在有一些人还是不太清楚。

一句话,初级领导用命令式,高级领导用声明式。

内存管理

是的,内存管理在60年的lisp里就已经实现了。使用lisp的程序员们不需要关心内存释放,甚至都不需要知道存在内存这样的东西(当然,没人不知道)。

由于lisp的基础很简单,所以lisp内存的实现也很简单。这是一个良性循环。

函数名的设计

这在现在看来已经是理所当然的了。

但是在最开始的设计中,如何实现数学中的函数是一个值得被推敲的事情。

lisp使用了 Church 中定义的 lambda 的原型,把函数实现中的某些参数(形参)和实参绑定,以此为上下文对实现进行求值。

但是 lambda 不能解决递归的问题,比如辗转相除求最大公约数或者利用牛顿公式求解微分方程。

函数名的设计可以解决这个问题,即将名称与实现绑定,实现中就可以使用名称来递归自身了。

lisp中的数据结构

所谓一生二,二生万物。

lisp中的基本数据结构只是一个包含两个元素的元组,但是元组可以互相组合,它的表达能力是无穷的(这里的表达能力指的不是可读性,而是表达的内容)。

而且在形式上有一个莫大的好处,就是它非常适合实现递归的思想。

这里插播一段代码,比如定义一个函数,将 APPLE 转换为 BOY

(defun replace-apple-to-boy (x)
  (cond ((atom x) (or (and (eq x 'apple) 'boy) x))
    (t (cons (replace-apple-to-boy (car x))
         (replace-apple-to-boy (cdr x))))))

(replace-apple-to-boy '((apple foo) apple)) ;; => ((boy foo) boy)

lisp的学习成本

是的,学习lisp几乎没有学习成本。主要成本在lisp的思想上,即递归,这几乎是学习所有语言必知必会的东西。

lisp的基础构建在几个原子操作上,学习的时间应该可以用分钟来计了。

所以emacs使用elisp来作为扩展语言,甚至很多非计算机专业的人来了兴致也可以敲上几行代码。

觉得lisp很难的人,很多都是有其它现代语言基础的人。

过程中遇到的一些小问题

html版本有个好处是组织结构更清晰,一章一个链接,不过公式排版差强人意。

pdf版好一些,但是在eval的定义一块格式还是有问题,特别是嵌套层次深了以后,很难一眼看出自己在哪里。最好是自己重新排版一下。

网络上还有一些排版更友好一些的版本,不过差别不会太大。

eval

这是lisp很强大的一个经典论证,即在语言之上实现该语言的求值器。

NOTE: 该代码使用了elisp,在其它lisp方言应该也可以正常运行。其中只使用了有限的几个函数(car cdr cons cond eq atom)。

(defun assoc2 (x y)
  (cond ((null y) nil)
    ((eq x (caar y)) (cdar y))
    (t (assoc2 x (cdr y)))))

(defun append2 (x y)
  (cond ((null x) y)
    (t (cons (car x) (append2 (cdr x) y)))))

(defun evcon (c a)
  (cond ((eval2 (caar c) a) (eval2 (cadar c) a))
    (t (evcon (cdr c) a))))

(defun evlis (m a)
  (cond ((null m) nil)
    (t (cons (eval2 (car m) a) (evlis (cdr m) a)))))

(defun pair (x y)
  (cond ((and (null x) (null y)) nil)
    ((and (not (atom x)) (not (atom y))) (cons (cons (car x) (car y)) (pair (cdr x) (cdr y))))))

(defun eval2 (e a)
  (cond ((atom e) (assoc2 e a))
    ((atom (car e))
     (cond ((eq (car e) 'QUOTE) (cadr e))
           ((eq (car e) 'ATOM) (atom (eval2 (cadr e) a)))
           ((eq (car e) 'EQ) (eq (eval2 (cadr e) a) (eval2 (caddr e) a)))
           ((eq (car e) 'COND) (evcon (cdr e) a))
           ((eq (car e) 'CAR) (car (eval2 (cadr e) a)))
           ((eq (car e) 'CDR) (cdr (eval2 (cadr e) a)))
           ((eq (car e) 'CONS) (cons (eval2 (cadr e) a) (eval2 (caddr e) a)))
           (t (eval2 (cons (assoc2 (car e) a) (evlis (cdr e) a)) a))))
    ((eq (caar e) 'LABEL) (eval2 (cons (caddar e) (cdr e)) (cons (cons (cadar e) (car e)) a)))
    ((eq (caar e) 'LAMBDA) (eval2 (caddar e) (append (pair (cadar e) (evlis (cdr e) a)) a)))))

下面这个例子使用了前面定义的 eval2 ,来拼接两个参数里的首元素。

(eval2 '((LAMBDA (x y) (CONS (CAR x) (CAR y))) (CONS (QUOTE A) (QUOTE B)) (CONS (QUOTE C) (QUOTE D))) nil)  ;; => (A . C)

这个函数有一个缺陷,在使用 LABEL 进行递归求值时,会再次对求值后的结果进行二次求值,导致错误。

论文中提到有一个修正版本,发布在91年的《Artificial and Mathematical Theory of Computation》。在sciencedirect.com上找到一个收费版的地址,没有继续下去了。地址在下方。

图书管可能有这类书?

引用

http://www-formal.stanford.edu/jmc/recursive.html
https://www.sciencedirect.com/book/9780124500105/artificial-and-mathematical-theory-of-computation