Copy Link
Add to Bookmark
Report
d2_0x02_Hacking_on_the_ext_of_future_GNU_System
_ _
_/T\_ _/P\_
(* *) DO NOT FUCK WITH A HACKER (* *)
| - | #2 File 0x02 | - |
| A | 如何在未来的GNU系统中扩展应用程序 | G |
| | | |
| | By 纳兰经若 <NalaGinrut@gmail.com> | |
| | | |
| | Dec 26th, 2011 | |
| |____________________________________________| |
(____________________________________________________)
Table of Contents
=================
1 Intro
1.1 开篇
1.2 基于b范式的一种开发方式
1.3 GNU Guile
1.4 接下来?
2 认识Guile
2.1 Scheme基本知识
2.2 Scheme几个基础概念
2.2.1 关于三种lambda基础算子的通俗解释
2.2.2 尾递归
2.2.3 Continuation
2.3 Guile的效率?
3 Guile的基本用法
3.1 安装
3.2 REPL环境的使用
3.3 hello world
3.4 Guile的基本类型
3.4.1 Number
3.4.2 Char
3.4.3 String & Symbol
3.5 其余基本用法
4 Guile进阶
4.1 人见人爱的Hash table
4.1.1 List & Cons
4.1.2 Association list
4.1.3 Vector
4.1.4 Hash table
4.2 面向对象
4.2.1 数据抽象
4.2.2 对象
4.2.3 对象系统
4.2.4 GOOPS
4.3 与C交互
4.3.1 FFI(Foreign Function Interface)
4.3.2 SMOB(SMall OBject)
4.3.3 Guile与C的原生交互
4.4 许可证问题
5 文献
1 Intro
~~~~~~~~
1.1 开篇
=========
众所周知,我们常见的Linux系统实质上是GNU/Linux。这样一个写法表明GNU才是
操作系统的本体,而Linux则是这个操作系统中一个可任意替换的内核。类似的内
核还有Hurd,或许以后还会有更多。理论上来讲,能与POSIX兼容的内核,都可以
用于GNU系统。倘若您是一名操作系统研究的狂热者(osdever),或许某天您会考
虑为GNU写一个新的内核,这样您就可以用自己这个新内核取代Linux/Hurd,而旁
人会以为您仍然用的是Linux,因为他们大多数人属于只看外表的。不过这工作可
没我说得那么轻松。有人可能会跳出来反驳:就算我们可以重新做个内核出来,
又怎么可能解决一大堆驱动问题呢?这在我看来并不算个问题,如果驱动框架是
既定的,那么写个转换层并不比重新实现这一大堆驱动更难。只是说是否真的有
必要去做这件事。
好吧,我承认这一切听起来很梦幻,实际上在工程中它也很梦幻。内核开发远没
有这么简单,即便是写个转换层加上测试或许也要耗费掉5年甚至更长。更麻烦的
问题在于可能没几个人对这件事有兴趣,如果有人决定这么做,等待他的可能是
漫长的孤军奋战以及各种冷嘲热讽。但是如果有谁听到这里就感到灰心丧气的
话,或许他不应该再继续往下读了,去看看芒果台的肥皂剧,欣赏下各种酱缸派
的征婚节目来得更有趣些。我的文章,是为那些面对巨大困难却从不退缩反而生
出更多激情的real hacker准备的。如果你确定自己不是,那就别往下看。本文不
打算讨论内核开发,我之所以说这一堆废话,是想说明:无论今后的GNU内核怎样
进化,都不会对应用程序本身构成影响,由于POSIX已经把这种影响隔离开了,对
于应用程序开发者来说无须担心内核更迭的问题,只需要专心扩展应用程序本身
即可。
那么关于应用程序的开发,你会采用哪一种范式呢?
a、我费了不少心思建立了自己的一套框架,并且有良好的接口定义,我甚至做了一
套很好的二次开发规范。任何人想给我的app做改进或者加插件都会感觉是一种享受,
有人甚至拿我的设计跟Apple相提并论,出于谦虚我只能说我暂时还没超过
Apple......(好极了!您的工作太棒了!或许您该考虑为此写一篇东西分享一下)
b、我很想建立一套框架,不过暂时还没思考成熟。无论如何,我会把一些泛用的东
西萃取出来,归纳成库。一方面降低维护成本,另一方面我希望能在社区里跟别人
共同进化。我把自己的库共享出来并写了一些文档来做接口说明。有人会有兴趣给我
的app做二次开发,我也期望着自己的作品能被更大的开源项目吸收引用.....
(干得漂亮!我想很多人都愿意对您的app做一些贡献,包括我)
c、我不大清楚怎么封装库。
我写过一些蛮实用的app,不过基本上我感觉一个不大灵活的小架构还能凑合着用。
我相信自己可以做得更好些,不过想不出有什么理由让我尝试做得更复杂
些......(或许您该尝试自我突破,试试更大一些的项目,别让这种局限成为您
的瓶颈)
d、我写过不少程序,但是从来没封装过库。而且我不大理解为什么有
人不喜欢IDE,像Makefile这类东西不是自找麻烦吗?曾经有人很兴奋地对我说,
他发现vi/Emacs里可以用正则表达式来重构,尽管我保持着微笑但总觉得这人挺
傻的......(嘿~说真的,您考虑过转行吗?forget it, just kidding)
e、
我写了不少程序,挣了不少钱。有人建议我GPL,还说希望我能搞个讲座分享一下。
开玩笑,我跟你们分享那不是断自己财路吗?我喜欢封装库,那是因为我只打算
发布binary code。偶尔我也喜欢全部打包在一起,这样我好加壳什么的。有人总
想从我的作品里偷学些东西,这些人就是想挡我财路......(哥们儿你走错路了
吧?M$在街对面小巷左拐)
我相信大部分人会是b范式(我自己也是),即便有些人现在还处于c/d以后也有很
大可能性会做到b。要做到a范式是很不容易的,这样的人如果愿意分享经验的话
是很值得敬佩的。绝对不会有e范式的人正在读我这篇文章,绝对不会有。如果真
的有的话,那算我倒霉。
————那么你打算告诉我怎样做了,是吗?那些框架神马的,分层神马的,模块划
分神马的......
————Emm...actually, no. 如果听到这个答案你不大满意,那还是回去看芒果台
吧。
1.2 基于b范式的一种开发方式
============================
关于刚才那个设计范式的问题到此为止。本文不会讨论架构设计这类大而庞杂的
问题,也不会教你怎样去做————我自己都没能做到。况且我也不信在键盘上敲个
几万字就能把这类问题讲清楚。
实际上,我写这篇文章希望能同您一起分享我在b范式当中采用的一种开发方式。
并且我相信这种方式在未来的GNU app开发中会大行其道,这种开发方式特别适合
b范式。而如我之前所言,我相信绝大多数人如果坚持hacking的话最终会在b范式
达到一个稳态。所以我们集中在b范式来讨论是有价值的。a范式?我个人认为要
达到a范式不仅需要灵感还需要一些运气,那是可遇不可求的。总之如果在开源社
区里大多数人能在b范式达到稳态,那对整体而言才是最有价值的。
在此之前先说两个真实故事:
02年那会儿,我读了一些.net和c#的资料,之后我强烈地感觉到这样一种框架
和多语言无缝连接的方式会主导将来的上层开发。于是我跟许多朋友谈论这个东
东,却没有人感兴趣,于是我也觉得很无趣,就不了了之了。之后C#发展成为一
门非常优秀的语言,而.net也对开发模式造成的巨大影响。
06年那会儿,我了解到什么是动态语言,并且认识了python。于是我又开始产生
某种强烈预感,之后我跟许多朋友谈及python,希望大家来一起学习一下。我不
大希望自己以一种领先姿态去尝试一种新事物,要么我观望,要么我说服身边的
人跟我一起去做。结果怎么样呢?所谓物以类聚,人以群分。跟我在一起混的朋
友们与我是一个模子出来的,他们也不愿意以领先姿态去尝试新事物。于是这件
事再次不了了之。几年后python在中国爆发出巨大潜能,一时间许多人以谈论
python为荣。在某几次网聚火锅的时候,几乎言必谈python。而我们则丧失了
pioneer的优势,时机已经错过。
————恩...所以你讲这两个故事来告诉我们你有过人的预见能力,好叫我们听你的?
————Emm...actually, no. 你不觉得我在这两件事情上表现出了惊人的愚蠢吗?
第一,如果我坚信某件事是对的,为什么我不靠自己亲力亲为而去依赖别人呢?
第二,为什么不敢以领先姿态去尝试新事物?既然没有这种胆魄,又何必遗憾于
自己的错失呢?
于是,当我接触了Guile之后,我没有去找任何人谈这个东西。等我积累到有一些
信息值得分享之后,我就分享出来。至于有没有人因此感兴趣,与我无关。我只
专注于自己的道路,然后贡献出一些路标给需要的人,也希望从其他朋友那里吸
取一些有益信息。总而言之,共同进化。
1.3 GNU Guile
==============
————呃,你刚才说,guile?
实际上,是GNU Guile。当我们向不熟悉它的人介绍它的时候应该用这个全称,免
得造成误会,就像他们对GNU/Linux的误会一样。关于GNU Guile的产生,涉及到
整个GNU系统的设计理念。即便您在短期内并不打算使用GNU Guile,也应该听我
介绍一下它的历史。原因就是因为理解它的产生,就能理解GNU未来的发展。
————说得夸张了吧......
一点不夸张。GNU从一开始到现在一直按照它自己的步调在走,就如同它的名字:
GNU is not Unix。许多人都知道GNU的名字是一个递归定义,但大多数人并不知
道这是一个大隐喻,它暗含了GNU的开端以及任何时候都可预见的未来进化方向。
如果您知道如何写一个递归算法,那您一定会抱有这样的理念:抓到了递归函
数,就掌控了一切,因为一切不过是围绕递归函数不断在重复中进化而已。
现在我们从GNU历史中来找寻这个神秘的递归函数。1984年,当Emacs的GNU版
本(Emacs有很多版本,当今最受欢迎的自然是GNU/Emacs)呼之欲出的时候,一
个问题摆在这帮hackers面前:我们需要怎样一种编程范式呢?通俗地说,要为今
后能够不断给这个东东添加程序而又不至于像搭积木那样某个时候由于加个小砖
块导致垮塌。后来这帮聪明的家伙确立了这样一个理念:需要两个正交的部分
(所谓正交,简单来说就是不会相互干扰),一个是用低级语言写的kernel,另
一部分则是拥有超级牛力的高级语言作为扩展语言的外延部分。当我说到这里的
时候,或许有一部分读者会恍然大悟,原来Emacs就是当今整个GNU系统的小型翻
版,也是一个内核+周边扩展。恩...既然你都想到这一点了,还需要我告诉你递
归函数是什么吗?一个喜爱并理解GNU/Emacs的人,要理解GNU本体只不过是时间
问题。
————等等,我也喜欢GNU系统,但是我更喜欢vi,我觉得vi更XX,不是吗?
——恩,是啊,话说今天天气真好啊...
扩展语言可以产生有弹性的外延体系,它可以兼容其他程序并且也很方便更新。
关于这一点,不需要理论上的证明。GNU/Emacs近四分之一世纪的发展历程就是明
证。program=kernel+extension就是这个递归函数。请注意,这个递归函数本身
也可能是递归构成的。于是GNU本身就是这样一种奇妙的隐喻,它不仅名字隐喻了
这个递归函数,它的任何组成部分都隐喻这个递归函数。那么,现在某些
Linux内核铁fan们是不是开始理解,为什么Hurd这样的微内核才是GNU钦
点的内核架构?
————莫非因为kernel=kernel+extension?
——你不觉得Linux本身也针对这个递归函数做了某种程度的修正吗?比如
module之类的......
————Linux不可能被hurd替代了...商业上的巨大成功已经导致Linux把GNU绑架了...
——关于hackers,你只知道他们是艺术家,是钻研者,还是偏执狂。实际上我告
诉你,远不止如此,他们是疯子。为了实现完美,他们无所不能......
OK,现在我们回到正题。既然我们已经提到kernel=kernel+extension,那么很明
显extension=kernel+extension。现在我可以回答GNU Guile是什么了。它就是
GNU系统那个“拥有超级牛力的高级扩展语言”。类比于GNU/Emacs,我们知道那是
elisp。不过GNU Guile也在做某种融合,有兴趣的人可以参考guile-emacs,这是
题外话了。
————呃...我对GNU Guile有一些了解。我听说它是由于当年Stallman所挑起的所
谓“TCL War”从而诞生的......
——确实,这是一个事实。Stallman由于不爽TCL语言而写了一封邮件“Why you
should not use Tcl”[1]把TCL语言痛骂了一顿。对于real hacker之间的
争论,通常结局都差不多:既然你觉得别人的东西不行,那你自己来一个?
刚好那时候Tom Lord写了一个Scheme实现,后来他说服Stallman干脆就用这个东
东。可以想象老牌lisp程序员Stallman一听到lisp like语言就觉得浑身舒爽,于
是就有了GNU Guile的前身GEL(GNU Extension Language)。后来改名Guile
(GNU's Ubiquitous Intelligent Language for Extensions),听名字就
知道它的野心。目前它是GNU的官方扩展语言。
GNU Guile诞生于1993年,到目前为止低调且平稳地走过了18个年头。许多GNU项
目使用了它,只不过大部分人不知道。其中有AutoGen,Lilypond(如果您是art
hacker,这个应该要知道),Denemo,Mailutils, TeXmacs和Gnucash(曾经有人在
twitter上问有没有开源的理财软件,我推荐了这个给他),还有gEDA(嘿!话说
有open hardware hacker正在读我的文章吗?),之外还有一些游戏。无论如
何,有一点我们可以确定,至少在GNU的软件中,会有GNU Guile的一席之地。无
论你看它顺不顺眼,别忘了它的名号:GNU中无处不在的智能扩展语言。
————为什么不用common lisp?
——我不知道。看当年的邮件,似乎大家都对common lisp做备选没太大兴趣。不
过这几乎算不上是个问题,就在几个星期前Guile社区还在讨论要不要把clisp和
Guile合并了。似乎clisp(GNU的common lisp实现)最近找不到什么玩处,正在
做一些嵌入语言的筹备工作,这就跟Guile重复了(common lisp确实是比scheme
还不受欢迎...)。GNU Guile发展到今天,已经不再单纯是个scheme解释器那么
简单,它有自己一套适用于FP的VM(它不用JVM是有原因的),而且支持多种语
言,比如ecmascript(就是所谓的javascript),还有把php加进去的,elisp也有。
最近有人在写lua和python的。如果真的哪天把clisp也合并了,那GNU Guile就不
能再称之为一门语言了,它几乎就是动态语言领域的GCC了。如果你有兴趣,也可
以添加一门新语言进去,自己设计的也行,这并不难。读一读源码目录里
modules/languages里面的代码自学一下就行了,甚至有家伙写了个brainfuck语
言(这个世界上最无聊的语言)在里面给你做样例。
————有这么简单?
——如果你读过SICP,了解元语言抽象,就会知道在lisp like中设计一门语言那
是......(恩,要不你自己hacking一下,然后把后续的形容词填上?)
1.4 接下来?
=============
————尽管你说了这么多,我还是毫无兴趣。
——昨晚非诚勿扰有个女的不错~
————我对这事不感兴趣!
——今晚有国足比赛!
————我更不感兴趣!
——恭喜你,出家吧!你已经看破红尘了...别发邮件给我,我对你也没什么兴趣。
...那么剩下的都是感兴趣的了?
到这里我不得不说一件事,那就是本文写作同样采用了GNU的递归函数:
本文=第一部分+其余垃圾部分
如果你已经领悟了这个递归函数,就继续往下看。
----我确实对Scheme感兴趣,我曾研读过SICP,但我不知道里面的东西学了有什
么实际用处...
--那你有福了。不过这福气不是我这篇文章给你的,而是整个Guile社区,实际
上,是GNU。
----我是资深Lisp/Scheme/Guile玩家。
--恩。下期ezine你投几篇吧,这份ezine似乎对这类冷门特别感兴趣。
2 认识Guile
~~~~~~~~~~~~
2.1 Scheme基本知识
===================
NO,我拒绝在本文中介绍Scheme的基本东西。这有多方面原因:
1、越基础的东西越不好讲;
2、对于Scheme的基础概念学习,有许多非常优秀的著作能够帮你彻底解决。我会
尽我所能把它们推荐出来:
入门课程SICP [2]
语法课程TSPL [3]
趣味课程BTLS [4] BTSS [5]
标准规范R5RS [6] R6RS [7]
除了规范以外,其他都还蛮有趣味性的。
3、本文的标题不是《21天精通Scheme》;
4、本文的内容是“如何扩展未来的GNU程序”,而不是“如何使用Scheme”;
5、有SICP在,我不觉得还有什么入门的东西可以更好;
6、Scheme简单到爆,SICP也认为没必要专门学它,只管用就是。不过真正涉及工
程领域,还是需要补充基础;
7、本文基于实用主义,让学过SICP的人知道自己有什么可以玩的,而不是学术探
究;
...
————Scheme不能说简单吧?
——用到一定程度什么语言都不简单。
————但我确实需要一些基础的讲解。
——反正ezine还会有下一期。
我用的Guile-2.x,所有的叙述都以Guile-2.x为准。如果您只有1.8-那该考虑更
新了。我再一次提醒您,咱们不是在纯谈Scheme,而是在谈如何用Guile。Guile
只是Scheme的一种实现,它遵循Scheme标准,但它也有不少东西是独有的。为了真
正达到扩展GNU的目的,我们必须使用这些扩展——这才是Guile存在的目的。
2.2 Scheme几个基础概念
=======================
————你不是说不讲么?
——这几个还真得讲一下,但是无法深入讲,凑合着听听看吧。
我尽可能用最通俗的语言讲,因为本文的目的是让更多的人了解Scheme,而不是
给学究挑刺。请确保您有Scheme的使用基础(至少读过SICP并做过若干题目)再
继续往下读。
2.2.1 关于三种lambda基础算子的通俗解释
---------------------------------------
了解过lambda演算的朋友都知道有三种最基本的lambda算子,它们乍听起来很吓
人,但实际上都是你们很熟悉的概念。
1、alpha-变换:
其实就是我们常说的“变量名称无关”,即f(x)可以把变量x更名为y、z......,变
量换个名称不影响函数本身。
2、beta-归约:
最通俗的解释,其实就是把变量相应的值代进去计算出来。比如(λx.(x*2+1) 5)
进行一次beta-归约之后就是:5*2+1=11。
3、eta-变换:
也叫eta-抽象或eta-展开,简单的来说,就是消除冗余的lambda表达。举个例子
就明白了:
(lambda (x)
(func x))
这是不是可以简化成(func x)?就是这个意思。大量嵌套lambda表达会让人头
疼,利用eta-变换可以把一些冗余的lambda表达简化成最简函数调用,这样就一
目了然。
2.2.2 尾递归
-------------
喜欢函数式编程(以下简称FP)的朋友有很大一部分是因为喜欢尾递归。毕竟绝
大多数算法的定义是递归定义,能够直接用递归实现算法而又不必担心撑爆
stack确实是一件很爽的事情。当然,也有人有不同意见,不过有不同意见的人应
该没兴趣读这篇文章,所以针对他们的一些回复就省略了吧。有些事情,辩论是
毫无意义的,比如,语言战争。把自己有说服力的作品拿出来,别人自然会闭嘴。
要是大家的作品都具有说服力,那就求同存异。分帮派?别傻了。连个强社区都
没有,还帮派林立。
关于尾递归,rnrs中的定义太过于学术化。不得不提一下,rnrs目前是Scheme的
事实标准,类似C99之于C语言。既然我们讨论的是Scheme,那一切都得按照
Scheme的规矩来。rnrs规定,一门语言如果要号称是Scheme实现,那必须要实现
proper tail recursive——这个词国内一般翻译是“严格尾递归”。我们现在不讨论
翻译问题,得先看看这东西究竟是什么。
我还是试图用最通俗的方式来解释。(以下如果出现英文专有名词我不会试图翻
译,我也不太清楚中文的一般译法是什么,重要的是我解释清楚它们是什么——如
果可能的话)
这需要先介绍几个概念:
1、tail context
从实用化的角度来说,我不需要介绍这个概念的定义,只需要把rnrs的规范贴出
来,您一看便知。根据r5rs,tail context只包含如下语法形式:
(if <expression> <tail expression> <tail expression>)
(if <expression> <tail expression>)
(cond <cond clause>+)
(cond <cond clause>* (else <tail sequence>))
(case <expression>
<case clause>+)
(case <expression>
<case clause>*
(else <tail sequence>))
(and <expression>* <tail expression>)
(or <expression>* <tail expression>)
(let (<binding spec>*) <tail body>)
(let <variable> (<binding spec>*) <tail body>)
(let* (<binding spec>*) <tail body>)
(letrec (<binding spec>*) <tail body>)
(let-syntax (<syntax spec>*) <tail body>)
(letrec-syntax (<syntax spec>*) <tail body>)
(begin <tail sequence>)
(do (<iteration spec>*)
(<test> <tail sequence>)
<expression>*)
where
<cond clause> --> (<test> <tail sequence>)
<case clause> --> ((<datum>*) <tail sequence>)
<tail body> --> <definition>* <tail sequence>
<tail sequence> --> <expression>* <tail expression>
凡是您在编程时用到以上列举的形式,都意味着<tail
expression/body/sequence>的位置处于tail context当中。这个很简单,甚至不
必要刻意去记它们,用到的时候查一下,用多了自然熟悉。
2、tail call
理解了tail context,则不难理解tail call,就是在tail context中的
*最后一条指令*调用了某个过程(call procedure),则该次调用属于tail call。
3、proper tail call
如果您之前疑惑为什么proper会翻译成“严格”,那现在可以释疑了,这个意译我
觉得还是很准确的。如果一次tail call的caller在跳转到callee之前,其堆栈帧
可以全部退栈,那么这次调用称为proper tail call[8]。我们举个例子:
(if (> a b)
(- a b) ;; call-1
(- b a)) ;; call-2
在上例中,call-1和call-2都处于tail context中,而且针对减法"-"的调用,是
在其各自tail context中的最后一条指令。如果您不太清楚什么叫最后一条指
令,那么可以看这个例子:
(if (> a b)
(set! c (- a b)) ;; call-3
(- b a)) ;; call-4
在这个例子中,call-4只进行了一次调用,即求b-a。而call-3中则是两次调用,
先进行(b-a),然后把结果赋值给c。这个时候,a-b不是该上下文中最后执行的一
条scheme指令,而set!才是。所以set!是proper tail call,而减法不是。
显然,在执行减法之前,该函数所有东西都没有必要保留,故call-4为
proper tail call。
明白了这些概念,我们可以来解释什么是proper tail recursion:当proper tail
call是一次递归调用时,称为proper tail recursion。实际上,proper tail
call的意义在于,当它出现的时候,意味着后面要执行的程序(实际上就是所谓
的continuation,如果不太清楚这个概念,就理解成该语句之后接下来即将执行
的所有东西)没有必要为其保留之前的信息,也就是说,可以“完全退栈”。或许
您在学校刚开始接触递归的概念时,老师会提醒您,递归是一种不好的用法,因
为可能导致栈溢出而程序崩溃。不知道您是否自己思考过这个问题——为什么会栈
溢出?
简单来说,每次函数调用时,程序会自动为其保留上下文信息——这个过程现代编
译器会自动为您完成——如果不断进行递归调用,则为该递归函数保留的信息会越
来越多,而不能把它们丢弃,否则信息会丢失,程序结果也将不正确。很明显,
内存总是有限资源,这么无限制保留信息迟早会把栈撑爆,这就是栈溢出的缘
由——我想这是最为粗浅的解释了。奇妙的是,proper tail recursion
意味着这些被保留的信息可以“全部丢弃”,因为以后再也用不到了。
那么,接下来一个问题是,如何理解“全部丢弃”或者“全部退栈”?还是用最通俗
的语言来解释,如果不需要保留上下文的任何信息,那一次函数调用跟跳转指令
的区别还有多少?
首先,需要处理参数,对吧?
然后跳转到需要的地址。没了。
如果是递归函数,无非是参数压栈->跳转到同一个函数的开头重新执行而已。
这个时候,propertail recursion的重大意义开始显现,递归可以用机械化
的方式变为循环迭代,这意味着编译器能够通过某种算法来自动完成。
我们一般称为尾递归消除或者尾递归优化。
OK,关于尾递归,就不再深入探究其机理,以及如何优化。毕竟我们要保持本文
的实用性,懂得机理会用,是本文试图达到的目的。但不代表以后不会有相关的
文章让我们继续讨论。
现在我们该考虑下,如何使用尾递归了。
一个经典的例子,就是求n!的问题。
首先看常见的递归算法:
(let lp ((n 1000))
(if (= n 0)
1
(* n (lp (1- n)))
))
结果是:
402387260077093773543702433923003985719374864210714632543799910429938512398629020592044208486969404800479988610197196058631666872994808558901323829669944590997424504087073759918823627727188732519779505950995276120874975462497043601418278094646496291056393887437886487337119181045825783647849977012476632889835955735432513185323958463075557409114262417474349347553428646576611667797396668820291207379143853719588249808126867838374559731746136085379534524221586593201928090878297308431392844403281231558611036976801357304216168747609675871348312025478589320767169132448426236131412508780208000261683151027341827977704784635868170164365024153691398281264810213092761244896359928705114964975419909342221566832572080821333186116811553615836546984046708975602900950537616475847728421889679646244945160765353408198901385442487984959953319101723355556602139450399736280750137837615307127761926849034352625200015888535147331611702103968175921510907788019393178114194545257223865541461062892187960223838971476088506276862967146674697562911234082439208160153780889893964518263243671616762179168909779911903754031274622289988005195444414282012187361745992642956581746628302955570299024324153181617210465832036786906117260158783520751516284225540265170483304226143974286933061690897968482590125458327168226458066526769958652682272807075781391858178889652208164348344825993266043367660176999612831860788386150279465955131156552036093988180612138558600301435694527224206344631797460594682573103790084024432438465657245014402821885252470935190620929023136493273497565513958720559654228749774011413346962715422845862377387538230483865688976461927383814900140767310446640259899490222221765904339901886018566526485061799702356193897017860040811889729918311021171229845901641921068884387121855646124960798722908519296819372388642614839657382291123125024186649353143970137428531926649875337218940694281434118520158014123344828015051399694290153483077644569099073152433278288269864602789864321139083506217095002597389863554277196742822248757586765752344220207573630569498825087968928162753848863396909959826280956121450994871701244516461260379029309120889086942028510640182154399457156805941872748998094254742173582401063677404595741785160829230135358081840096996372524230560855903700624271243416909004153690105933983835777939410970027753472000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
我想您可能没兴趣去看完这个数字,您可能更为注意的是:“哦~瞬间就得出结果
了?” 或者 “1000!也能轻松解决?!”那么试试1000000?
Throw to key `vm-error' with args `(vm-run "VM: Stack overflow"())'
恩~同样是瞬间得出结果,是吧?
如果用尾递归呢?按照刚才咱们的推导,尾递归最后肯定是优化成迭代形式的,
那么栈溢出是不可能的。不过哥们,我得提醒你一句,就算不会栈溢出,有些运
算可是算到世界末日也不会得出结果的。所以不要玩得忘乎所以,节约点时间干
正经事。
在使用尾递归之前,我要稍微对刚才的叙述做一些修正——并不是一点都不保留信
息,这是不可能的,否则您怎么可能使用上一步的结果来计算呢?关键有两点:
1、您可以自己选择保留什么信息;
2、它们会作为参数而被保留下来;
————喂!这不是还要保留信息在栈里面吗?
——嘿,不要忘了,您能自己决定保留什么。另外,您只是保留上一步的信息。而
递归的信息入栈可是包含第1步到k-1步的所有信息(假设目前运行到第k步)。
“全部退栈”是机器自己全部丢掉,您自己要放进去的东西没有谁能够阻止。
首先我们还是要看看原来的递归实现究竟哪个地方违背了proper tail
recursion原则。我们看到是(* n (lp (1- n)))这里出了问题,lp是递归调用,
而tail context中最后的语句是乘法。
那么要将上例改写成尾递归代码,最简单的方法就是遵循这两点:
1、决定您要保留什么
当然,对于这个算法,您需要的是上一步的计算结果。n!本质上是每一步计算结
果的累计乘积。所以每一步的结果是必须作为必要信息而保留到下一步的。
2、让这些必要信息作为参数而被保留下来
这意味着您必须这么设计函数:它有一个或几个参数位置是专门用来保留特定信
息的。在n!这个例子里面,我们只需要保留上一步的累积乘积就可以了。
比如:(func n result)
实现如下:
(let lp ([n 1000000000000000] [result 1])
(if (= n 0)
result
(lp (1- n) (* result n))))
PS:注意中括号[]跟小括号()在r6rs中是等同的,这意味着您可以合理利用它们来
分隔变量绑定。但仅限于r6rs及以后版本的标准。如果您所使用的Scheme实现
是r5rs的,那么把[]改成()即可。Guile-2.x原生支持r6rs。
我们看到,result的初始值是1,总不可能是0吧?每次计算结果都保留到
result,并作为参数传给下一次递归调用,当n为0时返回result。这时候lp确实
在tail context中处于最后一条指令,而且它没有任何上下文信息需要自动保
留,该保留的您已经自己保留了,这意味着它是proper tail recursion。
————这么简单,我决定在C/Ruby里面试试看~
——慢走不送...
最后说明一下,不是所有语言都支持尾递归优化,最简单的判别方法就是:
(let lp ()
(lp))
如果存在尾递归优化,这相当于C语言的
while(1);
如果不存在,那将直接报栈溢出错误。
不过您可以肯定的是,如果一门语言声称是Scheme方言,那它一定具备proper
tail recursion。如果没有,那就叫它别忽悠了。
2.2.3 Continuation
-------------------
看到这个标题很兴奋?
我学习C语言的时候,好不容易读到指针那一章,实在是很开心,因为我觉得自己
快要掌握C编程的精髓了。顺便一提,我用的是《C primer plus》,如果您想
零基础学C语言,这书真的不错。从后面的经历来看,我的想法是没错,指针确实
是C编程的精髓,问题在于,掌握了指针的定义和基本用法并不代表掌握了C编程的
精髓。
我们来看下指针的定义:指针就是内存里的一个地址。
定义掌握了。
来看基本用法:
type *ptr = value;
则ptr是指针,即地址,其解引用(dereference)——*ptr表示内容value。
定义和基本用法都掌握了,我们掌握了C编程的精髓了???
本节会向您介绍continuation的定义及其基本用法,至于Scheme的精髓您有没有
掌握,请自行思考。
————我从不觉得指针是C编程的精髓
——随你怎么想~你有保留想法的权利,我也有。
同样的,在介绍continuation之前,我们必须先介绍几个概念(前文已经提过,
出现任何专有英文名词都将不做翻译):
* closure
对于Scheme来说,过程(procedure)都是closure,您要是听不习惯,那请记住过
程等同于函数,只不过Scheme爱好者更喜欢过程这个说法。
closure是这么一种东西,它要求若干参数作为其被调用的前提,而它本身包括两
个指针:
1、一个指向代码段的指针
2、一个指向环境的指针
简单来说,closure是这么一个怪物,它肚子里有两个指针,如果你想让它动起
来,要喂它一些食物,这些食物叫做调用参数。
/----------\ <======= arguments
| |
| code-ptr=======> code segment
| |
| |
| env-ptr========> environment
| |
\----------/
closure
你想到什么?
恩~或许你熟悉C语言,知道C在函数调用时也存在两个东西,一个是帧指针
(frame pointer),它指向堆栈帧起始,堆栈帧里面存放着所有的本地变量;另一
个指针则指向函数入口,实际上,在函数开始运行的那一刻,它就在程序计数器
PC中。确实,基于图灵机模型设计的C语言,与基于lambda演算的Lisp like在
closure层级看上去是如此相似。
我之所以这么说,是因为图灵机跟lambda演算在“事实上”是等价的,但是它无法
进行形式化证明——这就是著名的“图灵丘奇论断”[9]。如同我们只能相信上帝是存在
的,但我们凭借自己的能力无法证明,除非上帝向我们显明。我们无法证明它,
但是我们只有在一次又一次发现它确实是这样的结果下知道它肯定是正确的。如
果我们连信它的心都没有,那计算机也就发展不到今天了。我们坚信它,尽管还
没有证据,抱着信心努力,等待答案来临的那一天。
“信心是所望之事的实底,是未见之事的确据”——《希伯来书 11:1》
当一个函数调用的时候,它需要一个“东西”帮它保存两个信息:
1、返回地址
2、调用者(caller)的执行环境
这两个信息可以保证函数调用之后能返回到正确的上下文(context)中。同样的,
我们只需要保存指针即可,所以这是两个指针。
请注意,这个“东西”,不是我们之前谈到的“该函数形成的closure”,尽管它们很
像——也要保留两个指针,一个是代码入口,一个是环境的入口——但“那个
closure”是函数或过程的本体。而这个“东西”则是函数所需要的一个同伴。其实
这个“东西”也需要一些食物,closure需要的是调用参数,那么这个“东西”要什么
呢?它要的是该closure返回值(return value)。必须要有这个食物它才愿意工作。
于是我们可以对比一下closure和这个“不明物体”的异同:
closure “不明物体”
本体内容 两个指针(执行代码入口,callee环境) 两个指针(返回代码入口,caller环境)
需要参数 该函数的调用参数 该函数的返回值
返回情况 返回一个return value 永远不返回(!)
* Continuation
continuation就在您的眼前。它就是那个“不明物体”。它包含三个特性:
1、它是一个closure;
2、它需要一个朋友才能存在,而这个朋友就是某个函数(同时也是另一个
closure),它需要这个朋友的返回值作为调用参数才能活下去;
3、它永远也不会自己返回(return by itself)。
我们可以看到,一个continuation实际上就是一个“类似函数的东西”——这很正
常,因为在Scheme中,所有的closure都可作为函数/过程而被调用——它被自己的
朋友(其所在的函数体)所调用,但永远也不会自己返回给它的调用者。
所谓continuation,就是一个函数接下来要执行但尚未执行的那一部分东西。所
以它是一个“未来”。
* First Class Continuation
等等。我们之前曾说过,C语言在closure层面跟Lisp like是一致的。那么C语言
有没有continuation?
答案是肯定的。
但对于纯正的C语言来说,这种思考没有任何的意义。
为什么?
因为它被“奥卡姆剃刀”剃掉了。
我们可以想象一头隐形的喷火巨龙站在你的身边,它的火焰不会灼伤你,它的声
音你听不到,它的样子你看不见。它永远也无法通过任何方式显现自己,你也没
有任何办法感知到它。于是“奥卡姆剃刀”帮你把这种无意义的东西去掉了。C语言
的continuation也是如此,你在程序里看不到它,用不了它,它自己也无法返回
任何值。那么你思考它有什么意义?这就是为什么我们会在Scheme中思考
continuation的原因。因为在Scheme中,continuation是第一等公民,它从函数
的身后站了出来,这使得它成为一个可以让你感知并且使用的closure,你得以利
用它做一些事情——其结果可能是匪夷所思的。我们之所以说得这么拟人化,是因
为这个概念整体上是非常抽象的,您需要尽可能动用你的想象力来思考它。但是
要让continuation变成可感知的,那就必须让它可以返回。
————你刚才说continuation的特性之一就是永不返回?
——我说的是永远不会“自己返回”。
Scheme提供了一个“梯子”让您可以触碰到continuation,并且您可以按照自己想
要的效果与它沟通,它会尽其所能给于您想要的结果。这个梯子就是
call-with-current-continuation,简称call/cc。按照rnrs的定义,call/cc是
这么一个东西,它可以捕捉当前continuation,并且定义一个“逃逸过程”(escape
procedure),让当前continuation在这个过程的帮助下返回您需要的值——这是您
能够合法与continuation交互的唯一方法。
我们看下call/cc的用法:
(call/cc proc)
我们看到call/cc只需要一个参数,这个参数是一个过程,这个过程是您自己定义
的,它将“*随着*当前continuation”被调用。而proc这个过程的定义为:
(lambda (escape)
......)
也就是说,proc需要一个参数,这个参数同样是一个过程,这个过程被称为“逃逸
过程”。当您在proc中任何时候调用这个escape,该函数将返回。其返回值就是
escape的参数值,它可以是任意数目的。
我举一个例子,比如从'(a b c d e)中搜索'c,如果存在则返回其索引值。
(call/cc (lambda (escape)
(for-each (lambda (e i)
(if (eq? e 'c)
(escape i)))
'(a b c d e) '(1 2 3 4 5))))
==> 3
您要是还没看懂,那么我们把escape替换成return,如果您熟悉C语言的话总该明
白了吧?当执行到(return i)时意味着之后所有的东西都不再需要执行了,返回
i作为整个closure的返回值。
————那这跟C语言的return有什么区别?
——如果您按照标准用法使用call/cc,那么效果就是跟return一回事。但是对于
for-each这个高阶过程来说,如果不借助call/cc,那么它不会返回值,因为它
的函数定义里没有返回。所以如果我们需要在中途返回,那么就必须在满足条
件时捕捉当前continuation,并返回。
不过,如果continuation的用法仅限于此,那这东东确实就没什么太大用处了。
Scheme中的continuation是first class continuation,这意味着什么呢?这意
味着Scheme中的continuation可以被保存起来,当作函数来调用。从之前的定
义,我们可以得知两点:
1、这样一个被保存的函数包含了另一个函数的未来(过去的已经过去了);
2、当这个函数被调用时,原来那个包含它的函数可以不被影响(已经被单独保存)。
您可以想象,第一点让您拥有操控时空的能力;而第二点,能够让您创造平行世
界。通俗点说,前者可以让您处理异常,后者能够做并发。所以,当一个
continuation被保存下来的时候,即变成第一等公民的时候,就是它可以发挥威
力的时候。
————我听说C语言的setjmp可以模拟continuation?
——它可以做到第一点,但不能做到第二点。
当一个continuation被捕捉到的时候,call/cc能够提供一种能力,让您传入的这
个过程如同当前continuaiton那样运作,也就是说您可以通过传入的这个过程改
变这个函数的未来,并利用这个过程的参数——逃逸函数将结果返回。
简单来说,一个“永远也不会自己返回的closure”通过call/cc会变成一个拥有正
常返回能力的普通函数,该函数的价值在于,它包含了另一个函数中我们所需要
的信息,即其在运行当中的一个快照(snapshot)。由于它本身是另一个函数的“未
来”,我们改变它,就可以改变另一个函数的未来——但允许您让它们处于平行世
界,互不干扰。
————接下来你是不是要讲解著名的“阴阳谜题”?
——NO,哥们,我们不是在讨论Scheme,我们只是复习一下Scheme最基本的一些东
西。然后开始讲Guile。如果你对continuation有兴趣,那下次或许可以有一篇
相关的文章来讨论。
实际上,对于continuation的讨论到此为止了。尽管continuation可以带来许多
特性,但是多数特性在Guile中已经实现为基本过程了,比如异常处理/结构化控
制/多线程等等。从实用化的角度,我们犯不着在这个时候找continuation的麻烦。
别垂头丧气的,你到底是想实用?还是只是想玩玩?
————我觉得continuation可以拿来炫耀...
——那你慢慢装逼吧。
————那amb总有价值讲吧?
——下次吧。弄个continuation专题。
在追求实用性的情况下,对call/cc的了解,暂时只需要知道如何利用它来返回就
可以了。因为某些过程中必须用到这个方法,这也是Scheme编程中最常见到的
call/cc用法。我只能说,关于continuation的讨论,不是这么几万字可以讲完的。
2.3 Guile的效率?
==================
既然我们提到Guile是Lisp方言,那么它必然拥有Lisp的一些特性,比如垃圾回收
(GC),比如动态类型。当然,最关键的,Guile目前还是一门动态语言。这就无法
避免效率问题,通常这类动态语言的效率都要比C语言低得多。问题是动态语言本
不是以C语言作为参照物的。况且动态语言也可以通过AOT Compiler编译成
native code。但既然Guile是扩展语言,那就意味着,你可以选择哪一部分代码
用C语言来写,那一部分用Guile来写,它们可以无缝连接。
Guile-1.8是纯解释型语言,它采用的解释器是经过精心优化过的,效率很高。到
了Guile-2.x则引入了VM,所以代码在执行前会先编译成bytecode,这样避免了一
些解释开销。但是2.0的解释器没有1.8那样高效,原因是Guile社区期望其效率在
VM的优化上来提高,而非靠解释器。绝大多数人认为,解释器的优化余地并不
大,但是VM可发挥空间是很大的。
另外,我们要谈效率的话,不能站在还原论角度纯从某个技术点来谈。比如一味
指责动态语言效率比C语言低是没道理的,C语言执行效率是很高,但是投入产出
比未必高。如果纯谈执行效率的话,那汇编不是更好些?
Guile社区的黑客们曾经就效率问题谈过几点看法:
1、速度永远是相对的。Guile比CPython和MRI要快,但是比V8要慢。一种语言实
现不能用“快”或“慢”来评价;只能说“比谁慢”或者快;
2、Ruby的普及证明了一点:速度绝不是最重要的东西。V8证明了“低速语言”也可
以做到非常快的效果。一切都会改变,应该把注意力集中在语言表现力和你所需
要面对的问题上;
3、测评得到的速度,永远只是理论速度。还有其他速度你得考虑:启动速度、开
发速度等等...
4、Guile比某些语言慢,因为目前采用的是bytecode,而某些Scheme方言则编译
为native code——迟早Guile也会有,AOT编译器已经在路上;
5、不要为使用者的人数担心。要么你选择其他语言。但如果你选择Guile,就别考虑这么多,把精力放在hacking上。
3 Guile的基本用法
~~~~~~~~~~~~~~~~~~
3.1 安装
=========
如果您所用的发行版暂时没有最新的Guile-2.x,那可以考虑从源码编译安装。
需要解决的依赖有:
- libgmp
- libiconv
- libintl
- libltdl
- libunistring
- libgc
- libffi
————怎么这么多?
——哥们,是个GNU系统基本上都差不多装了吧,偶尔少一两个倒是可能的。
尽可能用最新版本就行了。
编译参数也没什么需要考虑的:
./configure
make
make install
make pdf
顺便一提,Guile的pdf文档是很详细的,写得也很好,多看文档。
————我英文不行...
——哥们,这可是dnfwah,我们假定你是黑客,而且我们坚信你有足够的实力成为
黑客。
无论如何,Guile Manual这900多页的文档短期内是出不了中文版的。本篇文章也
无法涵盖所有内容,对于一名hacker来说,英文不行就只能处处抓瞎了...
3.2 REPL环境的使用
===================
装好Guile之后,在终端执行guile,就能进入交互模式。一般Guile社区管这个环
境叫REPL(Read–Eval–Print Loop)。平时的学习和试验适合在REPL下进
行,在~/.guile文件中添入如下几行:
(use-modules (ice-9 readline))
(activate-readline)
保存退出。然后REPL环境中就可以使用自动补全了。如果没有这个文件,就新建一个。
想要退出REPL,用Ctrl+d。
PS:您可以尝试Geiser,这是基于Emacs的一个开发环境,支持Guile。
对于我个人而言,平时的开发是在Emacs的scheme-mode下完成的,测试代码我通
常都是在REPL中测试。都是比较笨的办法,不过玩的花哨可能分散注意力。反正
个人自己看着办。
3.3 hello world
================
任何一门语言的入门教程都会首先介绍hello world。本文比较例外,写了几万字
才开始介绍hello world。原因前面已经提到了,本文需要您有一定Scheme使用基
础以及SICP的学习经验。这个hello world不是为了介绍Scheme语法,而是为您展
示如何通过命令行执行Guile程序。
在Emacs中建立hello.scm,输入如下语句(除了guile路径您可能需要修改以外,
第一次使用的话其余请照搬):
-e main
!#
(define main
(lambda (args)
(format #t "hello world! I'm ~a~%" (cadr args))))
保存退出。
chmod +x hello.scm
./hello.scm hacker
==> hello world! I'm hacker
参数解释:
-e
表示选择入口函数,e是entry,即该脚本执行时先调用哪个函数。我习惯性使用
main,名字您随意。入口函数有一个规范,其参数固定为一个list,相当于C语言
的argv。对于上例,args的值为("./hello.scm" "hacker")。您应该知道如何处
理list吧。
如果没有加这个参数,您需要在代码中显式调用main,命令行参数也需要您自行
传入: (main (command-line))其中command-line为内置过程。注意:如果您没
有使用-e选项,将按照您脚本中所有函数的调用次序依次调用。
代码解释:
format有点类似于C中常用的sprintf。
第二个参数#t表示打印到标准输出,这个参数如果置为#f则意味着将输出结果当成字符串返回,这是
个很有用的trick,尤其是你想根据格式化字符串生成一些字符串的时候。另外,这个参数如果传入
其他端口(在Scheme中打开任意文件都意味着产生一个端口,包括socket),那么结果将打印到
该端口中。
其中格式化字符串中的转义符%在Scheme中用~替代,其余都差不多:
~s <==> %S
~d <==> %d
~% <==> \n
至于我们用到的~a,它表示任意类型,如果我们不确定输出的是什么类型,那么用~a就可以了。
3.4 Guile的基本类型
====================
最基本的类型我们现在这里简单介绍,因为这没什么复杂的。复杂类型在进阶篇
里会介绍。
3.4.1 Number
-------------
对于Guile,数字是任意大小的。没听明白?就是除了硬件以外没有任何东西能够
阻止你使用一个你认为足够大数字。只要你内存扛得住,多大的数字都能表示。
所以永远也不要担心类型溢出,只要担心栈溢出就可以了。
数字包括几种:
* 整数 integer
这个不用介绍了吧。
* 分数
fraction这个倒是Guile中比较独特的,确切地说,分数属于exact number。(/
1 2) 不会等于0.5,而等于1/2。所以Lisp like确实是“数学家的语言”,如果您
热爱数学,您就会喜欢它。
我们总是喜欢回答别人不确定的答案,但是我们又期望从别人那里获得确定性的
答案。真正的Guile,可以帮你解决这个困顿:
(exact->inexact 1/2) ==> 0.5
反之亦然。
* float
其实Guile里面不用float这个称呼,如果您想使用小数,那么它的正式称呼是
inexact number。在一般计算中,Guile默认会将数值按照exact number处理,您
需要给它一点“提示”:
(/ 1 2) ==> 1/2 exact
(/ 1.0 2) ==> 0.5 inexact
不过您仍然可以用float的方式表示一个数:
0.0005 ==> 5.0e-4
(+ 1 5e-4) ==> 1.0005
* x进制
16进制: #x10 ==> 16
8进制: #o10 ==> 8
2进制: #b10 ==> 2
3.4.2 Char
-----------
字符在Guile里需要前缀"#\",如:
里面有些有趣的东西,您可以自己挖掘。
3.4.3 String & Symbol
----------------------
string: "Guile"
symbol: 'Guile
本质上来说,string和symbol是同样的东西。区别在于:相同内容的symbol在符
号表中是唯一的,而string则不然。这意味着,一个symbol可以只用其头指针来
表示其存在,因为它是唯一的。所以symbol在比较时只会比较其首地址;而如果
用string,那么必须每个字符都要去匹配。希望您能灵活运用。
3.5 其余基本用法
=================
其余的基本用法就是标准的Scheme用法,r5rs或r6rs任您选用。之前已经说过,
不讲基础用法,只能推荐书目——想想看,光讲三个基本概念就耗了多少字数,而
且还没深入讲。其他东西查Guile手册就行,本文不会试图取代Guile手册。
4 Guile进阶
~~~~~~~~~~~~
4.1 人见人爱的Hash table
=========================
————关于Lisp like的数据结构,不是应该从list开始吗?
——是啊。
4.1.1 List & Cons
------------------
我们都知道,所谓Lisp,是LISt Processing的缩写。也就是说,Lisp like擅长
处理list。而list,我们学数据结构的时候都很熟悉,类似单链表。一切复杂的
数据结构都是从单链表开始构建的。
————喂,list不能叫单链表吧?给你任意元素你能往下遍历吗?
——恩~确实不应该叫“链表(linked list)”,其实叫列表就对了...
Lisp封装了几乎所有list所需要的操作,加上其动态类型的特性,您可以很容易
用它构建及操作更为复杂的数据结构。对于计算机的初学者来说,不需要面对指
针,不需要熟悉类型转换,就可以直接思考数据结构和算法,是一件值得开心的
事情。这也是为什么SICP会作为入门课程的原因,之一。SICP甚至走向一个极
端,直接跳过Scheme基本语法的讲解,用它里面的话说:这种语言简单到根本不
用教,只要用就行了。
确切地说,SICP的这种自信乃至自负,是它悲剧的原因之一。我第一次读
SICP时,足足花了三个月才看完第一章。我曾经怀疑是否我水平太差,但经我了
解,绝大多数我认识的人从刚开始的信心百倍,到彻底远离这本书,基本用不了
三个月。既然您已经读到这里,想必已经承受过SICP的煎熬。
————我觉得这书很简单。
——这是入门课程,地球人都知道。
其实许多人刚开始学C语言的时候,也未见得有多轻松。之前已经说过,语言战争
是没有意义的。list在Guile中的实现,跟绝大多数人学习C语言数据结构时所写
的单链表差不多。只不过Guile里面的数据类型全部抽象为SCM对象。(出于实用
目的,本文不涉及Guile的具体实现)从这里您听出什么没有?很明显,list无法
做随机存取,每次都必须从头开始遍历整个list,直到找到合适的值来处理。由
list可以变换出各种复杂数据结构,这意味着其元素结构需要同样的灵活性。这
就是cons。cons本质上是一个点对(pair),但可以无限任意扩展,甚至扩展成
list。这种一生二,二生三,三生万物的极简本质,赋予了Lisp强大的数据处理
能力。
对于list和cons,复习这么多就够了。
4.1.2 Association list
-----------------------
Association list在Guile中简称assoc或者assoc-list。是一种特殊的list结构。
看一次就懂了:
(define al '((name . "John") (age . 16) (gender . "male")))
这就是assoc。很明显,它是list的一种,至于它的内容是您随意定制的。常见的
assoc是key-value对应表,适合用于查询。比如:
(assoc-ref al 'age)
==> 16
如果我们把assoc当作数据库,可以实现一个宏来抽象出类似sql的select语法,
用于查询。
(define db '(((name . "John") (age . 16) (gender . "male"))
((name . "Simon") (age . 18) (gender . "male"))
((name . "Cathy) (age . 17) (gender . "female"))
))
(define-syntax db:select
(syntax-rules (from where * = <)
((_ * from database where key = what)
(let lp ([d database] [r '()])
(if (null? d)
r
(if (equal? (assoc-ref (car d) key) what)
(lp (cdr d) (cons (car d) r))
(lp (cdr d) r)))))
((_ key1 from database where key2 < what)
(let lp ([d database] [r '()])
(if (null? d)
r
(if (< (assoc-ref (car d) key2) what)
(lp (cdr d) (cons (assoc-ref (car d) key1) r))
(lp (cdr d) r)))))))
------------cut----------
(db:select * from db where 'gender = "male")
==>(((name . "Simon") (age . 18) (gender . "male"))
((name . "John") (age . 16) (gender . "male")))
(db:select 'name from db where 'age < 18)
==>("Cathy" "John")
assoc-list用起来很方便,但是效率比较低。
4.1.3 Vector
-------------
Vector跟list最大的区别在于,vector可以随机存取,其存取时间不因vector的
长度而增加。简单地说,类似静态链表,或者C语言里的数组。vector的长度一旦
确定就不可以再变化了。vector的表示很简单,#(1 2 3)。当我们定义一个
vector时,可以有两种方式:
(define v1 (vector 1 2 3))
或者
(define v1 #(1 2 3))
根据索引获取对应值:
(vector-ref #(1 2 3 4 5) 3)相当于C中的arr[3 ]。
4.1.4 Hash table
-----------------
Guile的hash table,实质上是一个包含若干assoc的vector。
/----\
| 0 | ==>(key1 . val1)->(key8 . val8)->...冲突列表
| 1 | ==>(key2 . val2)
| 2 | ==>(key3 . val3)
| 3 | ...
| ... |
\----/
^
|
vector
大体上来说是这么个意思,vector的每一个cell会对应一个assoc,这个assoc包
含了所有对该cell索引存在冲突的键值对(key-value pair)。如果没有冲突,那
么每个cell只会有一个键值对——尽管它还是属于assoc,但这时候并不影响存取效
率。如果该索引存在较多冲突,那么对该索引的存取的效率等同于对assoc的存取
效率。这是显然的。但是在实际的hash table应用中,你的运气不会差到所有
key都算出同样的索引值吧?!况且,你可以自己定制hash函数。
————喂,我看手册上说,Guile提供了两种hash table!
——没错。你没错,它没错,我也没错。你怎么看?
* Abstract hash table
就实际应用来说,我们在Guile中真正要面对的是这个abstract hash table。而
不是刚才介绍那个“原始”的hash table实现。这种hash table有一个很好的特
性,就是能“伸缩自如”。我们都知道hash table尽管可以在固定时间内存取,但
是对内存的使用效率并不高。也就是说,即便你不知道未来会存多少数据进去,
也要提前分配一定空间给它,不管它最终是否能都用掉。另一方面,如果hash
table太小,可能在某个时候又要重新给它分配空间。
abstract hash table不需要你关心这些问题,它会自动帮你解决。甚至在定义它
的时候,也不需要告诉它初始表的大小,尽管这是个可选项。
(define ht (make-hash-table))
你也可以指定大小,不过abstract hash table的大小并不能任意指定,它有一个
固定的分配表: 31, 61, 113, 223, 443, 883, 1759, 3517, 7027, 14051,
28099, 56197, 112363, 224717, 449419, 898823, 1797641, 3595271,
7190537, 14381041, 28762081, 57524111, 115048217, 230096423, 460192829
所以hash table最终生成的大小只会在这个分配序列之内。比如:
(make-hash-table) ==> #<hash-table 0/31>
(make-hash-table 20) ==> #<hash-table 0/31>
(make-hash-table 32) ==> #<hash-table 0/61>
这个应该很好理解。如果在使用当中运行时发现hash table太小,它会自动扩张
的。我们选择hash table本来就是为了存取效率,浪费点内存无所谓,这年头内
存便宜。
* 原始hash table
这就是我一开始讲的vector+assoc方式的hash table,实际上abstract hash
table就是基于前者构建的,只不过它多增加了一个抽象层,所以表扩展能够自动
完成。这个抽象层不只是解决表扩展的问题,还可以让你定制hash函数,以及
key的比较函数。一般我们没有必要直接操作原始hash table,因为它是无法自动
扩展的——你喜欢手动也可以。
实际上,目前Guile没有提供直接控制原始hash table的过程,不过您可以自己写
个抽象层玩玩。
————我明明看到手册上说可以用hashq-set!控制!
——手册也是人写的...最好的办法是自己试试...
----------->一个简单的hash table抽象实现<----------
(define (my:make-hash-table size)
(cons size (make-vector size '()))) ;; 表结构为 (size . vector)
(define (my:hash-set! ht k v)
(let* ([size (car ht)] ;; 获取表大小
[table (cdr ht)] ;; 获取表
[i (hashv k size)] ;; 计算索引值,您可以自己定制hash函数,内置的hash函数为hashq/hashv
[ov (my:hash-ref ht k)] ;; 检查该key是否已经存在一个值
[nl (vector-ref table i)] ;; 获取冲突列表,若该索引无冲突则返回'()
[al (if ov (if (equal? ov v) #f (assoc-remove! nl k)) nl)]
)
(if al
(vector-set! table i `((,k . ,v) ,@al)))
v
))
(define (my:hash-ref ht k)
(let* ([size (car ht)] ;; 获取表大小
[table (cdr ht)] ;; 获取表
[i (hashv k size)] ;; 计算索引值,您可以自己定制hash函数,内置的hash函数为hashq/hashv
[al (vector-ref table i)] ;; 获取冲突列表,若该索引无冲突则返回'()
)
(assoc-ref al k)))
----------------->end<----------------------
现在来试试:
(define ht (my:make-hash-table 10))
==>(10 . #(() () () () () () () () () ()))
(my:hash-set! ht 'aaa "aaa")
==>"aaa"
ht
==>(10 . #(() () () () ((aaa . "aaa")) () () () () ()))
(my:hash-ref ht 'aaa)
==> "aaa"
要是您还没理解冲突是如何解决的,那么就多添加一些key-value,仔细观察看看。
至于自动扩展表之类的,您要是有兴趣就自己解决吧。
* 定制hash table
刚才我写的那段代码,确切地说是一些无用功,Guile内部已经有非常完备的
hash table抽象。但是您可以通过这些代码的运行过程,了解abstract hash
table究竟偷偷做了什么事情。定制hash table也需要重新实现刚才那样一组抽
象,只要利用一组过程就可以:
hashx-set! / hashx-ref / hashx-remove! / hashx-get-handle
很明显,要定制hash table,需要自己写两个函数,hash函数和assoc的比较函
数,实际上,一般情况下assoc的比较函数我们不怎么关心,可以直接沿用。
hash函数才关键。hash函数的定义如下,它需要两个参数,一个字符串用来生成
索引,还需要一个size用来控制该索引不要超过表的尺寸。或许您早就知道这个
定势:任何一个hash函数肯定最后一步都要对size求余(废话)。在Guile中,求
余有两个方法,remainder和modulo,区别您自己搜吧。
(define my-hash
(lambda (str size)
(remainder (您的hash函数 str) size)))
(define my-assoc
(lambda (str alist)
(assoc-ref alist str))) ;; 一般可直接沿用assoc-ref方法,当然您可以随意定制
(define (my:hash-ref ht key)
(hashx-ref my-hash my-assoc ht key))
(define (my:hash-set! ht key val)
(hashx-set! my-hash my-assoc ht key val))
按照这个模板,您就能轻松定制自己的hash table。
4.2 面向对象
=============
SICP的内容,总结起来就是四大抽象。
1、过程抽象
2、数据抽象
3、元语言抽象
4、机器抽象
这四大抽象是计算机程序构建的精髓所在。
————我不太喜欢听别人鼓吹“xx精髓”这种说法
——你对我进行言论审查?那么我修改一下:
这四大抽象是计算机程序构建的*敏感词*所在。
满意了吧?
应该指出的是,这四大抽象是互相关联的。必须用整体的观点来理解它们。但出
于实用主义考虑,我们不得不借助还原论,将它们还原为一些技术点。好让我们
清理出一个脉络来。
过程抽象大多数人都容易掌握,一般会自己建立较通用的高阶过程就可以了。
元语言抽象让您可以扩展语言本身,带来一些方便,如同我们之前构建的类sql查
询语言。我们当时采用了Scheme强大的宏来实现。宏本身不过是元语言抽象的语
法糖而已。我们之所以借助它,是为了更省力。当然它更大的用处是作为创造语
言的工具。但是必须指出,创造一门新语言,需要的更多不是技术上的东西,而
是哲学观。一门语言体现了一种独特的哲学观。如果一门语言没有自己独特的哲
学观,那么它只是语法形式的无聊堆砌——实际上,这同样体现了一种哲学观。
机器抽象带来两个东西:
一个是OS——机器功能模块的抽象,并以系统观点呈现出来;
另一个则是机器本身——我们通常把在软件层面上构建的机器本体称为虚拟机(VM)。
4.2.1 数据抽象
---------------
我们之前已经谈过closure——含有两个指针的,以参数为食物的,会返回结果的怪
物。但当我们真正要用到closure时,我们不能用眼睛看着这个怪物的本体,然后
控制它并指挥它如何工作。因为这样很累。
就如同我们上网聊天的时候,不会去操作套接字,不会去操控包传递,也不会去
用中断捕捉我们敲击键盘的行为。尽管我们心里非常清楚,这些东西才是我们整
个上网流程的实质。一个普遍的观点认为,当猿进化为人的时候,最显著的标志
是它懂得借助工具来减轻自己的劳动量。我并不赞同这个观点,因为据我所知,
比人更懂得使用工具的动物多了去了。但是人与它们的区别在于,人有一种本能
可以将外部的信息转化成内部信息,然后以一种新的形式将其应用于外部世界——
我们把这个过程简单称为,学习->思考->进化。以此循环反复。而动物只会根据
刺激条件来进行神经反射。猴子或者猿有那么一点点类似人的能力,可惜不多。
所以它们被认为接近人。
我们这么来思考:
1、我们已经学习过closure,它本来是外部信息;
2、现在closure的形象和定义就像一幅画一样在我脑海中浮现,这是内部信息;
3、直接操控这个怪物很累,于是我们期望有一种更便捷的方式来利用它;
4、它有一个指向code的指针,所以它能帮我们执行一些code,我们可以考虑交给
它什么样的code去执行;
5、所以它懂得执行code,我们只需要把需要的code写好,交给它,它会知道如何
完成工作;
6、它有一个指向环境的指针,它会帮我们记住环境的变化,我们可以考虑如何利
用它记忆的信息;
7、同样的,我们只需要告诉它:嘿,帮我记住这几样东西,a=1,b=2,c=3;
8、在任何时候,我们都确信它会完成工作,并肯定会以返回值汇报它的工作结果。
所以我们来构造这么一个“神秘”的closure,根据3,我们没必要自己去创建它并
操控它。
根据4,我们首先得写一些代码——可以写在它外部,也可以写在它内部,只要不影
响它使用就可以。
根据5,它必须要知道代码入口在哪里,我们给它一个code-entry。
根据6,我们得给它一个env-entry。
根据7,我们可以在任何时候让这个closure告诉我们它记住的东西(a,b,c)。
根据8,它是有用的。
(define (mystery)
(let ([a 1] [b 2] [c 3]) ;; env-entry
(define (get-value what)
(case what ((a) a) ((b) b) ((c) c)))
(define (set-a! what) (set! a what))
(define (set-b! what) (set! b what))
(define (set-c! what) (set! c what))
(define (print-all) (format #t "a=~a,b=~a,c=~a~%" a b c))
(define (plus-them-all) (+ a b c))
(lambda (how . what) ;; code-entry
(let ([what (or (null? what) (car what))])
(case how
((give-me what) (get-value what))
((set-a!) (set-a! what))
((set-b!) (set-b! what))
((set-c!) (set-c! what))
((print-all) (print-all))
((plus-them-all) (plus-them-all))
(else
(error "I don't know what you go to do!")))))
))
---------->运行<----------
(define aa (mystery))
(aa 'print-all)
==>a=1,b=2,c=3
(aa 'set-a! 5)
(aa print-all)
==>a=5,b=2,c=3
我们再重新定义一个实体出来看看:
(define bb (mystery))
(bb 'print-all)
==>a=1,b=2,c=3
我们看到由mystery定义出来的两个实体aa和bb是不会互相干扰的。
注意为什么是(define (mystery)而不是(define mystery?因为我们需要
mystery生成新的实体,而不是用自己本体来工作。我们也可以传入一些参数来做
初始化工作。这种情况下,mystery本身不能作为自己的实体,必须运行
它,mystery的运行结果才能作为mystery这个东西的实体。
(define aa (mystery))
如果我们想多要几个mystery的实体来使用而互不干扰,无需重复mytery的代码,
只需要依样定义即可。而后者是将mystery定义为一个普通函数,我们知道函数同
样是closure,但这时候mystery本身就已经是它自己的实体了,如果你在这个时
候定义(define aa mystery),那么aa就不会克隆出mystery的一个实体,而只能
作为mystery的一个别名。这样子aa一旦改变,mystery也随之改变——这意味着对
mystery的抽象失败了,因为这种抽象没有任何普适的应用意义。如果我们需要一
个跟mystery一模一样的实体,我们就得把mystery的代码原原本本再输入一次,
这可不好玩。
刚才这一切,我们用了我们所知道最基础的东西(没有任何新东西可言),折腾出
一个我们认为可以通用的一个“神秘”之物。这个“神秘”可以解决某一系列的问
题,每次我们面对类似问题时无需再重复编写这类代码。只要生成一个mystery的
实体即可。
4.2.2 对象
-----------
2.026 有了对象,世界才会以确定形式存在。
2.027 确定之物,存在之物,与对象是同一个东西。
2.0271 对象确定且存在,其配置可变且多变。
——维特根斯坦 《逻辑哲学论》
仍然从实用主义出发,我直接说结论。
现在我们把所掌握的面向对象概念套用到刚才的mystery上:
1、mystery本身不能直接用——类,而要生成新的实体再用——类的实例化;
2、mystery本身是一个函数,我们也可以定义成可传參的函数,用来做初始化工
作——构造函数;
3、mystery内包含一些变量a,b,c——属性;
4、mystery包含一些只有它才能调用的函数——方法;
5、以上——封装;
6、继承?你可以构造一个新的类,然后把mystery包含进去;
7、多态?至少函数重载很容易,你可以自己试试。
对了,我们现在可以把mystery改名为object了。
————引用维特根斯坦的话想说明什么?
——忽略它吧,这次没时间谈它了。我已经删了部分内容,但是仍然留着它,或许
有人会留心这三句话。
4.2.3 对象系统
---------------
如果我们是为了学习,那以上推导的一切都很有趣。但是不实用,没有谁会真的
在生产当中自己用最基础的东西封装出整个面向对象环境来,然后再用面向对象
解决问题。况且你真这么实现了,效率也很差。但是我们必须搞清楚它怎么来的。
对于绝大部分语言来说,它们都存在一整套已经封装并优化好的面向对象环境,
集成在语言内部供您使用。这么一整套东西称为对象系统。
有的语言是可以外接对象系统的,比如C语言,你可以外接Gobjct,虽然用起来蛮
搞笑的... 或许您掌握Guile之后会考虑让C干底层苦力活,然后让Guile在高层调
用您的C函数,这样就可以既享受C的底层效率又可以享受函数式编程的乐趣和方
便的面向对象。
Guile的对象系统叫做GOOPS(Guile Object-Oriented Programing System)。有趣
的是GOOPS并不完全是Guile社区的杰作,它是从Erick Gallesio的STk和Gregor
Kiczales的Tiny-Clos中派生出来的一个东东。与Common Lisp的对象系统CLOS非
常类似,但却是针对Scheme而设计的。
如果您熟悉Common Lisp或者CLOS,您会知道它们都是遵从MOP(Meta-Object
Protocol)协议设计的。或许您第一次听说MOP时与我一样惊讶——对象还有协议?
我之前已经告诉过您,Lisp like有足够的能力在语言之上来构建对象系统,而不
是依靠编译器或者编译器。普通语言无法脱离编译器或解释器本身来构建真正的
对象系统,即便是C,也要借力编译时的一些外部脚本;文艺语言如Lisp like,
可以一旦拥有别无所求,你不能问这样的问题:Lisp式的语言有没有xxx?你应该
这样问:目前有没有实现XXX,以后有没有兴趣实现?我说的是不对编译器/解释
器做任何修改就能实现。其他语言似乎就没有谈论的必要了。
MOP能够让您在语言本身上构建一个对象系统,只要顺着协议走就可以了,做通信
的同学大多喜欢有协议的感觉,见到通过验证的协议心里就踏实。不排除今后
Guile会出现另一套对象系统,虽然没什么必要,但某人总是对对象系统这种东西
有浓厚兴趣,跃跃欲试想写一个试试。不过即便真的有新对象系统出现,您基于
GOOPS开发的程序也不用担心,一定会兼容的,别忘了Lisp like的对象系统是可
外接的。
GOOPS不是完全用Guile代码写的,有一部分用C扩展写的,这很好理解,为了提速。
就我个人而言,不是太喜欢面向对象。以Guile的强大表现力,完全可以脱离基于
面向对象的设计模式。巧合的是,当我敲完上面这句话时,看见John Carmack在
twitter上转了一篇文章:
Object oriented vs. functional programming[9]
文中第一句话就认为,OO添加额外封装增加可读性,而FP尽可能减少冗余步骤降低
可读性。OO就是以效率为代价增加可读性的,而FP为了灵活性放弃了可读性,您
自己看着办吧,反正Guile把两样东西都放在您手中。不要痛恨做选择,这是一个
人之所以为人最基本的权利了,好好享受吧。
4.2.4 GOOPS
------------
(use-module (oop goops))
于是Guile里面所有的类型突然一瞬间都成了对象。
123是<integer>
"sdf"是<string>
'abc是<symbol>
对于Guile来说,类型的名称通常用<class-name>来表示,尖括号仅仅是名字的一
部分,没有任何特殊语法含义。定义类很简单:
(define-class <my-class> (父类列表)
属性列表
)
举个例子:
(define-class <aaa> ()
(a #:init-value 5)
(b #:init-value 0 #:accessor aaa:b)
)
(define aaa (make <aaa>)) ;; 类的实例化用make
(slot-ref aaa 'a) ;; 相当于aaa.a
==> 5
(slot-set! aaa 'a 9) ;; 相当于aaa.a=9
我们看到这样默认的属性存取方式显得麻烦,所以我们可以用accessor方式:
(aaa:b aaa) ;; 相当于aaa.b
==> 0
(set! (aaa:b aaa) 100) ;; 相当于aaa:b=100
采用accesor的好处是可以直接用set!来赋值。
* 方法定义和重载
(define-method (方法名 (self <目标对象>) 其余参数)
...)
(define-method (test (self <aaa>))
(slot-ref self 'a))
注意self这个名字可以任意取的,只要您知道它表示什么就行。这类似于this指
针,但是编译器会帮您偷偷传递进去。Guile的话,由于对象系统构建在语言之
上,所以self需要您自己传进去。
举例:
(define-method (+ (s1 <string>) (s2 <string>))
(string-append s1 s2))
(+ "sd" "Dd")
==> "sdDd"
我们注意到这个字符串的+方法,跟默认存在的integer的+方法重名了,这意味着
产生了一次重载。在Guile中,方法依靠参数类型和数目及排列来区分。
* 构造函数
如果您熟悉Ruby,那么这一节几乎可以跳过去了。构造函数很简单,您只要定义
以initialize为名称的函数即可。
(define-method (initialize (self <aaa>) . init-args)
(set! (aaa:b self) (car init-args))
(slot-set! aaa 'a (cadr init-args)))
(define aaa (make 'a 'b))
这样aaa在实例化时调用initialize,并初始化a='a,b='b。
* 继承
GOOPS在多重继承时,如果有两个及以上父类拥有重载的方法,那么调用优先级遵
循什么规则呢?如:
(define-class <aaa> (<a1> <a2> <a3>)
...)
将按照父类列表中从左到右的次序来调用重载方法。
您可以灵活运用next-method来处理重载方法之间的关系。请参阅手册。
GOOPS包含的东西太多,想要灵活运用也需要多思考和实践。
4.3 与C交互
============
4.3.1 FFI(Foreign Function Interface)
--------------------------------------
对于Guile来说,与C交互最简单的方式就是FFI。如果你有一个C编写的库,而你
手头也有这个库的API说明,那么在Guile中调用这些库的函数轻而易举。
比如我们自己封装一个库:
-----mmr.c------
int ffi_test(int a, int b)
{
return a+b;
}
-----end-------
生成库:
cc -shared -fPIC mmr.c -o libmmr.so
然后进入Guile:
(define libmmr (dynamic-link "./libmmr.so"))
(define ffi-test
(pointer->procedure int
(dynamic-func "ffi_test" libmmr)
(list int int)))
(ffi-test 1 2)
==> 3
应该一看就懂吧?其余的查手册就行了。
你也可以直接在Guile里面嵌入C代码,可以参考我写的guile-inline[10],用法很
简单:
(use-modules (inline inline) (system foreign))
(define in (make <inline> #:name "mytest"))
(inline:eat-code in
"int ffi_test(int a ,int b)
{
return a+b;
}"
)
(define ffi-test (inline:get in))
(ffi-test 1 2)
==> 3
效果跟前例是一样的。
实际上,Guile社区的一个原则是:与C应该尽可能使用FFI。这意味着还有另外一
种与C交互的方式。这是可以想见的,因为FFI隐藏了C模块中的数据类型,你只能
调用过程。如果C模块里面有个结构体你想直接操作,目前有三种方法:
1、在C模块中封装该结构体的get/set方法,而无法直接通过C语法来处理
;
比如 aaa->a = 5;
您得封装一个:
void set_a_from_aaa(mystruct *aaa ,int a)
{ aaa->a = 5; }
这样才可以通过FFI来交互。当然,这样很烦。
2、SMOB方式
3、在Guile中通过指针来反向struct(这个我真的不推荐,用起来很不舒服,但至少是一种可行方式)
4.3.2 SMOB(SMall OBject)
-------------------------
Guile允许您定义自己需要的新数据类型。相当于扩展Guile的类型。对于C语言来
说,这种类型扩展通常用typedef配合struct来完成。由于Guile主要是作为C程序
的扩展语言来设计的,所以我们必须要结合C语言的类型扩展方式来使用SMOB。
在这里我不会详细叙述SMOB的原生用法,因为它并不难,您查阅手册就可以了
解,而且在Guile源码的example中有比较详细的例程。我要介绍一种我自己常用
的SMOB定义方式,我在自己的通用服务器(Generic Server)项目Ragnarok[11]
中采用这种方式来实现消息处理[12],用于Guile代码跟select/epoll/kqueue的交
互,这种方式十分便捷。需要指出的是,这种方式不是我原创的,我是在
Joel Smith<joels@mobyfoo.org>的Guile-SDL项目中发现这种用法的,
它以GPL发布。我不确定这是他第一个发明的,但我绝对是从他的代码中第一次学到这种用法的。
BTW,似乎很多家伙对Guile写游戏十分感兴趣,据我所知Guile-SDL项目至少有3
个,实现上有所不同,不过大体一致,这还不包括我自己针对其中一个版本的修
改版作为自己今后某个项目的支撑。不过"官方”认定的是ttn的版本[12],说官方有
点搞笑,只是因为ttn最早做了这个版本,而且一直在维护,目前一般推荐这个。
实际黑客社区是类似兄弟会一般的存在,大家都很平等,没什么“官方”。如果大
家承认你的作品是"official",那说明你这家伙干的真不赖——当然某人也可以
自认official,只要他的脸皮足够厚。
我们姑且称这种方法为MSMOB,因为它用C宏做了一些封装,让您用起来更便捷(话
说这是四大抽象的第一抽象,过程抽象的体现)。这种用法只包含四个宏: obj是
结构体名称,cm是C代码里的字段名称,gm是Guile代码里你想要的字段名称,一般
这两个都用同一个不容易混淆,c_scm是从C到Guile的类型转换方法(可以查手
册),scm_c则反之。
SCM_OBJ_GETTER( obj, cm, gm, c_scm )
SCM_OBJ_SETTER( obj, cm, gm, c_scm, scm_c )
SCM_MAKE_GSUBR_OBJ_GET( obj, x )
SCM_MAKE_GSUBR_OBJ_SET( obj, x )
PS:您需要对这些宏做一些修改,这样可以获取自己想要的类型名称,我们以
aaa为例,get方法统一为aaa-x:get,set方法统一为aaa-x:set!
现在我们有一个结构体定义:
typedef struct AAA
{
int a;
char b;
char *ptr;
}aaa_t;
那么我们需要为每个字段定义get/set方法:
SCM_OBJ_GETTER(aaa_t ,a ,a ,scm_from_int);
SCM_OBJ_GETTER(aaa_t ,a ,a ,scm_to_int);
SCM_OBJ_GETTER(aaa_t ,b ,b ,scm_from_char);
SCM_OBJ_GETTER(aaa_t ,b ,b ,scm_to_char);
SCM_OBJ_GETTER(aaa_t ,ptr ,ptr ,scm_from_pointer);
SCM_OBJ_GETTER(aaa_t ,ptr ,ptr ,scm_to_pointer);
然后在注册函数中逐个注册进去,关于注册函数,我们稍候介绍Guile的原生C交
互方式时会提到:
SCM_MAKE_GSUBR_OBJ_GET(aaa_t ,a);
SCM_MAKE_GSUBR_OBJ_SET(aaa_t ,a);
...
这样我们就可以在Guile代码中处理aaa_t:
(aaa-a:set! aaa 5)
(aaa-a:get aaa)
==> 5
4.3.3 Guile与C的原生交互
-------------------------
目前为止,任何一门动态语言想要跟C交互,一般有两种方式:
1、接口自动转换,比如SWIG。这实际上跟语言的扩展能力没有关系,只是把一种
语言转成另一种语言而已;你把Guile代码转换成C,当然就可以跟C交互,反之亦
然;
2、原生交互,语言本身提供一系列C接口,可以跟C语言对接。
FFI是Guile-2.x的新特性,在此之前都采用原生交互。
Guile针对扩展性做了优化,比如与C交互速度更快,continuation的实现放弃了
效率增加兼容性。专门针对一个目的设计的语言,总是比通用语言要更有优势。
当然,你想把Guile当成一门通用语言,也没问题。
本文肯定不会讲解SWIG,SWIG意味着你要先获取源码,然后转换,最后再扩展。
Guile不需要这样,无论FFI还是原生扩展,只要你有人家程序/库的API文档就可
以了。即便人家没开源,您也可以做扩展。只要您没有用到Guile readline
module,Guile就可以按LGPL发布。
————Guile readline module是干什么的?
——这个问题充分证明了我的观点,您真的可以随意扩展并发布,因为不能随意的
那部分大多数人基本也没听过更别说用了...
我们没有必要过于详细地介绍原生交互,只需要记住它的定势即可,其余的主要
就是查手册。
* 定势
用C伪代码来展现:
// Guile的所有类型都是SCM,这很简单。
SCM func(SCM a ,SCM b)
{
// 1、先判断a和b是否为您需要的类型,因为Guile是动态语言,无须自己判断,C是静态语言,您得确保类型无误;
// 2、把a和b转化为相应的C类型:
类型1 c_a = scm_to_类型1(a);
类型2 c_b = scm_to_类型2(b);
// 3、OK,接下来是C编程,这个没什么好说的,就是普通C编程而已
// 需要注意的是,您可以在C编程里使用Guile的函数,它们每一个都有自己相对应的C接口;
// 另外,关于malloc,您也可以选择Guile的GC方式分配,好处是想分配就分配,不必考虑free,也不必手工处理堆。具体查文档。
// 4、将结果转换成SCM类型,并返回
return scm_from_返回类型(ret);
}
那怎么调用它呢?
首先您要知道,所有的C交互代码,都必须封装到一个动态链接库里,比如libmmr.so。
我们需要为这个库写一个注册函数,否则我们就得一个一个在Guile程序里注册,麻烦。
void my_register()
{
scm_c_define_gsubr("func" ,2 ,0 ,0 ,func);
}
说明下:
scm_c_define_gsubr("Guile中您想看到的函数名" ,固定参数数目 ,可变参数数
目 ,剩余参数数目 ,要注册的C函数)第2、3位的参数比较常用,第四个一般都是
0。
更多的请参考实际项目,可以参考我的Ragnarok项目[]。
4.4 许可证问题
===============
Guile-2.x的许可证比较复杂[15],虽然简单来说满足GPL。
1、libguile同时以LGPL或GPL3发布,您可以任意选择LGPL或GPL3;
2、Guile的readline module仅以GPL3发布;
3、Guile文档按照GFDL发布;
4、您用C跟Guile交互的代码,必须符合libguile的协议,但是由于1中提到
LGPL,所以您其实可以随意选择任何许可证;
5、如果您的C代码用到Guile readline module,那您必须将该代码以GPL发布;
6、如果您没有对Guile相关的库做任何链接,仅仅是把它当成一门Scheme方言使
用,那想怎么发布就怎么发布。
以上只是介绍,遇到法律问题请以律师解释为准。
总而言之如果您没用到Guile readline module,那您可以按自己喜欢的许
可证发布。当然,我还是强烈建议您采用GPL3——当您真正触碰到大型垄断企业的专
利地雷阵的时候,才会知道GPL3究竟是如何保护您的。否则您可以仅仅把GPL3当成
一个扯淡的玩意——如果您拒绝黑客伦理的话。
5 文献
~~~~~~~
[1] [http://www.vanderburg.org/OldPages/Tcl/war/0000.html]
[2] [http://mitpress.mit.edu/sicp/full-text/book/book.html]
[3] [http://www.scheme.com/tspl3/]
[4] [http://www.ccs.neu.edu/home/matthias/BTLS/]
[5] [http://www.ccs.neu.edu/home/matthias/BTSS/]
[6] [http://schemers.org/Documents/Standards/R5RS/]
[7] [http://www.r6rs.org/]
[8] [http://users.cecs.anu.edu.au/~baueran/thesis/baueran_thesis.pdf]
[9] [http://en.wikipedia.org/wiki/Church–Turing_thesis]
[10] [http://www.johndcook.com/blog/2010/11/03/object-oriented-vs-functional-programming/]
[11] [https://gitorious.org/guile-inline/]
[12] [https://gitorious.org/glow/ragnarok]
[13] [https://gitorious.org/glow/ragnarok/blobs/master/cee/event/rag_struct.h]
[14] [http://www.nongnu.org/guile-sdl/]
[15] [http://www.gnu.org/software/guile/manual/html_node/Guile-License.html]