├── javascripts └── main.js ├── images ├── body-bg.jpg ├── header-bg.jpg ├── sidebar-bg.jpg ├── github-button.png ├── highlight-bg.jpg └── download-button.png ├── params.json ├── README.md ├── stylesheets ├── github-light.css ├── print.css └── stylesheet.css ├── .emacs ├── index.html ├── Cocoa(Objective-C) 到 Lisp 的桥转换基本规则 (教程翻译).md ├── 用Lisp(CCL)调用Cocoa的nib文件-Nib-Loading 使用指导(教程翻译).md ├── Clozure CL 多线程函数详细说明.md └── Common Lisp 初学者快速入门指导.md /javascripts/main.js: -------------------------------------------------------------------------------- 1 | console.log('This would be the main JS file.'); 2 | -------------------------------------------------------------------------------- /images/body-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeBlues/PwML/HEAD/images/body-bg.jpg -------------------------------------------------------------------------------- /images/header-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeBlues/PwML/HEAD/images/header-bg.jpg -------------------------------------------------------------------------------- /images/sidebar-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeBlues/PwML/HEAD/images/sidebar-bg.jpg -------------------------------------------------------------------------------- /images/github-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeBlues/PwML/HEAD/images/github-button.png -------------------------------------------------------------------------------- /images/highlight-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeBlues/PwML/HEAD/images/highlight-bg.jpg -------------------------------------------------------------------------------- /images/download-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreeBlues/PwML/HEAD/images/download-button.png -------------------------------------------------------------------------------- /params.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pwml", 3 | "tagline": "Programming with Mother Language", 4 | "body": "### Welcome to GitHub Pages.\r\nThis automatic page generator is the easiest way to create beautiful pages for all of your projects. Author your page content here [using GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/), select a template crafted by a designer, and publish. After your page is generated, you can check out the new `gh-pages` branch locally. If you’re using GitHub Desktop, simply sync your repository and you’ll see the new branch.\r\n\r\n### Designer Templates\r\nWe’ve crafted some handsome templates for you to use. Go ahead and click 'Continue to layouts' to browse through them. You can easily go back to edit your page before publishing. After publishing your page, you can revisit the page generator and switch to another theme. Your Page content will be preserved.\r\n\r\n### Creating pages manually\r\nIf you prefer to not use the automatic generator, push a branch named `gh-pages` to your repository to create a page manually. In addition to supporting regular HTML content, GitHub Pages support Jekyll, a simple, blog aware static site generator. Jekyll makes it easy to create site-wide headers and footers without having to copy them across every page. It also offers intelligent blog support and other advanced templating features.\r\n\r\n### Authors and Contributors\r\nYou can @mention a GitHub username to generate a link to their profile. The resulting `` element will link to the contributor’s GitHub Profile. For example: In 2007, Chris Wanstrath (@defunkt), PJ Hyett (@pjhyett), and Tom Preston-Werner (@mojombo) founded GitHub.\r\n\r\n### Support or Contact\r\nHaving trouble with Pages? Check out our [documentation](https://help.github.com/pages) or [contact support](https://github.com/contact) and we’ll help you sort it out.\r\n", 5 | "note": "Don't delete this file! It's used internally to help with page regeneration." 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PwML 2 | 3 | --- 4 | 5 | ### **开源母语编程** 6 | ### **open source Programming with Mother Language** 7 | 8 | --- 9 | 10 | 前一阵子看代码看得双眼干涩、头脑发烫,尤其是英文自定义函数名,太短了不清楚意思,想清楚表达意思就要忍受长长的名字,而且类似的函数还不只一个,所有的自定义函数为了清楚表达含义,都很长、很长,看得人只想吐,于是萌发了用中文做函数名的念头,再进一步,用中文、标点符号、数字来写代码,再进一步,使用任何语言来写代码,比如梵语。 11 | 12 | 首先介绍一下背景知识,英文是一种拼音语言,中文是一种拼义语言,拼义语言这个概念是由香港中文大学心理学教授 [张学新] [1] 根据其对汉字的研究结果提炼出的一个概念,大概的理解就是英语是基于不同语音的组合,而中文则是基于不同含义的组合,详细的理论可以参考他的2012年发表在[《科学通报》] [2] 的论 13 | 文 14 | [《汉字拼义理论》] [3] 和 15 | [《顶中区N200: 一个中文视觉词汇识别特有的脑电反应》] [4] 16 | 17 | [1]: http://blog.sina.com.cn/s/articlelist_1787161060_0_1.html 18 | [2]: http://csb.scichina.com:8080/kxtb/CN/volumn/volumn_6364.shtml 19 | [3]: http://www.kuaipan.cn/index.php?ac=file&oid=6054104296062993 20 | [4]: http://csb.scichina.com:8080/kxtb/CN/abstract/abstract506586.shtml# 21 | 22 | 前面提到英文程序中的函数名称过长的问题,事实上用中文做函数名有先天优势,两个英文字母对应一个中文字符,两个中文字符组成的词语可以很清晰地表达一个概念。但是对应存储大小的四个英文字母却基本不可能表达清楚同样概念。 23 | 24 | 所以甚至如果你精通古代文言文,你还可以把函数名称简化为一个汉字字符--只要你自己能读懂,再举个例子,比如 **“define”** 这个关键字,直译为现代中文是 **“定义”** ,原来要6个字节,现在变成了4个字节,那么把它翻译为只占一个字节的古文言文单字该怎么翻?就是 **“名”** ,《道德经》的第一句大家肯定都很熟悉:“道可道,非常道,名可**名**,非常名”,这句话里出现的第二个“名”就是命名、定义的意思,看看下面列出的这三种形式的代码的比较: 25 | 26 | 英文代码: 27 | > define function1() 28 | 29 | 现代中文代码: 30 | > 定义 函数1() 31 | 32 | 文言文代码: 33 | > 名 函数1() 34 | 35 | 除了函数名称,关键字以及语法也可以以文言文的简约方式来定义,这样你写的程序就是文言文程序了,这样一想是不是觉得很有趣? 36 | 37 | 就像上面提到的“函数”这个概念,中文两个字符表达得很清楚,只占4个比特的存储空间,但是英文的“ function”就需要用到两倍个数的英文字符,要占8个比特的存储空间。 38 | 39 | 英语这种基于语音的特点,使得英文天生只能是一种一维线性文字,也就是说英文适合于听觉处理,而不适合视觉处理,而中文则恰恰相反,中文是一种基于视觉的二维文字,中文不需要那么多单独的读音去区分,因为它是各种基本概念的组合,中文的同音字特别多,就是因为中文主要依赖于视觉的识别。就像现在比较流行的二维条形码,因为设计者受拼音文字影响,所以设计成只适合机器识别的方式,人却无法直接看懂,其实后续的二维条形码可以设计为中文的样式,这样机器和人都可以阅读,这其实是中文的一种巨大优势。 40 | 41 | 因为程序员不可能依靠耳朵去听代码,只能是依靠视觉去看代码,所以在这种使用场景下,用中文作为程序语言要优于英文。 42 | 43 | 根据上面的分析,我们发现使用中文编程可以有效缩小源代码的规模,而且项目越大,这种效果越明显,如果使用现代中文的方式进行写作,源码体积可缩小 1/3 到 1/2,如果使用文言文的方式进行编程,源码体积可在现代中文的基础上再次缩小 1/3 到 1/2(当然这个只是粗略的估计,具体的数据等我完成源代码翻译器再进行精确的统计)。 44 | 45 | 有人可能会担心输入的工作量会增加,现在的开发环境一般都会有非常完善的自动完成功能,经过我的试验,使用 Emacs 编辑环境中的自动完成插件,输入的工作量跟使用英文基本持平,所以不必担心这一点。 46 | 47 | 目前中国国内有两种中文程序语言,一种是易语言,一种就是中文化的PYTHON,前者属于商业化软件,不适合进行探索性试验,后者则受限于 Python 语言本身的能力,也不太适合,在了解了 Common Lisp 的一些特性后,忽然发现 LISP 是最适合做开源母语开发的基础平台,首先是 Lisp 语言强大到逆天的宏能力,具备自定义一种新语言的能力,其次是全部开源的开发平台环境,再加上 Lisp 本身的迭代开发模式,非常适合我们用它来进行母语编程的探索。 48 | 49 | 初步计划是这样:先尝试搞一个中文编程的环境出来,然后再选择几种其他非英文类型的语言进行试验,逐步扩展,最终,地球上每个国家的程序员都可以自由使用自己的母语编程,当然也可以自由使用人类历史上存在过的任何一种语言文字编程,而且最重要的是程序员写的程序也可以被其他非编程专业的读者较为轻松地阅读。 50 | -------------------------------------------------------------------------------- /stylesheets/github-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 GitHub, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | .pl-c /* comment */ { 27 | color: #969896; 28 | } 29 | 30 | .pl-c1 /* constant, variable.other.constant, support, meta.property-name, support.constant, support.variable, meta.module-reference, markup.raw, meta.diff.header */, 31 | .pl-s .pl-v /* string variable */ { 32 | color: #0086b3; 33 | } 34 | 35 | .pl-e /* entity */, 36 | .pl-en /* entity.name */ { 37 | color: #795da3; 38 | } 39 | 40 | .pl-smi /* variable.parameter.function, storage.modifier.package, storage.modifier.import, storage.type.java, variable.other */, 41 | .pl-s .pl-s1 /* string source */ { 42 | color: #333; 43 | } 44 | 45 | .pl-ent /* entity.name.tag */ { 46 | color: #63a35c; 47 | } 48 | 49 | .pl-k /* keyword, storage, storage.type */ { 50 | color: #a71d5d; 51 | } 52 | 53 | .pl-s /* string */, 54 | .pl-pds /* punctuation.definition.string, string.regexp.character-class */, 55 | .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, 56 | .pl-sr /* string.regexp */, 57 | .pl-sr .pl-cce /* string.regexp constant.character.escape */, 58 | .pl-sr .pl-sre /* string.regexp source.ruby.embedded */, 59 | .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */ { 60 | color: #183691; 61 | } 62 | 63 | .pl-v /* variable */ { 64 | color: #ed6a43; 65 | } 66 | 67 | .pl-id /* invalid.deprecated */ { 68 | color: #b52a1d; 69 | } 70 | 71 | .pl-ii /* invalid.illegal */ { 72 | color: #f8f8f8; 73 | background-color: #b52a1d; 74 | } 75 | 76 | .pl-sr .pl-cce /* string.regexp constant.character.escape */ { 77 | font-weight: bold; 78 | color: #63a35c; 79 | } 80 | 81 | .pl-ml /* markup.list */ { 82 | color: #693a17; 83 | } 84 | 85 | .pl-mh /* markup.heading */, 86 | .pl-mh .pl-en /* markup.heading entity.name */, 87 | .pl-ms /* meta.separator */ { 88 | font-weight: bold; 89 | color: #1d3e81; 90 | } 91 | 92 | .pl-mq /* markup.quote */ { 93 | color: #008080; 94 | } 95 | 96 | .pl-mi /* markup.italic */ { 97 | font-style: italic; 98 | color: #333; 99 | } 100 | 101 | .pl-mb /* markup.bold */ { 102 | font-weight: bold; 103 | color: #333; 104 | } 105 | 106 | .pl-md /* markup.deleted, meta.diff.header.from-file */ { 107 | color: #bd2c00; 108 | background-color: #ffecec; 109 | } 110 | 111 | .pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { 112 | color: #55a532; 113 | background-color: #eaffea; 114 | } 115 | 116 | .pl-mdr /* meta.diff.range */ { 117 | font-weight: bold; 118 | color: #795da3; 119 | } 120 | 121 | .pl-mo /* meta.output */ { 122 | color: #1d3e81; 123 | } 124 | 125 | -------------------------------------------------------------------------------- /.emacs: -------------------------------------------------------------------------------- 1 | ;; .emacs 2 | 3 | ;;; begin of file 4 | 5 | (custom-set-variables 6 | ;; custom-set-variables was added by Custom. 7 | ;; If you edit it by hand, you could mess it up, so be careful. 8 | ;; Your init file should contain only one such instance. 9 | ;; If there is more than one, they won't work right. 10 | '(current-language-environment "Chinese-GBK") 11 | '(uniquify-buffer-name-style (quote forward) nil (uniquify))) 12 | (custom-set-faces 13 | ;; custom-set-faces was added by Custom. 14 | ;; If you edit it by hand, you could mess it up, so be careful. 15 | ;; Your init file should contain only one such instance. 16 | ;; If there is more than one, they won't work right. 17 | ) 18 | 19 | ;;; ============================================================ 20 | ;;; suppose "~/LispBox-0.92/" is your current working directory 21 | ;;; ============================================================ 22 | 23 | (set-language-environment "utf-8") 24 | 25 | (add-to-list 'load-path "~/LispBox-0.92/ccl-1.8-darwinx86/") 26 | (add-to-list 'load-path "~/LispBox-0.92/sbcl-1.0.55/") 27 | 28 | (add-to-list 'load-path "~/LispBox-0.92/slime-2012-11-13/") 29 | (setq load-path (cons "~/LispBox-0.92/slime-2012-11-13/" load-path)) 30 | 31 | ;(add-to-list 'load-path "~/.emacs.d/slime-2012-11-13/") 32 | ;(setq load-path (cons "~/.emacs.d/slime-2012-11-13/" load-path)) 33 | 34 | 35 | (global-font-lock-mode t) 36 | 37 | (font-lock-add-keywords 'lisp-mode '("[(]" "[)]")) 38 | (font-lock-add-keywords 'emacs-lisp-mode '("[(]" "[)]")) 39 | (font-lock-add-keywords 'lisp-interaction-mode '("[(]" "[)]")) 40 | 41 | 42 | (set-cursor-color "white") 43 | (set-mouse-color "blue") 44 | (set-foreground-color "green") 45 | (set-background-color "gray30") 46 | (set-border-color "lightgreen") 47 | (set-face-foreground 'highlight "red") 48 | (set-face-background 'highlight "lightblue") 49 | (set-face-foreground 'region "darkcyan") 50 | (set-face-background 'region "lightblue") 51 | (set-face-foreground 'secondary-selection "skyblue") 52 | (set-face-background 'secondary-selection "darkblue") 53 | 54 | 55 | (require 'linum) 56 | (setq linum-format "%3d ") 57 | (add-hook 'find-file-hooks (lambda () (linum-mode 1))) 58 | 59 | 60 | (add-to-list 'load-path "~/LispBox-0.92/auto-complete-1.3.1/") 61 | (require 'auto-complete-config) 62 | (add-to-list 'ac-dictionary-directories "~/LispBox-0.92/auto-complete-1.3.1/ac-dict") 63 | (ac-config-default) 64 | (add-hook 'lisp-mode-hook 65 | '(lambda () 66 | (define-key lisp-mode-map "\C-ca" 'auto-complete-mode))) 67 | 68 | 69 | (add-to-list 'default-frame-alist '(width . 120)) 70 | (add-to-list 'default-frame-alist '(height . 50)) 71 | 72 | 73 | (set-buffer-file-coding-system 'utf-8) 74 | (set-terminal-coding-system 'utf-8) 75 | (set-keyboard-coding-system 'utf-8) 76 | (set-selection-coding-system 'utf-8) 77 | (set-default-coding-systems 'utf-8) 78 | (set-clipboard-coding-system 'utf-8) 79 | (setq ansi-color-for-comint-mode t) 80 | (modify-coding-system-alist 'process "*" 'utf-8) 81 | (setq-default pathname-coding-system 'utf-8) 82 | (prefer-coding-system 'utf-8) 83 | (setq default-process-coding-system '(utf-8 . utf-8)) 84 | (setq locale-coding-system 'utf-8) 85 | (setq file-name-coding-system 'utf-8) 86 | (setq default-buffer-file-coding-system 'utf-8) 87 | (setq slime-net-coding-system 'utf-8-unix) 88 | 89 | 90 | ;;; Note that if you save a heap image, the character 91 | ;;; encoding specified on the command line will be preserved, 92 | ;;; and you won't have to specify the -K utf-8 any more. 93 | ;;; (setq inferior-lisp-program "/usr/local/bin/ccl64 -K utf-8") 94 | 95 | (setq inferior-lisp-program "dx86cl64 -K utf-8") 96 | (setq inferior-lisp-program "sbcl -K utf-8") 97 | 98 | (setq slime-lisp-implementations 99 | '((ccl ("/Users/admin/LispBox-0.92/ccl-1.8-darwinx86/dx86cl64") :coding-system utf-8-unix) 100 | (sbcl ("/Users/admin/LispBox-0.92/sbcl-1.0.55/sbcl") :coding-system utf-8-unix))) 101 | 102 | (require 'slime-autoloads) 103 | 104 | (global-set-key "\C-cs" 'slime-selector) 105 | 106 | (slime-setup '(slime-fancy slime-asdf slime-banner)) 107 | 108 | ;;; end of file 109 | -------------------------------------------------------------------------------- /stylesheets/print.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | padding: 0; 15 | margin: 0; 16 | font: inherit; 17 | font-size: 100%; 18 | vertical-align: baseline; 19 | border: 0; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-spacing: 0; 42 | border-collapse: collapse; 43 | } 44 | body { 45 | font-family: 'Helvetica Neue', Helvetica, Arial, serif; 46 | font-size: 13px; 47 | line-height: 1.5; 48 | color: #000; 49 | } 50 | 51 | a { 52 | font-weight: bold; 53 | color: #d5000d; 54 | } 55 | 56 | header { 57 | padding-top: 35px; 58 | padding-bottom: 10px; 59 | } 60 | 61 | header h1 { 62 | font-size: 48px; 63 | font-weight: bold; 64 | line-height: 1.2; 65 | color: #303030; 66 | letter-spacing: -1px; 67 | } 68 | 69 | header h2 { 70 | font-size: 24px; 71 | font-weight: normal; 72 | line-height: 1.3; 73 | color: #aaa; 74 | letter-spacing: -1px; 75 | } 76 | #downloads { 77 | display: none; 78 | } 79 | #main_content { 80 | padding-top: 20px; 81 | } 82 | 83 | code, pre { 84 | margin-bottom: 30px; 85 | font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal; 86 | font-size: 12px; 87 | color: #222; 88 | } 89 | 90 | code { 91 | padding: 0 3px; 92 | } 93 | 94 | pre { 95 | padding: 20px; 96 | overflow: auto; 97 | border: solid 1px #ddd; 98 | } 99 | pre code { 100 | padding: 0; 101 | } 102 | 103 | ul, ol, dl { 104 | margin-bottom: 20px; 105 | } 106 | 107 | 108 | /* COMMON STYLES */ 109 | 110 | table { 111 | width: 100%; 112 | border: 1px solid #ebebeb; 113 | } 114 | 115 | th { 116 | font-weight: 500; 117 | } 118 | 119 | td { 120 | font-weight: 300; 121 | text-align: center; 122 | border: 1px solid #ebebeb; 123 | } 124 | 125 | form { 126 | padding: 20px; 127 | background: #f2f2f2; 128 | 129 | } 130 | 131 | 132 | /* GENERAL ELEMENT TYPE STYLES */ 133 | 134 | h1 { 135 | font-size: 2.8em; 136 | } 137 | 138 | h2 { 139 | margin-bottom: 8px; 140 | font-size: 22px; 141 | font-weight: bold; 142 | color: #303030; 143 | } 144 | 145 | h3 { 146 | margin-bottom: 8px; 147 | font-size: 18px; 148 | font-weight: bold; 149 | color: #d5000d; 150 | } 151 | 152 | h4 { 153 | font-size: 16px; 154 | font-weight: bold; 155 | color: #303030; 156 | } 157 | 158 | h5 { 159 | font-size: 1em; 160 | color: #303030; 161 | } 162 | 163 | h6 { 164 | font-size: .8em; 165 | color: #303030; 166 | } 167 | 168 | p { 169 | margin-bottom: 20px; 170 | font-weight: 300; 171 | } 172 | 173 | a { 174 | text-decoration: none; 175 | } 176 | 177 | p a { 178 | font-weight: 400; 179 | } 180 | 181 | blockquote { 182 | padding: 0 0 0 30px; 183 | margin-bottom: 20px; 184 | font-size: 1.6em; 185 | border-left: 10px solid #e9e9e9; 186 | } 187 | 188 | ul li { 189 | padding-left: 20px; 190 | list-style-position: inside; 191 | list-style: disc; 192 | } 193 | 194 | ol li { 195 | padding-left: 3px; 196 | list-style-position: inside; 197 | list-style: decimal; 198 | } 199 | 200 | dl dd { 201 | font-style: italic; 202 | font-weight: 100; 203 | } 204 | 205 | footer { 206 | padding-top: 20px; 207 | padding-bottom: 30px; 208 | margin-top: 40px; 209 | font-size: 13px; 210 | color: #aaa; 211 | } 212 | 213 | footer a { 214 | color: #666; 215 | } 216 | 217 | /* MISC */ 218 | .clearfix:after { 219 | display: block; 220 | height: 0; 221 | clear: both; 222 | visibility: hidden; 223 | content: '.'; 224 | } 225 | 226 | .clearfix {display: inline-block;} 227 | * html .clearfix {height: 1%;} 228 | .clearfix {display: block;} 229 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | Pwml by FreeBlues 17 | 18 | 19 | 20 |
21 |
22 |

Pwml

23 |

Programming with Mother Language

24 |
View project on GitHub 25 |
26 |
27 | 28 |
29 |
30 |
31 |

32 | Welcome to GitHub Pages.

33 | 34 |

This automatic page generator is the easiest way to create beautiful pages for all of your projects. Author your page content here using GitHub Flavored Markdown, select a template crafted by a designer, and publish. After your page is generated, you can check out the new gh-pages branch locally. If you’re using GitHub Desktop, simply sync your repository and you’ll see the new branch.

35 | 36 |

37 | Designer Templates

38 | 39 |

We’ve crafted some handsome templates for you to use. Go ahead and click 'Continue to layouts' to browse through them. You can easily go back to edit your page before publishing. After publishing your page, you can revisit the page generator and switch to another theme. Your Page content will be preserved.

40 | 41 |

42 | Creating pages manually

43 | 44 |

If you prefer to not use the automatic generator, push a branch named gh-pages to your repository to create a page manually. In addition to supporting regular HTML content, GitHub Pages support Jekyll, a simple, blog aware static site generator. Jekyll makes it easy to create site-wide headers and footers without having to copy them across every page. It also offers intelligent blog support and other advanced templating features.

45 | 46 |

47 | Authors and Contributors

48 | 49 |

You can @mention a GitHub username to generate a link to their profile. The resulting <a> element will link to the contributor’s GitHub Profile. For example: In 2007, Chris Wanstrath (@defunkt), PJ Hyett (@pjhyett), and Tom Preston-Werner (@mojombo) founded GitHub.

50 | 51 |

52 | Support or Contact

53 | 54 |

Having trouble with Pages? Check out our documentation or contact support and we’ll help you sort it out.

55 |
56 | 57 | 71 |
72 |
73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Cocoa(Objective-C) 到 Lisp 的桥转换基本规则 (教程翻译).md: -------------------------------------------------------------------------------- 1 | # Cocoa(Objective-C) 到 Lisp 的桥转换基本规则 (教程翻译) 2 | 3 | === 4 | 原文地址: 5 | 网络: http://trac.clozure.com/ccl/wiki/CocoaBridgeTranslation 6 | 原文标题: 7 | Cocoa Bridge Translation 8 | 翻译者: 9 | FreeBlues 2013-07-18 10 | 11 | === 12 | 13 | ## 目录 14 | 15 | * [0 概述 Overview](#0) 16 | * [1 直接量 literals](#1) 17 | * [2 类型 types](#2) 18 | * [3 常量,枚举和变量 constants, enumerations and variables](#3) 19 | * [4 选择器 selectors](#4) 20 | * [5 类定义 class definition](#5) 21 | * [6 方法定义 method definition](#6) 22 | * [7 实例化对象 instantiating objects](#7) 23 | * [8 方法调用 method call](#8) 24 | * [9 调用设置器/设置属性 calling setters/setting properties](#9) 25 | 26 | 27 | ## [0 概述](id:0) 28 | 29 | 这里有一堆从 OBJ-C 代码到等效的 Clozure CL 的 Cocoa 桥代码之间的转换,示范不同的语言习惯如何编码。这些东西有些是 Clozure CL FFI 的一部分,未指定具体的桥,但它们被包含在这里给出一个总体概览。 30 | 31 | ## [1 直接量 literals](id:1) 32 | 33 | T 和 NIL 被映射到对应的布尔值 YES 和 NO. 所有数字也是可移植的. NSStrings 需要被明确地创建: 34 | 35 | Objective-C 的代码为: 36 | 37 | @"some string" 38 | 39 | 对应的 Lisp 代码变为: 40 | 41 | #@"some string" 42 | 43 | 如果你需要在 Lisp 字符串和 NSStrings 之间做转换, 下面的函数就是你想要的. 44 | 45 | (let ((a-lisp-string "foo")) 46 | (ccl::%make-nsstring a-lisp-string)) 47 | 48 | 并且当你接收到 NSStrings 时,你同样需要转换: 49 | 50 | (ccl::lisp-string-from-nsstring (#/title some-object)) 51 | 52 | Clozure CL 习惯于自动转换字符串, 不过这很容易引起内存管理问题. 所以一定要确保你所需要的任何 NSStrings 的保持/释放. 53 | 54 | nil 55 | NULL 56 | 57 | 这两个都是空指针. 在 Lisp 中要使用: 58 | 59 | ccl:+null-ptr+ 60 | 61 | 来表示它们. 62 | 63 | ## [2 类型 types](id:2) 64 | 65 | 在其他一些情况下,您可能需要使用类型(不是类)名称, 作为您定义的方法的返回值。 66 | 67 | Objective-C 的代码为: 68 | 69 | NSInteger 70 | BOOL 71 | 72 | 对应的 Lisp 代码变为: 73 | 74 | #>NSInteger 75 | #>BOOL 76 | 77 | ## [3 常量枚举和变量 constants, enumerations and variables](id:3) 78 | 79 | Objective-C 的代码为: 80 | 81 | NSTitledWindowMask 82 | NSUTF8StringEncoding 83 | 84 | 对应的 Lisp 代码变为: 85 | 86 | #$NSTitledWindowMask 87 | #$NSUTF8StringEncoding 88 | 89 | 90 | ## [4 选择器 selectors](id:4) 91 | 92 | Objective-C 的代码为: 93 | 94 | @selector(someSelector:withParams:) 95 | 96 | 对应的 Lisp 代码变为: 97 | 98 | (@selector "someSelector:withParams:") 99 | 100 | 译者注:就是要去掉 Objective-C 代码里的括号--为了避免和 Lisp 的括号发生混淆 101 | 102 | ## [5 类定义 class definition](id:5) 103 | 104 | Objective-C 的代码为: 105 | 106 | @interface SomeClass : SuperClass { 107 | IBOutlet NSString *aString; 108 | } 109 | 110 | 译者注: 这段的代码实际上是 Object-C 中对一个类的接口的定义, 保存在 .h 文件中,也就是头文件, 使用语法形式为: 111 | 112 | @interface 类名:父类名 113 | { 114 | 实例变量声明; 115 | } 116 | - 实例方法声明; 117 | + 类方法声明; 118 | @end 119 | 120 | 对应的 Lisp 代码变为: 121 | 122 | (defclass some-class (super-class) 123 | ((a-string :foreign-type :id)) 124 | (:metaclass ns:+ns-object)) 125 | 126 | ## [6 方法定义 method definition](id:6) 127 | 128 | Objective-C 的代码为: 129 | 130 | @implementation SomeClass // just included so we show the class name 131 | 132 | - (id) initWithFrame:(NSRect)frame andStuff:(id)stuff { 133 | if ((self = [super initWithFrame:frame])) { 134 | // body 135 | } 136 | return self; 137 | } 138 | 139 | - (void) viewDidAppear:(BOOL)animated { 140 | [super viewDidAppear:animated]; 141 | // body 142 | } 143 | 144 | 译者注: 这段的代码实际上是 Object-C 中对一个类的实现的定义, 具体内容为类的方法的实现代码, 保存在 .m 文件中,也就是存放代码的文件中, 类似于 C 的 .c 文件, 使用的语法形式为: 145 | 146 | @implementation 类名 147 | - 实例方法实现 148 | + 类方法实现 149 | @end 150 | 151 | 对应的 Lisp 代码变为: 152 | 153 | (objc:defmethod (#/initWithFrame:andStuff: :id) 154 | ((self some-class) (frame #>NSRect) (stuff :id)) 155 | (let ((new-self (#/initWithFrame: self frame))) 156 | (when new-self 157 | ;; body 158 | ) 159 | new-self)) 160 | 161 | (objc:defmethod (#/viewDidAppear: :void) ((self some-class) (animated #>BOOL)) 162 | (call-next-method)) 163 | ;; body 164 | ) 165 | 166 | 通常有 CALL-NEXT-METHOD (仅如预期在 OBJ-C 方法中工作),但是,这并不包括你在 OBJ-C 中需要的所有使用场景。如同在 init 方法中常见的,有时你需要使用跟当前方法不同的名字来调用一个 super-method。这就是 CALL-NEXT-METHOD 失败的地方。在上面的例子中, 自己来处理这个问题的最简单的方法是, 把调用 super 当做 调用 self 。 167 | 168 | 译者注:因为 Common Lisp 的面向对象系统 CLOS 的实现机制跟 Objective-C 有很大的不同, Lisp 的面向对象是基于广义函数的, 而 Objective-C 的面向对象是基于消息的, 所以这里的 Lisp 代码尽量按照 Objective-C 的风格来写了. 169 | 170 | ## [7 实例化对象 instantiating objects](id:7) 171 | 172 | Objective-C 的代码为: 173 | 174 | [[NSWindow alloc] initWithContentRect:NSRectMake(0, 0, 300, 300) 175 | styleMask:NSTitledWindowMask 176 | backing:NSBackingStoreBuffered 177 | defer:YES]; 178 | 179 | 对应的 Lisp 代码变为: 180 | 181 | (make-instance 'ns:ns-window 182 | :with-content-rect (ns:make-ns-rect 0 0 300 300) 183 | :style-mask #$NSTitledWindowMask 184 | :backing #$NSBackingStoreBuffered 185 | :defer t) 186 | 187 | ## [8 方法调用 method call](id:8) 188 | 189 | Objective-C 的代码为: 190 | 191 | [self doSomethingToObject:anObject]; 192 | [NSDate date]; 193 | 194 | 对应的 Lisp 代码变为: 195 | 196 | (#/doSomethingToObject: self an-object) 197 | (#/date ns:ns-date) 198 | 199 | 译者注: Objective-C 的方法调用语法为: 200 | 201 | [接收者 消息] 202 | 203 | 其中接收者是对象, 消息就是该对象要调用的方法, 所以写成 Common Lisp 的形式就需要把前后顺序调一下: 把方法函数放在前面, 调用方法函数的对象则作为方法函数的参数传递给方法函数. 204 | 205 | ## [9 调用设置器/设置属性 calling setters/setting properties](id:9) 206 | 207 | 你可以使用和调用任何其他方法相同的方式来调用 setter ,但要想使它们的工作更像是属性或槽,您也可以对它们使用 SETF。 208 | 209 | Objective-C 的代码为: 210 | 211 | [self setName:aName]; 212 | self.name = aName; // provided there is a @property defined 213 | 214 | 对应的 Lisp 代码变为: 215 | 216 | (#/setName: self aName) 217 | 218 | 或者是 219 | 220 | (setf (#/name self) a-name) 221 | 222 | 而 SETF 形式无论是否有一个定义好的属性都将工作,但必须存在一个 getter (例如 (#/name self))。这是因为 SETF 的用户期望把保存值返回,但 Cocoa 的 setters 一般​​没有返回值。 223 | 224 | -------------------------------------------------------------------------------- /用Lisp(CCL)调用Cocoa的nib文件-Nib-Loading 使用指导(教程翻译).md: -------------------------------------------------------------------------------- 1 | # 用Lisp(CCL)调用Cocoa的nib文件-Nib-Loading 使用指导(教程翻译) 2 | 3 | === 4 | 原文地址: 5 | 本地: ~/ccl-1.8-darwinx86/examples/cocoa/nib-loading/HOWTO.html 6 | 网络: http://trac.clozure.com/ccl/browser/trunk/source/examples/cocoa/nib-loading/HOWTO.html 7 | 原文标题: 8 | Nib-Loading HOWTO 9 | 翻译者: 10 | FreeBlues 2013-06-17 11 | 12 | === 13 | 14 | ## 目录 15 | 16 | * [0 概述](#0) 17 | * [1 Nibfiles 的相关知识](#1) 18 | * [2 对 Nibfiles 的使用](#2) 19 | * [3 如何调用一个 nibfile](#3) 20 | * [3.1 获取区域 zone](#3.1) 21 | * [3.2 建立字典](#3.2) 22 | * [3.3 调用 nibfile](#3.3) 23 | * [4 建立一个 nib-loading 函数](#4) 24 | * [5 如何卸载 nibfile](#5) 25 | 26 | 27 | ## [0 概述](id:0) 28 | 29 | 这篇教程说明如何通过对 Lisp 形式(forms)求值, 加载 nibfiles 到正在运行的 Clozure CL 副本中。你可能想通过这种方式加载 nibfiles 来为你正在工作的一个应用程序项目来测试用户界面元素,或使应用程序能够动态加载可选的用户界面元素。 30 | 31 | ## [1 Nibfiles 的相关知识](id:1) 32 | 33 | Cocoa应用程序开发有很大一部分是使用Cocoa框架来创建用户界面元素的。虽然它是完全有可能只是通过对框架的方法调用创建任何用户界面元素,设计用户界面更标准的方式是使用苹果的 InterfaceBuilder 应用程序去创建 nibfiles 文件, 这些文件归档于 Objective-C 对象,实现用户界面元素。 34 | 35 | Interface builder 是苹果的开发工具附带的一个应用程序。开发工具是 Mac OS X 自带的一个可选的安装,在你使用这个HOWTO之前,您需要确保您的系统上安装了苹果的开发工具。苹果的开发者计划的成员可以免费从苹果的开发者网站下载工具,但通常没有必要。你可以简单地使用 Mac OS X系统磁盘上可选的开发者工具安装程序来安装工具。 36 | 37 | ## [2 对 Nibfiles 的使用](id:2) 38 | 39 | 使用 InterfaceBuilder,您可以快速,轻松地创建窗口,对话框,文本字段,按钮和其他用户界面元素。你用 InterfaceBuilder 创建的的界面元素拥有符合苹果人机界面指南规定的标准的外观和行为。 40 | 41 | InterfaceBuilder 把对这些对象的描述保存在 nibfiles 文件中。这些文件包含归档的 Objective-C 类和对象表示。当你启动一个应用程序,并加载一个nibfile,Cocoa 运行时(runtime)在内存中创建这些Objective-C 对象,完成任何实例变量引用其他对象, 这些可能已被保存在 nibfile 文件中。总之,nibfile是一个已归档的用户界面对象集合,Cocoa 能够快速,轻松地在内存中把它复苏。 42 | 43 | Objective-C 程序员使用 nibfiles 一般的方式是将它们存储在应用程序束(bundle)中。应用程序的Info.plist 文件(也存储在 bundle)的指定哪个 nibfile 是应用程序的主要 nibfile,应用程序启动时自动加载该文件。应用程序也可以从 bundle 进行方法调用动态加载其他 nibfiles。 44 | 45 | 通过 Clozure CL 编写的 Lisp 应用程序也可以以同样时尚的方式来使用 nibfiles(见 “currency-converter” HOWTO “Cocoa” 的例子文件夹中),但Lisp程序员习惯于高度互动的开发,并可能想在一个运行着的Clozure CL 会话中简单地加载任意一个 nibfile 文件。幸运的是,这非常容易。 46 | 47 | ## [3 如何调用一个 nibfile](id:3) 48 | 49 | 让我们开始从 Clozure CL 的 Listener 窗口加载一个非常简单的nibfile。通过启动 Clozure CL 应用程序来开始。 50 | 51 | 在这份 HOWTO 文件相同的目录,你会发现一个 nibfile 名为 “hello.nib”。这是一个极其简单的nibfile, 它创建了一个带着一条单一问候语的 Cocoa 窗口。我们将使用输入到 Listener 窗口的 Lisp 形式来加载它。 52 | 53 | 我们要调用的 Objective-C 类方法 loadNibFile:externalNameTable:withZone: 来加载 nibfile 到内存中,按照文件中的描述来创建窗口。不过,首先,我们需要建立一些数据结构,我们将把这些数据结构传递给这个方法。 54 | 55 | loadNibFile:externalNameTable:withZone: 的参数就是一个路径名,一个字典对象,以及一个内存区域。随着每个 Objective-C 的方法调用,我们还把收到的消息传递给对象,在这种情况下是类 NSBundle的对象。 56 | 57 | 译者注: 58 | 这里需要了解 Objective-C 的类方法命名规则, 我暂时还没学会, 等彻底掌握了再详细解释 59 | 60 | 路径名仅仅是一个我们要加载的 nibfile 的引用。这个字典持有对象的引用。在这个简单的例子里,我们将使用它以识别 nibfile 的所有者,在这种情况下,所有者就是应用程序本身。该区域是对即将分配给 nibfile 对象的内存区域的引用。 61 | 62 | 不要担心,如果你对以上所述都无感的话, 用来创建这些对象的代码是简单明了的,并应有助于澄清这是怎么回事。 63 | 64 | ### [3.1 获取区域 Zone](id:3.1) 65 | 66 | 首先,我们将得到​​一个存储区。我们会告诉 Cocoa 在应用程序使用的同一区域中分配 nibfile 对象,因此通过询问应用程序正在使用的那个区域来获取一个区域是一件简单的事情。 67 | 68 | 我们可以要求任何应用之前,我们需要一个指向它的引用。当 Clozure CL 应用程序启动时,它把一个指向 Cocoa 应用程序对象的引用存储到一个特殊变量\*NSApp\*中。 69 | 70 | 从改变为 CCL 包开始; 我们将使用的大部分的实用功能都被定义在这个包中: 71 | 72 | ? (in-package :ccl) 73 | # 74 |        75 | 我们获得一个运行中的 Clozure CL 应用程序对象的引用在特殊变量\*NSApp*\中。我们可以询问它的区域,也就是它在内存中分配对象的区域: 76 | 77 | ? (setf *my-zone* (#/zone *NSApp*)) 78 | # 79 | 80 | 现在我们有一个应用程序的区域,这是我们需要传递给 loadNibFile:externalNameTable:withZone 的参数之一。 81 | 82 | (译者注: 其实就是获得了一个地址变量, 这个地址变量指向这个应用程序在内存中的入口地址) 83 | 84 | ### [3.2 建立字典](id:3.2) 85 | 86 | loadNibFile:externalNameTable:withZone: 的字典参数用于两个目的:识别 nibfile 的属主,并收集顶层(toplevel)对象。 87 | 88 | 本 nibfile 的属主变成雇主的顶层对象中创建加载的nibfile时,对象如窗口,按钮,等等。一个nibfile的所有者管理的对象加载时创建nibfile的,并为您的代码提供了一种方法来对这些对象的引用。您提供一个所有者对象字典,根据键“NSNibOwner”,。 89 | 90 | 顶层的对象都是对象,如窗户,被加载时创建的nibfile。为了收集这些,你可以传递一个NSMutableArray对象下的关键NSNibTopLevelObjects。 91 | 92 | 对于这第一个例子中,我们将通过一个所有者对象(应用程序对象),但我们并不需要收集顶层的对象,所以我们会省略NSNibTopLevelObjects,关键。 93 | 94 | [原文的错误代码:] 95 | ? (setf *my-dict* 96 | (#/dictionaryWithObject:forKey: ns:ns-mutable-dictionary 97 | *my-app* 98 | #@"NSNibOwner")) 99 | #; 101 | } (#x137F3DD0)> 102 | 103 | 译者注: 这段代码文档写错了, 里面的那个 *my-app* 要改为 *NSApp* 才可以得到后面的输出. 104 | 105 | [修改后的正确代码:] 106 | ? (setf *my-dict* 107 | (#/dictionaryWithObject:forKey: ns:ns-mutable-dictionary 108 | *NSApp* 109 | #@"NSNibOwner")) 110 | #; 112 | } (#x137F3DD0)> 113 | 114 | 115 | ### [3.3 调用 nibfile](id:3.3) 116 | 117 | 现在,我们有了我们需要的区域和字典,我们可以加载 nibfile。我们只需要以正确的路径名创建一个NSString 就可以了: 118 | 119 | ? (setf *nib-path* 120 | (%make-nsstring 121 | (namestring "~/LispBox-0.92/ccl-1.8-darwinx86//examples/cocoa/nib-loading/hello.nib"))) 122 | # 123 | ? 124 | 125 | 现在,我们可以实际加载 nibfile,传递我们已经创建对象的方法: 126 | 127 | ? (#/loadNibFile:externalNameTable:withZone: 128 | ns:ns-bundle 129 | *nib-path* 130 | *my-dict* 131 | *my-zone*) 132 | T 133 | 134 | 译者注: 成功后会弹出一个小窗口, 如下图所示: 135 | ![hello示意](http://static.oschina.net/uploads/space/2013/0617/233314_vbJ1_219279.png) 136 | 137 | “hello.nib” 文件中定义的窗口应该出现在屏幕上。 loadNibFile:externalNameTable:withZone: 方法返回T来表示它成功地加载了 nibfile,如果它失败了,它将返回NIL。 138 | 139 | 在这一点上,我们不再需要路径名和字典对象。 \*nib-path\* 我们必须释放: 140 | 141 | ? (setf *nib-path* (#/release *nib-path*)) 142 | NIL 143 | 144 | \*my-dict\* 实例没有被 #/alloc (或者被 MAKE-INSTANCE) 创建,所以这已经是自动释放,我们不需要再次释放。 145 | 146 | 147 | ## [4 建立一个 nib-loading 函数](id:4) 148 | 149 | 加载一个 nibfile 似乎就像我们可能要反复地做的事情,所以尽可能让它变得容易是有道理的。让我们做一个单一的函数,我们可以根据需要,调用它来加载一个 nib。 150 | 151 | nib-loading 函数可以把 nib 文件作为一个参数来加载,然后按照上一节中所列的步骤序列执行。如果我们仅仅按照字面意思去做,写出来的函数代码会是这个样子: 152 | 153 | (defun load-nibfile (nib-path) 154 | (let* ((app-zone (#/zone \*NSApp\*)) 155 | (nib-name (%make-nsstring (namestring nib-path))) 156 | (dict (#/dictionaryWithObject:forKey: 157 | ns-mutable-dictionary app #@"NSNibOwner"))) 158 | (#/loadNibFile:externalNameTable:withZone: ns:ns-bundle 159 | nib-name 160 | dict 161 | app-zone))) 162 | 163 | 使用这个函数的麻烦是,每次我们调用它都会泄漏字符串。返回前我们需要释放 nib-name。所以, 看看下面这个替代的版本如何? 164 | 165 | (defun load-nibfile (nib-path) 166 | (let* ((app-zone (#/zone \*NSApp*)) 167 | (nib-name (%make-nsstring (namestring nib-path))) 168 | (dict (#/dictionaryWithObject:forKey: 169 | ns-mutable-dictionary app #@"NSNibOwner")) 170 | (result (#/loadNibFile:externalNameTable:withZone: ns:ns-bundle 171 | nib-name 172 | dict 173 | app-zone))) 174 | (#/release nib-name) 175 | result)) 176 | 177 | 这个版本解决了泄漏问题,办法是: 把调用 loadNibFile:externalNameTable:withZone: 的结果绑定到 result,然后在返回调用结果之前,释放了 nib-name。 178 | 179 | 只是有一个问题:如果我们想用字典来收集 nibfile 的顶层对象,这样我们就可以从我们的代码访问到它们?我们需要函数的另一个版本。 180 | 181 | 为了收集顶层对象,我们将要传递 NSNibTopLevelObjects 给字典,它被存储在键NSMutableArrayObjects。因此,我们首先需要在 let 形式体里创建这样一个数组对象: 182 | 183 | (let* (... 184 | (objects-array (#/arrayWithCapacity: ns:ns-mutable-array 16)) 185 | ...) 186 | ...) 187 | 188 | 现在,我们有存储 nibfile 顶层对象的数组,我们需要修改创建字典的代码,使其不仅包含属主对象,也包含我们刚刚创建的数组: 189 | 190 | (let* (... 191 | (dict (#/dictionaryWithObjectsAndKeys: ns:ns-mutable-dictionary 192 | app #@"NSNibOwner" 193 | objects-array #&NSNibTopLevelObjects 194 | +null-ptr+)) 195 | ...) 196 | ...) 197 | 198 | 我们现在要把对象收集起来。我们会创建一个局部变量来存储它们,然后遍历数组对象把他们全都弄到。 (通常情况下,当我们要保持一个对象数组,我们必须把它保留下来。顶层 nib 对象是一种特殊情况:它们是由 nib 加载进程创建, 保留计数为1(a retain count of 1),当我们通过它们时我们负责释放它们)。 199 | 200 | (let* (... 201 | (toplevel-objects (list)) 202 | ...) 203 | (dotimes (i (#/count objects-array)) 204 | (setf toplevel-objects 205 | (cons (#/objectAtIndex: objects-array i) 206 | toplevel-objects))) 207 | ...) 208 | 209 | 收集对象后,就可以释放该数组,然后返回对象的列表。我们可能会想知道调用是否成功,仍然是可能的,所以我们使用变量 values 来返回顶层对象以及调用成功或失败。 210 | 211 | nib-loading 代码的最终版本看起来像这样: 212 | 213 | (defun load-nibfile (nib-path) 214 | (let* ((app-zone (#/zone \*NSApp\*)) 215 | (nib-name (%make-nsstring (namestring nib-path))) 216 | (objects-array (#/arrayWithCapacity: ns:ns-mutable-array 16)) 217 | (dict (#/dictionaryWithObjectsAndKeys: ns:ns-mutable-dictionary 218 | \*NSApp\* #@"NSNibOwner" 219 | objects-array #&NSNibTopLevelObjects 220 | +null-ptr+)) 221 | (toplevel-objects (list)) 222 | (result (#/loadNibFile:externalNameTable:withZone: ns:ns-bundle 223 | nib-name 224 | dict 225 | app-zone))) 226 | (dotimes (i (#/count objects-array)) 227 | (setf toplevel-objects 228 | (cons (#/objectAtIndex: objects-array i) 229 | toplevel-objects))) 230 | (#/release nib-name) 231 | (values toplevel-objects result))) 232 | 233 | 现在,我们可以拿一些合适的 nibfile 作为参数来调用这个函数,比如这个 HOWTO 文档中简单的的“hello.nib”: 234 | 235 | ? (ccl::load-nibfile "~/LispBox-0.92/ccl-1.8-darwinx86//examples/cocoa/nib-loading/hello.nib") 236 | (# (#x5B9810)> # (#x1F8BE0)>) 237 | T 238 | ? 239 | 240 | “hello!” 窗口出现在屏幕上,并且两个值被返回。第一个值是已加载的顶层对象列表。第二个值,T表示,已成功加载 nibfile。 241 | 242 | ## [5 如何卸载 nibfile](id:5) 243 | 244 | Cocoa 没有提供通用的的 nibfile-unloading API。替代方案是,如果你要卸载一个 nib,可接受的方法是关闭所有跟 nibfile 相关的窗口,并释放所有顶层对象。这是一个原因,你可能要对你传给 loadNibFile:externalNameTable的:withZone: 的字典对象使用 “NSNibTopLevelObjects” 键--来获得一个顶层对象集合在不再需要 nibfile 时释放这些对象。 245 | 246 | 在基于文档的 Cocoa 应用程序,主 nibfile 的属主通常是应用程序对象,并且在应用程序运行时,主 nibfile 永远不会被卸载。 副 nibfiles 的属主一般是控制器对象,通常是 NSWindowController 子类的实例。当你使用 NSWindowController 对象加载 nibfiles 时,他们负责加载和卸载 nibfile 对象。 247 | (译者注: 原文中拼写错误 Auxliliary , 正确应为 Auxiliary) 248 | 249 | 当你试验交互地加载 nibfile 时,您可能无法由创建 NSWindowController 对象加载 nibfiles 来开始,所以你可能需要自己手动做更多的对象管理。一方面,手动加载 nibfiles 可能是主要的应用程序的问题的来源。另一方面,如果您在交互式会话中长期试用 nib-loading ,很可能随着对生存着的并且可能被释放的对象的各种引用,你会被许多丢弃的对象塞满内存而退出。在使用 Listener 探索 Cocoa 时请务必记住这一点时。通过重启 Lisp 您可以随时让您的 Lisp 系统恢复到一个干净的状态,但是理所当然地,你将失去在探索中建立的任何状态。它往往是一个好主意,在一个文本文件上工作,而不是直接在 Listener 上工作,让你有一个你所做过试验的记录。这样的话,如果你需要重新开始(或者,如果你不小心会导致应用程序崩溃),你不会失去你已经获得的所有信息。 250 | 251 | ## 译者补充说明: 252 | 253 | 试验环境需要自行编译的 CCL-IDE 版本(Cocoa-IDE), 或者使用从苹果 APP STORE 下载的 Clozure CL 的 dmg 安装版本也可以, 非 IDE 版本暂时还没搞定, 比如 Emacs 使用的那个命令行的 CCL 版本, 文件名为 dx86cl64 , 这是因为默认编译出来的非 IDE 版本的特性里没有对 Cocoa 和 Objectiv-C 的支持, 输入 \*features\* 就可以看到不同版本支持哪些特性,如下: 254 | 255 | 1 unix 终端窗口命令行用的版本支持特性: 256 | 257 | Air:ccl-1.8-darwinx86 admin$ ./dx86cl64 258 | Welcome to Clozure Common Lisp Version 1.8-r15286M (DarwinX8664)! 259 | ? \*features\* 260 | (:PRIMARY-CLASSES :COMMON-LISP :OPENMCL :CCL :CCL-1.2 :CCL-1.3 :CCL-1.4 :CCL-1.5 :CCL-1.6 :CCL-1.7 :CCL-1.8 :CLOZURE :CLOZURE-COMMON-LISP :ANSI-CL :UNIX :OPENMCL-UNICODE-STRINGS :OPENMCL-NATIVE-THREADS :OPENMCL-PARTIAL-MOP :MCL-COMMON-MOP-SUBSET :OPENMCL-MOP-2 :OPENMCL-PRIVATE-HASH-TABLES :X86-64 :X86_64 :X86-TARGET :X86-HOST :X8664-TARGET :X8664-HOST :DARWIN-HOST :DARWIN-TARGET :DARWINX86-TARGET :DARWINX8664-TARGET :DARWINX8664-HOST :64-BIT-TARGET :64-BIT-HOST :DARWIN :LITTLE-ENDIAN-TARGET :LITTLE-ENDIAN-HOST) 261 | ? 262 | 263 | 2 Emacs 用的版本支持的特性:(比前者多了对 slime 的支持) 264 | 265 | CL-USER> \*features\* 266 | (:SWANK :PRIMARY-CLASSES :COMMON-LISP :OPENMCL :CCL :CCL-1.2 :CCL-1.3 :CCL-1.4 :CCL-1.5 :CCL-1.6 :CCL-1.7 :CCL-1.8 :CLOZURE :CLOZURE-COMMON-LISP :ANSI-CL :UNIX :OPENMCL-UNICODE-STRINGS :OPENMCL-NATIVE-THREADS :OPENMCL-PARTIAL-MOP :MCL-COMMON-MOP-SUBSET :OPENMCL-MOP-2 :OPENMCL-PRIVATE-HASH-TABLES :X86-64 :X86_64 :X86-TARGET :X86-HOST :X8664-TARGET :X8664-HOST :DARWIN-HOST :DARWIN-TARGET :DARWINX86-TARGET :DARWINX8664-TARGET :DARWINX8664-HOST :64-BIT-TARGET :64-BIT-HOST :DARWIN :LITTLE-ENDIAN-TARGET :LITTLE-ENDIAN-HOST) 267 | CL-USER> 268 | 269 | 3 自行编译的 Cocoa-IDE 版本支持的特性: 270 | 271 | ? \*features\* 272 | (:EASYGUI :ASDF2 :ASDF :HEMLOCK :APPLE-OBJC-2.0 :APPLE-OBJC :PRIMARY-CLASSES :COMMON-LISP :OPENMCL :CCL :CCL-1.2 :CCL-1.3 :CCL-1.4 :CCL-1.5 :CCL-1.6 :CCL-1.7 :CCL-1.8 :CLOZURE :CLOZURE-COMMON-LISP :ANSI-CL :UNIX :OPENMCL-UNICODE-STRINGS :OPENMCL-NATIVE-THREADS :OPENMCL-PARTIAL-MOP :MCL-COMMON-MOP-SUBSET :OPENMCL-MOP-2 :OPENMCL-PRIVATE-HASH-TABLES :X86-64 :X86_64 :X86-TARGET :X86-HOST :X8664-TARGET :X8664-HOST :DARWIN-HOST :DARWIN-TARGET :DARWINX86-TARGET :DARWINX8664-TARGET :DARWINX8664-HOST :64-BIT-TARGET :64-BIT-HOST :DARWIN :LITTLE-ENDIAN-TARGET :LITTLE-ENDIAN-HOST) 273 | ? 274 | 275 | 4 苹果 APP STORE 下载的 dmg 版本支持的特性: 276 | 277 | ? \*features\* 278 | (:EASYGUI :ASDF2 :ASDF :HEMLOCK :APPLE-OBJC-2.0 :APPLE-OBJC :PRIMARY-CLASSES :COMMON-LISP :OPENMCL :CCL :CCL-1.2 :CCL-1.3 :CCL-1.4 :CCL-1.5 :CCL-1.6 :CCL-1.7 :CCL-1.8 :CLOZURE :CLOZURE-COMMON-LISP :ANSI-CL :UNIX :OPENMCL-UNICODE-STRINGS :OPENMCL-NATIVE-THREADS :OPENMCL-PARTIAL-MOP :MCL-COMMON-MOP-SUBSET :OPENMCL-MOP-2 :OPENMCL-PRIVATE-HASH-TABLES :X86-64 :X86_64 :X86-TARGET :X86-HOST :X8664-TARGET :X8664-HOST :DARWIN-HOST :DARWIN-TARGET :DARWINX86-TARGET :DARWINX8664-TARGET :DARWINX8664-HOST :64-BIT-TARGET :64-BIT-HOST :DARWIN :LITTLE-ENDIAN-TARGET :LITTLE-ENDIAN-HOST) 279 | ? 280 | 281 | 容易看出, 主要的区别是前面, 后面基本一样. 282 | -------------------------------------------------------------------------------- /Clozure CL 多线程函数详细说明.md: -------------------------------------------------------------------------------- 1 | # Clozure CL 多线程函数详细说明 2 | 3 | `补充:本文尚未完成,存在多处拷贝错误,还需要修改,应某位网友要求先发布一个半成品的草稿,谢谢` 4 | 5 | ## 目录 6 | 7 | * [一、简单介绍](#1) 8 | * [二、多线程相关全局变量](#2) 9 | * [1、\*current-process\*](#2-1) 10 | * [2、\*ticks-per-second\*](#2-2) 11 | * [三、多线程相关函数](#3) 12 | * [1、all-processes](#3-1) 13 | * [2、make-proces](#3-2) 14 | * [3、process-suspend](#3-3) 15 | * [4、process-resume](#3-4) 16 | * [5、process-suspend-count](#3-5) 17 | * [6、process-preset](#3-6) 18 | * [7、process-enable](#3-7) 19 | * [8、process-run-function](#3-8) 20 | * [9、all-processes](#3-9) 21 | * [10、make-proces](#3-10) 22 | * [11、all-processes](#3-11) 23 | * [12、make-proces](#3-12) 24 | * [13、process-suspend](#3-13) 25 | * [14、process-resume](#3-14) 26 | * [15、process-suspend-count](#3-15) 27 | * [16、process-preset](#3-16) 28 | * [17、process-enable](#3-17) 29 | * [18、process-run-function](#3-18) 30 | * [19、all-processes](#3-19) 31 | * [20、make-proces](#3-20) 32 | * [21、all-processes](#3-21) 33 | * [22、make-proces](#3-22) 34 | * [23、process-suspend](#3-23) 35 | * [24、process-resume](#3-24) 36 | * [25、process-suspend-count](#3-25) 37 | * [26、process-preset](#3-26) 38 | * [27、process-enable](#3-27) 39 | * [28、process-run-function](#3-28) 40 | * [29、all-processes](#3-29) 41 | * [四、多线程相关宏](#4) 42 | * [1、without-interrupts](#4-1) 43 | * [2、with-interrupts-enabled](#4-2) 44 | * [3、with-lock-grabbed](#4-3) 45 | * [4、with-read-lock](#4-4) 46 | * [5、with-write-lock](#4-5) 47 | * [6、with-terminal-input](#4-6) 48 | * [7、待定](#4-7) 49 | 50 | 51 | 52 | 53 | 54 | ## [一、简单介绍](id:1) 55 | 56 | 本文主要根据 CCL 的官方文档多线程的内容(Threads Dictionary)进行翻译和扩展,为每个线程函数增加一到二个使用范例,便于初学者理解使用,如果感觉翻译有误,请以原文档为准。 57 | 58 | ## [二、多线程相关全局变量](id:2) 59 | 60 | ### [1、\*current-process\*](id:2-1) 61 | 62 | 实现功能:保存当前正处于激活态的线程 63 | 64 | 输入参数:无 65 | 66 | 返回结果:当前线程 67 | 68 | 使用方法:\*current-process\* 69 | 70 | 范例: 71 | 72 | #### 1)直接显示 73 | 74 | ? *current-process* 75 | # 76 | ? 77 | 78 | #### 2)绑定到全局变量 79 | 80 | ? (defparameter *selected-process* *current-process*) 81 | *SELECTED-PROCESS* 82 | ? *SELECTED-PROCESS* 83 | # 84 | ? 85 | 86 | ### 2、[\*ticks-per-second\*](id:2-2) 87 | 88 | 实现功能:每秒时间片 89 | 90 | 输入参数:无 91 | 92 | 返回结果:每秒时间片 93 | 94 | 使用方法:\*ticks-per-second\* 95 | 96 | 范例: 97 | 98 | #### 1)直接显示 99 | 100 | ? *ticks-per-second* 101 | 100 102 | ? 103 | 104 | 105 | ## [三、多线程相关函数](id:3) 106 | 107 | ### [1、all-processes](id:3-1) 108 | 109 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 110 | 111 | 输入参数:无 112 | 113 | 返回结果:线程对象的列表 114 | 115 | 使用方法:(all-processes) 116 | 117 | 【范例】: 118 | 119 | #### 1)直接获取线程列表 120 | 121 | ? (all-processes) 122 | (# # # # # #) 123 | ? 124 | 125 | #### 2)选择线程对象 126 | 127 | ? (defparameter *selected-process* (second (all-processes))) 128 | *SELECTED-PROCESS* 129 | ? *SELECTED-PROCESS* 130 | # 131 | ? 132 | 133 | #### 3) 循环打印全部线程对象 134 | 135 | ? (loop for i from 0 to (- (length (all-processes)) 1) 136 | do (print (nth i (all-processes)))) 137 | 138 | # 139 | # 140 | # 141 | # 142 | # 143 | # 144 | NIL 145 | ? 146 | 147 | ### [2、make-process](id:3-2) 148 | 149 | 实现功能:创建并返回一个新的线程对象 150 | 151 | 输入参数: 152 | 153 | name 线程名,字符串类型,如 "process1" 154 | &key persistent 布尔类型,默认为 nil 155 | priority 优先级,因为不同平台实现的复杂,该参数通常被忽略,默认为0 156 | class 新建线程对象的类,应该为 CCL:PROCESS 的子类,默认为 CCL:PROCESS 157 | initargs 所有准备传递给 MAKE-INSTANCE 的附加初始化参数列表,默认为() 158 | stack-size 新建线程的控制栈的大小,默认值为 CCL:*DEFAULT-CONTROL-STACK-SIZE* 159 | vstack-size 新建线程的数值栈的大小,默认值为 CCL:*DEFAULT-VALUE-STACK-SIZE* 160 | tstack-size 新建线程的临时栈的大小,默认值为 CCL:*DEFAULT-TEMP-STACK-SIZE* 161 | initial-bindings 一个 (symbol . valueform) 的 alist 对,默认值为 nil 162 | use-standard-initial-bindings 布尔类型,默认值为 T 163 | 164 | 返回结果:新创建的线程对象 165 | 166 | 附加描述: 167 | 168 | 使用指定的属性创建和返回一个线程对象,但是创建的线程对象不会立即执行,它需要首先被 preset (使用 process-preset),然后被 enable (使用 process-enable),之后才会实际运行起来 169 | 170 | 使用方法:(make-process “*process1*”) 171 | 172 | 【范例】: 173 | 174 | #### 1)使用默认值创建一个线程 175 | 176 | #### 2)指定参数值创建一个线程 177 | 178 | ### [3、process-suspend](id:3-3) 179 | 180 | 实现功能:挂起指定线程 181 | 182 | 输入参数:一个线程对象,目前只支持以全局变量的形式绑定的线程对象 183 | 184 | 返回结果:操作结果,T 或 nil 185 | 186 | 使用方法:(process-suspend *process*) 187 | 188 | 【范例】: 189 | 190 | #### 1)挂起一个处于 reset 状态的线程 191 | 192 | ? (all-processes) 193 | (# # # # # #) 194 | ? 195 | ?(defparameter *selected-process* (second (all-processes))) 196 | *SELECTED-PROCESS* 197 | ? *SELECTED-PROCESS* 198 | # 199 | ? (process-suspend *selected-process*) 200 | T 201 | ? *SELECTED-PROCESS* 202 | # 203 | ? 204 | 205 | 206 | #### 2)挂起一个处于 semaphore wait 状态的线程 207 | 208 | ? (defparameter *selected-process* (third (all-processes))) 209 | *SELECTED-PROCESS* 210 | ? *SELECTED-PROCESS* 211 | # 212 | ? (process-suspend *selected-process*) 213 | NIL 214 | ? *SELECTED-PROCESS* 215 | # 216 | ? 217 | 218 | ### [4、process-resume](id:3-4) 219 | 220 | 实现功能:恢复一个线程 221 | 222 | 输入参数:一个线程对象,目前只支持以全局变量的形式绑定的线程对象 223 | 224 | 返回结果:操作结果,T 或 nil 225 | 226 | 使用方法:(process-resume *process*) 227 | 228 | 【范例】: 229 | 230 | #### 1)恢复一个线程,返回 T 231 | 232 | ? (defparameter *selected-process* (second (all-processes))) 233 | *SELECTED-PROCESS* 234 | ? *SELECTED-PROCESS* 235 | # 236 | ? (process-resume *SELECTED-PROCESS*) 237 | T 238 | ? *SELECTED-PROCESS* 239 | # 240 | ? 241 | 242 | #### 2)恢复一个线程,返回 nil 243 | 244 | ? *SELECTED-PROCESS* 245 | # 246 | ? (process-resume *SELECTED-PROCESS*) 247 | NIL 248 | ? 249 | 250 | ### [5、process-suspend-count](id:3-5) 251 | 252 | 实现功能:线程挂起次数 253 | 254 | 输入参数:一个线程对象,目前只支持以全局变量的形式绑定的线程对象 255 | 256 | 返回结果:操作结果,一个数字 或 nil(线程过期则返回 nil) 257 | 258 | 使用方法:(process-suspend-count *process*) 259 | 260 | 【范例】: 261 | 262 | #### 1)返回线程挂起次数 263 | 264 | ? (defparameter *selected-process* (third (all-processes))) 265 | *SELECTED-PROCESS* 266 | ? *SELECTED-PROCESS* 267 | # 268 | ? (process-suspend-count *selected-process*) 269 | 1 270 | ? 271 | 272 | #### 2)返回线程挂起次数 273 | 274 | ? *SELECTED-PROCESS* 275 | # 276 | ? (process-suspend-count *selected-process*) 277 | 0 278 | ? 279 | 280 | ### [6、process-preset](id:3-6) 281 | 282 | 实现功能:为指定线程设置的初始化函数和参数 283 | 284 | 输入参数: 285 | 286 | process 一个线程对象,目前只支持以全局变量的形式绑定的线程对象 287 | function 一个函数 288 | &rest args 函数的参数 289 | 290 | 返回结果:未确定,取决于函数的返回结果 291 | 292 | 使用方法:(process-preset *process* function (arg1 arg2 argn)) 293 | 294 | 【范例】: 295 | 296 | #### 1)使用默认值创建一个线程 297 | 298 | #### 2)指定参数值创建一个线程 299 | 300 | ### [7、process-enable](id:3-7) 301 | 302 | 实现功能:开始执行指定线程的初始化函数 303 | 304 | 输入参数: 305 | 306 | process 一个线程对象,目前只支持以全局变量的形式绑定的线程对象 307 | &optional timeout 可选参数,超时时长,以秒为单位的时间间隔,最大不超过 32 位字长,默认为 1 308 | 309 | 返回结果:线程对象的列表 310 | 311 | 使用方法:(process-enable *process* 超时时间) 312 | 313 | 【范例】: 314 | 315 | #### 1)使用默认值创建一个线程 316 | 317 | #### 2)指定参数值创建一个线程 318 | 319 | 320 | ### [8、process-run-function](id:3-8) 321 | 322 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 323 | 324 | 输入参数: 325 | 326 | name 用来标识线程的字符串. 会传递给 make-process. 327 | function 函数,会传递给 make-process. 328 | persistent 布尔值,会传递给 make-process. 329 | priority 优先级,忽略 330 | class CCL:PROCESS的一个子类,会传递给 make-process. 331 | initargs 额外的初始化参数列表,会传递给 make-process. 332 | stack-size 控制栈大小,单位 bytes,会传递给 make-process. 333 | vstack-size 数值栈大小,单位 bytes,会传递给 make-process. 334 | tstack-size 临时栈大小,单位 bytes,会传递给 make-process. 335 | 336 | 返回结果:线程对象的列表 337 | 338 | 附加说明: 339 | 340 | 本函数 通过 make-process 创建一个线程,然后分别通过 process-preset 和 process-enable 对其 preset 和 enable,这样这个线程就可以立即运行,使用 process-run-function 是最简单的创建和执行线程的方法。 341 | 342 | 使用方法:(process-run-function process-specifier function &rest args) 343 | 344 | 【范例】: 345 | 346 | #### 1)使用默认值创建一个线程 347 | 348 | #### 2)指定参数值创建一个线程 349 | 350 | ### [9、process-interrupt](id:3-9) 351 | 352 | 实现功能:中断线程 353 | 354 | 输入参数: 355 | 356 | process---a lisp process (thread). 357 | function---a function. 358 | args---a list of values, appropriate as arguments to function. 359 | 360 | 返回结果:线程对象的列表 361 | 362 | 使用方法:(process-interrupt process function &rest args) 363 | 364 | 【范例】: 365 | 366 | #### 1)使用默认值创建一个线程 367 | 368 | #### 2)指定参数值创建一个线程 369 | 370 | ### [10、process-reset](id:3-10) 371 | 372 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 373 | 374 | 输入参数: 375 | 376 | process---a lisp process (thread). 377 | kill-option---an internal argument, must be nil. 378 | 379 | 返回结果:线程对象的列表 380 | 381 | 使用方法:(process-reset process &optional kill-option) 382 | 383 | 【范例】: 384 | 385 | #### 1)使用默认值创建一个线程 386 | 387 | #### 2)指定参数值创建一个线程 388 | 389 | ### [11、process-reset-and-enable](id:3-11) 390 | 391 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 392 | 393 | 输入参数:无 394 | 395 | 返回结果:线程对象的列表 396 | 397 | 使用方法:(process-reset-and-enable *process*) 398 | 399 | 【范例】: 400 | 401 | #### 1)使用默认值创建一个线程 402 | 403 | #### 2)指定参数值创建一个线程 404 | 405 | ### [12、process-kill](id:3-12) 406 | 407 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 408 | 409 | 输入参数:无 410 | 411 | 返回结果:线程对象的列表 412 | 413 | 使用方法:(process-kill process) 414 | 415 | 【范例】: 416 | 417 | #### 1)使用默认值创建一个线程 418 | 419 | #### 2)指定参数值创建一个线程 420 | 421 | ### [13、process-abort](id:3-13) 422 | 423 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 424 | 425 | 输入参数:无 426 | 427 | 返回结果:线程对象的列表 428 | 429 | 使用方法:(process-abort process &optional condition) 430 | 431 | 【范例】: 432 | 433 | #### 1)使用默认值创建一个线程 434 | 435 | #### 2)指定参数值创建一个线程 436 | 437 | ### [14、process-whostate](id:3-14) 438 | 439 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 440 | 441 | 输入参数:无 442 | 443 | 返回结果:线程对象的列表 444 | 445 | 使用方法:(process-whostate process) 446 | 447 | 【范例】: 448 | 449 | #### 1)使用默认值创建一个线程 450 | 451 | #### 2)指定参数值创建一个线程 452 | 453 | 454 | ### [15、process-allow-schedule](id:3-15) 455 | 456 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 457 | 458 | 输入参数:无 459 | 460 | 返回结果:线程对象的列表 461 | 462 | 使用方法:(process-allow-schedule) 463 | 464 | 【范例】: 465 | 466 | #### 1)使用默认值创建一个线程 467 | 468 | #### 2)指定参数值创建一个线程 469 | 470 | ### [16、make-process](id:3-16) 471 | 472 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 473 | 474 | 输入参数: 475 | 476 | whostate---a string, which will be the value of process-whostate while the process is waiting. 477 | function---a function, designated by itself or by a symbol which names it. 478 | args---a list of values, appropriate as arguments to function. 479 | 480 | 481 | 482 | 返回结果:线程对象的列表 483 | 484 | 使用方法:(process-wait whostate function &rest args) 485 | 486 | 【范例】: 487 | 488 | #### 1)使用默认值创建一个线程 489 | 490 | #### 2)指定参数值创建一个线程 491 | 492 | 493 | ### [17、process-wait-with-timeout](id:3-17) 494 | 495 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 496 | 497 | 输入参数: 498 | 499 | whostate---a string, which will be the value of process-whostate while the process is waiting. 500 | ticks---either a positive integer expressing a duration in "ticks" (see *ticks-per-second*), or NIL. 501 | function---a function, designated by itself or by a symbol which names it. 502 | args---a list of values, appropriate as arguments to function. 503 | 504 | 505 | 返回结果:线程对象的列表 506 | 507 | 使用方法:(process-wait-with-timeout whostate ticks function args) 508 | 509 | 【范例】: 510 | 511 | #### 1)使用默认值创建一个线程 512 | 513 | #### 2)指定参数值创建一个线程 514 | 515 | 516 | ### [18、make-lock](id:3-18) 517 | 518 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 519 | 520 | 输入参数: 521 | 522 | name---any lisp object; saved as part of lock. Typically a string or symbol which may appear in the process-whostates of threads which are waiting for lock. 523 | 524 | 返回结果:线程对象的列表 525 | 526 | 使用方法:(make-lock &optional name) 527 | 528 | 【范例】: 529 | 530 | #### 1)使用默认值创建一个线程 531 | 532 | #### 2)指定参数值创建一个线程 533 | 534 | 535 | ### [19、grab-lock](id:3-19) 536 | 537 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 538 | 539 | 输入参数:无 540 | 541 | 返回结果:线程对象的列表 542 | 543 | 使用方法:(grab-lock lock) 544 | 545 | 【范例】: 546 | 547 | #### 1)使用默认值创建一个线程 548 | 549 | #### 2)指定参数值创建一个线程 550 | 551 | ### [20、release-lock](id:3-20) 552 | 553 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 554 | 555 | 输入参数:无 556 | 557 | 返回结果:线程对象的列表 558 | 559 | 使用方法:(release-lock lock) 560 | 561 | 【范例】: 562 | 563 | #### 1)使用默认值创建一个线程 564 | 565 | #### 2)指定参数值创建一个线程 566 | 567 | ### [21、try-lock](id:3-21) 568 | 569 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 570 | 571 | 输入参数:无 572 | 573 | 返回结果:线程对象的列表 574 | 575 | 使用方法:(try-lock lock) 576 | 577 | 【范例】: 578 | 579 | #### 1)使用默认值创建一个线程 580 | 581 | #### 2)指定参数值创建一个线程 582 | 583 | ### [22、make-read-write-lock](id:3-22) 584 | 585 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 586 | 587 | 输入参数:无 588 | 589 | 返回结果:线程对象的列表 590 | 591 | 使用方法:(make-read-write-lock) 592 | 593 | 【范例】: 594 | 595 | #### 1)使用默认值创建一个线程 596 | 597 | #### 2)指定参数值创建一个线程 598 | 599 | ### [23、make-semaphore](id:3-23) 600 | 601 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 602 | 603 | 输入参数:无 604 | 605 | 返回结果:线程对象的列表 606 | 607 | 使用方法:(make-semaphore) 608 | 609 | 【范例】: 610 | 611 | #### 1)使用默认值创建一个线程 612 | 613 | #### 2)指定参数值创建一个线程 614 | 615 | ### [24、signal-semaphore](id:3-24) 616 | 617 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 618 | 619 | 输入参数:无 620 | 621 | 返回结果:线程对象的列表 622 | 623 | 使用方法:(signal-semaphore semaphore) 624 | 625 | 【范例】: 626 | 627 | #### 1)使用默认值创建一个线程 628 | 629 | #### 2)指定参数值创建一个线程 630 | 631 | ### [25、wait-on-semaphore](id:3-25) 632 | 633 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 634 | 635 | 输入参数:无 636 | 637 | 返回结果:线程对象的列表 638 | 639 | 使用方法:(wait-on-semaphore semaphore) 640 | 641 | 【范例】: 642 | 643 | #### 1)使用默认值创建一个线程 644 | 645 | #### 2)指定参数值创建一个线程 646 | 647 | ### [26、timed-wait-on-semaphore](id:3-26) 648 | 649 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 650 | 651 | 输入参数:无 652 | 653 | 返回结果:线程对象的列表 654 | 655 | 使用方法:(timed-wait-on-semaphore semaphore timeout) 656 | 657 | 【范例】: 658 | 659 | #### 1)使用默认值创建一个线程 660 | 661 | #### 2)指定参数值创建一个线程 662 | 663 | ### [27、process-input-wait](id:3-27) 664 | 665 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 666 | 667 | 输入参数:无 668 | 669 | 返回结果:线程对象的列表 670 | 671 | 使用方法:(process-input-wait fd &optional timeout) 672 | 673 | 【范例】: 674 | 675 | #### 1)使用默认值创建一个线程 676 | 677 | #### 2)指定参数值创建一个线程 678 | 679 | ### [28、process-output-wait](id:3-28) 680 | 681 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 682 | 683 | 输入参数: 684 | 685 | fd---a file descriptor, which is a non-negative integer used by the OS to refer to an open file, socket, or similar I/O connection. See ccl::stream-device. 686 | timeout---either NIL or a time interval in milliseconds. Must be a non-negative integer. The default is NIL. 687 | 688 | 689 | 返回结果:线程对象的列表 690 | 691 | 使用方法:(process-output-wait fd &optional timeout) 692 | 693 | 【范例】: 694 | 695 | #### 1)使用默认值创建一个线程 696 | 697 | #### 2)指定参数值创建一个线程 698 | 699 | ### [29、join-process](id:3-29) 700 | 701 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 702 | 703 | 输入参数: 704 | 705 | process---a process, typically created by process-run-function or by make-process 706 | 707 | default---A default value to be returned if the specified process doesn't exit normally. 708 | 709 | 710 | 711 | 返回结果:线程对象的列表 712 | 713 | 使用方法:(join-process process &optional default) 714 | 715 | 【范例】: 716 | 717 | #### 1)使用默认值创建一个线程 718 | 719 | #### 2)指定参数值创建一个线程 720 | 721 | 722 | ## [四、多线程相关宏](id:4) 723 | 724 | ### [1、without-interrupts](id:4-1) 725 | 726 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 727 | 728 | 输入参数: 729 | 730 | body---an implicit progn. 731 | 732 | 返回结果:线程对象的列表 733 | 734 | 使用方法:(without-interrupts &body body) 735 | 736 | 【范例】: 737 | 738 | #### 1)使用默认值创建一个线程 739 | 740 | #### 2)指定参数值创建一个线程 741 | 742 | 743 | ### [2、with-interrupts-enabled](id:4-2) 744 | 745 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 746 | 747 | 输入参数: 748 | 749 | body---an implicit progn. 750 | 751 | 返回结果:线程对象的列表 752 | 753 | 使用方法:(with-interrupts-enabled &body body) 754 | 755 | 【范例】: 756 | 757 | #### 1)使用默认值创建一个线程 758 | 759 | #### 2)指定参数值创建一个线程 760 | 761 | 762 | ### [3、with-lock-grabbed](id:4-3) 763 | 764 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 765 | 766 | 输入参数: 767 | 768 | lock---an object of type CCL:LOCK. 769 | body---an implicit progn. 770 | 771 | 返回结果:线程对象的列表 772 | 773 | 使用方法:(with-lock-grabbed (lock) &body body) 774 | 775 | 【范例】: 776 | 777 | #### 1)使用默认值创建一个线程 778 | 779 | #### 2)指定参数值创建一个线程 780 | 781 | 782 | ### [4、with-read-lock](id:4-4) 783 | 784 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 785 | 786 | 输入参数: 787 | 788 | read-write-lock---an object of type CCL:READ-WRITE-LOCK. 789 | body---an implicit progn. 790 | 791 | 返回结果:线程对象的列表 792 | 793 | 使用方法:(with-read-lock (read-write-lock) &body body) 794 | 795 | 【范例】: 796 | 797 | #### 1)使用默认值创建一个线程 798 | 799 | #### 2)指定参数值创建一个线程 800 | 801 | 802 | ### [5、with-write-lock](id:4-5) 803 | 804 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 805 | 806 | 输入参数: 807 | 808 | process---a process, typically created by process-run-function or by make-process 809 | 810 | default---A default value to be returned if the specified process doesn't exit normally. 811 | 812 | 813 | 814 | 返回结果:线程对象的列表 815 | 816 | 使用方法:with-write-lock (read-write-lock) &body body) 817 | 818 | 【范例】: 819 | 820 | #### 1)使用默认值创建一个线程 821 | 822 | #### 2)指定参数值创建一个线程 823 | 824 | 825 | ### [6、with-terminal-input](id:4-6) 826 | 827 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 828 | 829 | 输入参数: 830 | 831 | body---an implicit progn. 832 | 833 | 返回结果:线程对象的列表 834 | 835 | 使用方法:(with-terminal-input &body body) 836 | 837 | 【范例】: 838 | 839 | #### 1)使用默认值创建一个线程 840 | 841 | #### 2)指定参数值创建一个线程 842 | 843 | 844 | ### [7、待定](id:4-7) 845 | 846 | 实现功能:获取当前系统中所有已知 Lisp 线程的最新列表 847 | 848 | 输入参数: 849 | 850 | process---a process, typically created by process-run-function or by make-process 851 | 852 | default---A default value to be returned if the specified process doesn't exit normally. 853 | 854 | 返回结果:线程对象的列表 855 | 856 | 使用方法:(join-process process &optional default) 857 | 858 | 【范例】: 859 | 860 | #### 1)使用默认值创建一个线程 861 | 862 | #### 2)指定参数值创建一个线程 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | -------------------------------------------------------------------------------- /stylesheets/stylesheet.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | -ms-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | margin: 0.67em 0; 137 | font-size: 2em; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | color: #000; 146 | background: #ff0; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | position: relative; 164 | font-size: 75%; 165 | line-height: 0; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | height: 0; 213 | -moz-box-sizing: content-box; 214 | box-sizing: content-box; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | margin: 0; /* 3 */ 258 | font: inherit; /* 2 */ 259 | color: inherit; /* 1 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | padding: 0; 314 | border: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-box-sizing: content-box; /* 2 */ 359 | -moz-box-sizing: content-box; 360 | box-sizing: content-box; 361 | -webkit-appearance: textfield; /* 1 */ 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | padding: 0.35em 0.625em 0.75em; 381 | margin: 0 2px; 382 | border: 1px solid #c0c0c0; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | padding: 0; /* 2 */ 392 | border: 0; /* 1 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-spacing: 0; 421 | border-collapse: collapse; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | 429 | /* LAYOUT STYLES */ 430 | body { 431 | font-family: 'Helvetica Neue', Helvetica, Arial, serif; 432 | font-size: 15px; 433 | font-weight: 400; 434 | line-height: 1.5; 435 | color: #666; 436 | background: #fafafa url(../images/body-bg.jpg) 0 0 repeat; 437 | } 438 | 439 | p { 440 | margin-top: 0; 441 | } 442 | 443 | a { 444 | color: #2879d0; 445 | } 446 | a:hover { 447 | color: #2268b2; 448 | } 449 | 450 | header { 451 | padding-top: 40px; 452 | padding-bottom: 40px; 453 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 454 | background: #2e7bcf url(../images/header-bg.jpg) 0 0 repeat-x; 455 | border-bottom: solid 1px #275da1; 456 | } 457 | 458 | header h1 { 459 | width: 540px; 460 | margin-top: 0; 461 | margin-bottom: 0.2em; 462 | font-size: 72px; 463 | font-weight: normal; 464 | line-height: 1; 465 | color: #fff; 466 | letter-spacing: -1px; 467 | } 468 | 469 | header h2 { 470 | width: 540px; 471 | margin-top: 0; 472 | margin-bottom: 0; 473 | font-size: 26px; 474 | font-weight: normal; 475 | line-height: 1.3; 476 | color: #9ddcff; 477 | letter-spacing: 0; 478 | } 479 | 480 | .inner { 481 | position: relative; 482 | width: 940px; 483 | margin: 0 auto; 484 | } 485 | 486 | #content-wrapper { 487 | padding-top: 30px; 488 | border-top: solid 1px #fff; 489 | } 490 | 491 | #main-content { 492 | float: left; 493 | width: 690px; 494 | } 495 | 496 | #main-content img { 497 | max-width: 100%; 498 | } 499 | 500 | aside#sidebar { 501 | float: right; 502 | width: 200px; 503 | min-height: 504px; 504 | padding-left: 20px; 505 | font-size: 12px; 506 | line-height: 1.3; 507 | background: transparent url(../images/sidebar-bg.jpg) 0 0 no-repeat; 508 | } 509 | 510 | aside#sidebar p.repo-owner, 511 | aside#sidebar p.repo-owner a { 512 | font-weight: bold; 513 | } 514 | 515 | #downloads { 516 | margin-bottom: 40px; 517 | } 518 | 519 | a.button { 520 | width: 134px; 521 | height: 58px; 522 | padding-top: 22px; 523 | padding-left: 68px; 524 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 525 | font-size: 23px; 526 | line-height: 1.2; 527 | color: #fff; 528 | } 529 | a.button small { 530 | display: block; 531 | font-size: 11px; 532 | } 533 | header a.button { 534 | position: absolute; 535 | top: 0; 536 | right: 0; 537 | background: transparent url(../images/github-button.png) 0 0 no-repeat; 538 | } 539 | aside a.button { 540 | display: block; 541 | width: 138px; 542 | padding-left: 64px; 543 | margin-bottom: 20px; 544 | font-size: 21px; 545 | background: transparent url(../images/download-button.png) 0 0 no-repeat; 546 | } 547 | 548 | code, pre { 549 | margin-bottom: 30px; 550 | font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; 551 | font-size: 13px; 552 | color: #222; 553 | } 554 | 555 | code { 556 | padding: 0 3px; 557 | background-color: #f2f8fc; 558 | border: solid 1px #dbe7f3; 559 | } 560 | 561 | pre { 562 | padding: 20px; 563 | overflow: auto; 564 | text-shadow: none; 565 | background: #fff; 566 | border: solid 1px #f2f2f2; 567 | } 568 | pre code { 569 | padding: 0; 570 | color: #2879d0; 571 | background-color: #fff; 572 | border: none; 573 | } 574 | 575 | ul, ol, dl { 576 | margin-bottom: 20px; 577 | } 578 | 579 | 580 | /* COMMON STYLES */ 581 | 582 | hr { 583 | height: 0; 584 | margin-top: 1em; 585 | margin-bottom: 1em; 586 | border: 0; 587 | border-top: solid 1px #ddd; 588 | } 589 | 590 | table { 591 | width: 100%; 592 | border: 1px solid #ebebeb; 593 | } 594 | 595 | th { 596 | font-weight: 500; 597 | } 598 | 599 | td { 600 | font-weight: 300; 601 | text-align: center; 602 | border: 1px solid #ebebeb; 603 | } 604 | 605 | form { 606 | padding: 20px; 607 | background: #f2f2f2; 608 | 609 | } 610 | 611 | 612 | /* GENERAL ELEMENT TYPE STYLES */ 613 | 614 | #main-content h1 { 615 | margin-top: 0; 616 | margin-bottom: 0; 617 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 618 | font-size: 2.8em; 619 | font-weight: normal; 620 | color: #474747; 621 | text-indent: 6px; 622 | letter-spacing: -1px; 623 | } 624 | 625 | #main-content h1:before { 626 | padding-right: 0.3em; 627 | margin-left: -0.9em; 628 | color: #9ddcff; 629 | content: "/"; 630 | } 631 | 632 | #main-content h2 { 633 | margin-bottom: 8px; 634 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 635 | font-size: 22px; 636 | font-weight: bold; 637 | color: #474747; 638 | text-indent: 4px; 639 | } 640 | #main-content h2:before { 641 | padding-right: 0.3em; 642 | margin-left: -1.5em; 643 | content: "//"; 644 | color: #9ddcff; 645 | } 646 | 647 | #main-content h3 { 648 | margin-top: 24px; 649 | margin-bottom: 8px; 650 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 651 | font-size: 18px; 652 | font-weight: bold; 653 | color: #474747; 654 | text-indent: 3px; 655 | } 656 | 657 | #main-content h3:before { 658 | padding-right: 0.3em; 659 | margin-left: -2em; 660 | content: "///"; 661 | color: #9ddcff; 662 | } 663 | 664 | #main-content h4 { 665 | margin-bottom: 8px; 666 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 667 | font-size: 15px; 668 | font-weight: bold; 669 | color: #474747; 670 | text-indent: 3px; 671 | } 672 | 673 | h4:before { 674 | padding-right: 0.3em; 675 | margin-left: -2.8em; 676 | content: "////"; 677 | color: #9ddcff; 678 | } 679 | 680 | #main-content h5 { 681 | margin-bottom: 8px; 682 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 683 | font-size: 14px; 684 | color: #474747; 685 | text-indent: 3px; 686 | } 687 | h5:before { 688 | padding-right: 0.3em; 689 | margin-left: -3.2em; 690 | content: "/////"; 691 | color: #9ddcff; 692 | } 693 | 694 | #main-content h6 { 695 | margin-bottom: 8px; 696 | font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; 697 | font-size: .8em; 698 | color: #474747; 699 | text-indent: 3px; 700 | } 701 | h6:before { 702 | padding-right: 0.3em; 703 | margin-left: -3.7em; 704 | content: "//////"; 705 | color: #9ddcff; 706 | } 707 | 708 | p { 709 | margin-bottom: 20px; 710 | } 711 | 712 | a { 713 | text-decoration: none; 714 | } 715 | 716 | p a { 717 | font-weight: 400; 718 | } 719 | 720 | blockquote { 721 | padding: 0 0 0 30px; 722 | margin-bottom: 20px; 723 | font-size: 1.6em; 724 | border-left: 10px solid #e9e9e9; 725 | } 726 | 727 | ul { 728 | list-style-position: inside; 729 | list-style: disc; 730 | padding-left: 20px; 731 | } 732 | 733 | ol { 734 | list-style-position: inside; 735 | list-style: decimal; 736 | padding-left: 3px; 737 | } 738 | 739 | dl dd { 740 | font-style: italic; 741 | font-weight: 100; 742 | } 743 | 744 | footer { 745 | padding-top: 20px; 746 | padding-bottom: 30px; 747 | margin-top: 40px; 748 | font-size: 13px; 749 | color: #aaa; 750 | background: transparent url('../images/hr.png') 0 0 no-repeat; 751 | } 752 | 753 | footer a { 754 | color: #666; 755 | } 756 | footer a:hover { 757 | color: #444; 758 | } 759 | 760 | /* MISC */ 761 | .clearfix:after { 762 | display: block; 763 | height: 0; 764 | clear: both; 765 | visibility: hidden; 766 | content: '.'; 767 | } 768 | 769 | .clearfix {display: inline-block;} 770 | * html .clearfix {height: 1%;} 771 | .clearfix {display: block;} 772 | 773 | /* #Media Queries 774 | ================================================== */ 775 | 776 | /* Smaller than standard 960 (devices and browsers) */ 777 | @media only screen and (max-width: 959px) { } 778 | 779 | /* Tablet Portrait size to standard 960 (devices and browsers) */ 780 | @media only screen and (min-width: 768px) and (max-width: 959px) { 781 | .inner { 782 | width: 740px; 783 | } 784 | header h1, header h2 { 785 | width: 340px; 786 | } 787 | header h1 { 788 | font-size: 60px; 789 | } 790 | header h2 { 791 | font-size: 30px; 792 | } 793 | #main-content { 794 | width: 490px; 795 | } 796 | #main-content h1:before, 797 | #main-content h2:before, 798 | #main-content h3:before, 799 | #main-content h4:before, 800 | #main-content h5:before, 801 | #main-content h6:before { 802 | padding-right: 0; 803 | margin-left: 0; 804 | content: none; 805 | } 806 | } 807 | 808 | /* All Mobile Sizes (devices and browser) */ 809 | @media only screen and (max-width: 767px) { 810 | .inner { 811 | width: 93%; 812 | } 813 | header { 814 | padding: 20px 0; 815 | } 816 | header .inner { 817 | position: relative; 818 | } 819 | header h1, header h2 { 820 | width: 100%; 821 | } 822 | header h1 { 823 | font-size: 48px; 824 | } 825 | header h2 { 826 | font-size: 24px; 827 | } 828 | header a.button { 829 | position: relative; 830 | display: inline-block; 831 | width: auto; 832 | height: auto; 833 | padding: 5px 10px; 834 | margin-top: 15px; 835 | font-size: 13px; 836 | line-height: 1; 837 | color: #2879d0; 838 | text-align: center; 839 | background-color: #9ddcff; 840 | background-image: none; 841 | border-radius: 5px; 842 | -moz-border-radius: 5px; 843 | -webkit-border-radius: 5px; 844 | } 845 | header a.button small { 846 | display: inline; 847 | font-size: 13px; 848 | } 849 | #main-content, 850 | aside#sidebar { 851 | float: none; 852 | width: 100% ! important; 853 | } 854 | aside#sidebar { 855 | min-height: 0; 856 | padding: 20px 0; 857 | margin-top: 20px; 858 | background-image: none; 859 | border-top: solid 1px #ddd; 860 | } 861 | aside#sidebar a.button { 862 | display: none; 863 | } 864 | #main-content h1:before, 865 | #main-content h2:before, 866 | #main-content h3:before, 867 | #main-content h4:before, 868 | #main-content h5:before, 869 | #main-content h6:before { 870 | padding-right: 0; 871 | margin-left: 0; 872 | content: none; 873 | } 874 | } 875 | 876 | /* Mobile Landscape Size to Tablet Portrait (devices and browsers) */ 877 | @media only screen and (min-width: 480px) and (max-width: 767px) { } 878 | 879 | /* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */ 880 | @media only screen and (max-width: 479px) { } 881 | 882 | -------------------------------------------------------------------------------- /Common Lisp 初学者快速入门指导.md: -------------------------------------------------------------------------------- 1 | # Common Lisp 初学者快速入门指导 2 | 3 | V 0.90 4 | 5 | ## 目录 6 | 7 | * [一、简单介绍](#1) 8 | * [1、本文目标](#1.1) 9 | * [2、适用读者](#1.2) 10 | * [3、迭代式学习](#1.3) 11 | * [4、本章内容小结](#1.4) 12 | 13 | * [二、快速上手](#2) 14 | * [1、推荐开发环境 Lispbox ](#2.1) 15 | * [2、开发环境简要介绍](#2.2) 16 | * [3、第一个简单的 Lisp 程序](#2.3) 17 | * [4、为程序增加些复杂性](#2.4) 18 | * [5、这么好的程序一定要保存起来](#2.5) 19 | * [6、补充阅读:让程序支持中文符号](#2.6) 20 | * [7、本章内容小结](#2.7) 21 | 22 | * [三、适用于初学者的基本概念](#3) 23 | * [1、Lisp 程序由 S-表达式组成,表达式是列表或单个原子](#3.1) 24 | * [2、Lisp 中的列表是什么样的?](#3.2) 25 | * [3、Lisp 中的原子又是什么样的?](#3.3) 26 | * [4、Lisp 中求值的概念:对数字、符号、字符串和列表求值](#3.4) 27 | * [5、对列表中函数调用形式、宏形式和特殊形式求值](#3.5) 28 | * [6、单引号的特殊作用--构建宏的基础](#3.6) 29 | * [7、本章内容小结](#3.7) 30 | 31 | * [四、一个简单实践:文本数据库-藏书阁](#4) 32 | * [1、项目简单说明](#4.1) 33 | * [2、定义你的原型数据结构](#4.2) 34 | * [3、开工:首先是数据录入模块](#4.3) 35 | * [4、其次是数据显示模块](#4.4) 36 | * [5、充分发挥迭代的优势:改进一下输入方式](#4.5) 37 | * [6、保存和加载已经录入的数据](#4.6) 38 | * [7、查询数据库](#4.7) 39 | * [8、更新记录](#4.8) 40 | * [9、再次迭代:用宏来消除重复](#4.9) 41 | * [10、本章内容小结](#4.10) 42 | 43 | * [五、跨越初学者阶段](#5) 44 | * [1、其实说实话我也是初学者…](#5.1) 45 | * [2、HyperSpec:Common Lisp 有哪些“标准库函数”?](#5.2) 46 | * [3、如何查找各种具体实现(如SBCL、CCL、LispWorks)的帮助信息](5.3) 47 | * [4、更深一步的学习](#5.4) 48 | * [5、本章内容小结](#5.5) 49 | 50 | * [六、贡献者列表](#6) 51 | * [1、修订记录](#6.1) 52 | * [2、贡献者列表](#6.2) 53 | 54 | 55 | 56 | ## [一、简单介绍](id:1) 57 | 58 | 写作本文的缘起:我也是一名 Common Lisp 的初学者,在对照着各种教程学习 Common Lisp 的过程中,发现有不少细节都需要自己去摸索,比如对开发环境的进一步配置(推荐使用开发环境--LispBox 作为一个一键式开发环境,大大降低了许多不熟悉 Emacs 的初学者的学习和使用门槛,不过遗憾的是它已经停止更新了,现在 LispBox 中各软件的版本偏低,如果想要使用最新版本的 Common Lisp 实现,就需要自己去动手配置了),包括更新版本、支持中文符号名称、自定义函数名始终高亮显示等等,诸如此类很多细节,都需要自己摸索、尝试和验证,这个过程不可避免会花费一些时间。 59 | 60 | 我觉得只要有一个人经历过这种摸索就可以了,其他人完全可以借鉴他现成的经验,否则每个人都去做重复的摸索,是一种相当大的浪费,所以就不揣冒昧,把自己学习过程中的一些经验和体会记录下来,希望能为其他 Common Lisp 初学者节省一些时间。 61 | 62 | 学习任何知识,都不能仅仅把它们当做知识,更重要的是要把它们在实际编程实践中应用起来,持有这样的学习观念才不至于让你变成学究式的活字典,对于程序员来说这一点尤其重要,你学习的任何语言知识,一定要在实际的程序编写过程中不断练习、不停实践,纸上得来终觉浅,绝知此事须躬行。 63 | 64 | ### [1、本文目标](id:1.1) 65 | 66 | 写作本文的目标是希望能为 Common Lisp 初学者提供一份简单易懂、容易上手、学习结合(学为学,习为实践)的初学者教程。 67 | 68 | 【说明】:Lisp 家族相当庞大, Common Lisp 是其中的一个分支,具体的分类我就不在这里赘述 69 | 了,建议初学者可以到 wiki 百科去了解,当然,如果你和我一样,也在看冰河(田春)翻译的《实用 70 | Common Lisp 编程》这本书,那么直接阅读书中的前面章节也可以对此有一个大致的了解。 71 | 72 | 我一向认为,学习任何知识体系都要遵循从易到难、从简单到复杂的规律,就像 Lisp 的迭代式开发一样,最初出现在程序员手中的版本是一个很不完善,但实现了基本核心功能的原型系统,然后再经过反复的迭代开发来逐步完善,直到把粗糙的原型系统变成可用的工程系统。 73 | 74 | 学习一门复杂艰深、体系庞杂的程序语言--Common Lisp 同样要遵循这个规律,这份教程也尽量遵循这个规律来编写,也就是开始时不会涉及太深入的概念,只会提到一些基本概念,而这些基本概念也会以很直观易懂的描述方式来表述,不会用到任何可能令初学者疑惑难解的术语,目的就是让初学者对 Common Lisp 程序迅速建立一种感性认识跟理解,依据这些知识可以迅速读懂其他开源作者写的 Common Lisp 程序。 75 | 76 | 所以,本文的表述可能不是那么严谨,比如本文会有这样的表述: 77 | 78 | “Lisp 程序由 S-表达式(符号表达式)组成,S-表达式是列表或者单个原子” 79 | “列表是由 0 个或者更多的原子或者内部列表组成,原子或者列表之间由空格分隔开,并由括号括起来。 80 | 列表可以是空的” 81 | 82 | 初学者看到这个表述就会对 Lisp 的列表建立一种初步直观的印象,可以据此识别程序中使用的列表,也可以合法地构造出自己使用的列表,这样初学者就可以很迅速地入门了。 83 | 84 | 我不会在这里说什么 85 | 86 | “列表本质上是由 cons(点对)构成的,点对表示的列表是这个样子(OR NULL CONS),点对不仅可以构成列表,还可以构成树、集合和查询表” 87 | 88 | 虽然说这种表述更确切,但是我觉得这种表述明显会让初学者感到复杂和困惑,这些内容应该放在初学者入门之后继续深入学习的过程中逐步去了解的。 89 | 90 | 我发现之前看过的两本教程在开始章节部分都不约而同地采用了非常直观易懂的描述方式来讲解列表,而把 点对cons 的讲解放到了后续章节,看来大家都采取了同样的讲解策略。 91 | 92 | 因此,后续就不再一一详细解释了,本文提到的所谓的 Lisp 基本概念都是针对初学者的入门阶段的,等初学者真正入了门,再进一步深入学习时,同时初学者也对 Common Lisp 的实现机制有了更深入的了解时,自然会发现在这里了解到的基本概念会有更底层、更确切的表述,那就用那些更确切的表述来更新你脑海中这些入门阶段学到的直观易懂的基本概念吧。 93 | 94 | 当然,为了避免不必要的误解,我也会适当加一些说明,比如 Emacs Lisp 和 Common Lisp 的读取求值机制不太一样,Emacs Lisp 使用 Lisp 解释器进行读取和求值;而 Common Lisp 则使用 R-E-P-L 机制,要分为读取器(R)和求值器(E),读取器处理的是 S-表达式,而求值器处理的则是经过读取器处理输出的一些特殊的 S-表达式:Lisp形式--form。 95 | 96 | 正如《实用 Common Lisp 编程》的作者所说 **“难道在确定一门语言真正有用之前就要先把它所有的细节都学完吗?”**,尤其在面对 Common Lisp 这样一门体系异常庞大的语言时,初学者在开始阶段不可能也没必要深究它所有的细节,先学一点最简单的基础知识---而凭借这些基础知识又足够支撑你去写一些最简单的程序,然后让你的简单程序跑起来,这就是一个很好的开始了。 97 | 98 | ### [2、适用读者](id:1.2) 99 | 100 | 本文适用的读者群体就是 Common Lisp 的初学者,他们从来没有用 Lisp 写过程序,对于 Lisp 的分类一无所知,也不清楚用什么开发工具来运行 Common Lisp 程序,但是忽然对 Common Lisp 产生了兴趣,想学习一下,大致来说就是那些对于 Lisp 的认知停留在:“Lisp 是一门专门用于 AI 的学术性程序语言,不适合用来做商业开发” 这个程度的读者----这也是我在看《黑客与画家》之前对 Lisp 的认识。 101 | 102 | 我推荐的参考书,就是建议在学习过程中备在案头,可以随时查阅那种: 103 | 104 | 初学者阶段: 105 | 106 | 《实用 Common Lisp 编程》 --比较适合初学者的常备工具书,不仅有对 Common Lisp 的精彩讲解,更有非常实用、贴切的例程进行参考 107 | 108 | 《ANSI Common Lisp》中文版 --基本读物,可以作为额外的参考补充 109 | 110 | 《GNU Emacs Lisp 编程入门》 --此书专门讲 Emacs Lisp ,和 Common Lisp 具体细节不太一样,不过建议能对照着看看,会有意想不到的收获,尤其是其中的一些基本概念很接近; 111 | 112 | 《On Lisp 中文版》 --此书属于扩展阅读,有大量的代码实例,重点放在传授一种编程思想,主要探讨 Common Lisp 的函数和宏,建议读冰河翻译的中文版,因为纠正了原文的一些代码错误,初学者在了解一些基本概念之后就可以看懂这个了,推荐 113 | 114 | [《Google Common Lisp 风格》] [1] --该文档涉及的范围比较广,建议先大致浏览,学到哪里再细看哪里的相关章节 115 | 116 | [1]: http://gclsg.lisp.tw 117 | 118 | 119 | ### [3、迭代式学习](id:1.3) 120 | 121 | **迭代式学习**:本文尝试使用一种名为迭代式学习的方式进行内容,也就是说按照从前到后的写作顺序,最前面出现的都是非常基本的知识和操作,读者可以***边阅读、边理解、边实践***,可以迅速从最简单的部分入手,然后再以这部分比较简单的知识为基础,不断展开新的稍微难一点的内容,这部分的学习内容同样需要遵循 ***边阅读、边理解、边实践*** 的原则,等把这部分难度有所提升的内容掌握后,就到了更难一点的内容,继续按照 ***边阅读、边理解、边实践*** 的原则进行: 122 | 123 | 初级难度==》边阅读、边理解、边实践 124 | 二级难度==》边阅读、边理解、边实践 125 | 三级难度==》边阅读、边理解、边实践 126 | . . . . . . 127 | 超级难度==》边阅读、边理解、边实践 128 | 129 | 看到这里,有些对 Lisp 略有所知的朋友想必明白了,这不就是 Lisp 的迭代开发模式 **Read-Eval-Print-Loop :REPL** 的变形吗?哈,恭喜你看穿了,就是这样,经过一段时间使用 REPL 迭代方式的 Lisp 程序写作,我发现这种探索性的渐进式迭代开发方式非常适合用来从无到有、从简单到复杂构建一个全新的系统。 130 | 131 | 一种全新的知识体系也是这样一个未知的需要渐进探索的大系统,人类的认知过程应该遵循这种从易到难、知行合一(理论+实践)的渐进循环方式,这样每个学习阶段你都能感觉到进步,所有的反馈都是正面的,它既为你带来成就感,又能激励你接受难度渐增的挑战而期待更多的成就感,这种认知方式不会因为难度太高让你产生挫折感,进而丧失继续学习的兴趣,正所谓:学而时习之,不亦乐乎(我的理解是:学习了理论知识然后去实践中应用它,是多么有趣啊!)。 132 | 133 | 对于初学者而言,一定要多看、多想、多试,千万不要怕出错,事实上,现在错误就是下一轮迭代的起点,从另一个角度来说:如果你能把错误提示信息里的各类错误全部都尝试一遍,那你对这门语言也掌握得差不多了! 134 | 135 | 所以,初学者要要勇于思考、勇于尝试、勇于犯错! 136 | 137 | --- 138 | 139 | **【小提示】:** 140 | 141 | 用一个简单的文本文件把每次出错的信息记录下来,后面如果解决了就把解决方法也记录一下,养成这种学习习惯,你会受益匪浅。 142 | 143 | --- 144 | 145 | 在这里,对于初学者而言, Common Lisp 体系经过多年的发展,就是这样一种全新而复杂的知识体系,有很多内容相互关联,个人自学起来难以下手,想要掌握这个知识体系,最好的办法就是 REPL 迭代式学习,当然,这里的难度迭代式教程写作也是我个人的一种思考和探索,目前还没有得到什么实际验证,是否可行还不一定,不过我们可以一起在这里尝试一下,反正也没什么损失。:) 146 | 147 | 148 | ### [4、本章内容小结](id:1.4) 149 | 150 | * 写作本文的目标是希望能为 Common Lisp 初学者提供一份简单易懂,容易上手的初学者教程。 151 | * 本文适用的读者群体就是对 Lisp 了解很少但是希望尽快开始学习、实践一门 Lisp 语言的初学者。 152 | * 本文尝试的迭代式学习就是学习内容循序渐进,学习过程反复迭代的一种学习方法。 153 | 154 | 155 | ## [二、快速上手](id:2) 156 | 157 | 学习任何一门程序语言都不应该仅仅停留在理论阶段,更重要的是实践,有些概念可能看半天文字讲解都不得要领,但是一写成代码,到计算机上跑一遍就比什么讲解都清楚了。 158 | 159 | 因此对于一门语言的初学者而言,动手实践是非常重要的,但是不幸的事实是:配置开发环境是绝大多数初学者不得不面临的一个难题,要么是需要自己配置编译参数、自己编译版本,要么是需要定制各种配置文件,而且常常会有一些莫名其妙的错误提示,让初学者的第一个程序夭折,说老实话,这些知识点在初学者入门之后根本不算什么,都是常识,但是在没有入门之前,那就是天大的障碍,现在网络比较发达,很多类似问题都可以上网搜索,以前网络没这么发达的时候,初学者遇到这种问题那真是痛苦… 160 | 161 | 所以如果能有一个一键式的开发环境那是多么幸福的事情,这样初学者就可以迅速进入状态,避免无关的干扰,快速上手! 162 | 163 | 164 | ### [1、推荐开发环境 Lispbox ](id:2.1) 165 | 166 | 在这里,Lisp 初学者们有福了,有一个非常简单的一键式开发环境 LispBox 等着大家使用(虽然目前 LispBox 已经停止更新,不过托开源之福,我们可以自己更新版本),这个开发环境在各种主流平台都提供了对应的版本,目前支持: 167 | 168 | MS-Windows 169 | Mac OSX 170 | Linux 171 | 172 | 我使用了 MS-Windows 和 Mac OSX 下的版本,就目前使用情况来看,还是比较满意的,因此我会强烈推荐初学者使用这个开发环境,它不需要你做任何配置,把压缩包下载回来,解压后直接双击可执行文件就可以运行,没有任何障碍。 173 | 174 | LispBox 下载地址: 175 | 176 | http://gigamonkeys.com/lispbox/ 177 | 这个地址是《实用 Common Lisp 编程》的作者提供的,包括了写给即将学习 Lisp 的新手们的一段话,大家可以看看。 178 | 179 | http://common-lisp.net/project/lispbox/ 180 | 这里是 LispBox 的正式下载地址 181 | 182 | 183 | ### [2、开发环境简要介绍](id:2.2) 184 | 185 | LispBox 实际上是把 Emacs、Slime、Clozure CL 以及 QuickLisp 集成到一起,关于 LispBox 更详细具体的介绍可以参考我以前写的文章: 186 | 就不再这里重复了。 187 | 188 | 等初学者对 LispBox 熟悉一些后,就可以自己修改配置来使用其他 Common Lisp 实现了,比如加入 SBCL 189 | 190 | #### 扩展阅读 191 | 192 | 使用 LispBox 做开发环境就相当于选择了 Emacs 作为编辑器、选择 Slime 作为交互界面,那么一定要熟悉 Emacs 和 Slime 的各种快捷键,这不仅会让你的学习开发过程事半功倍,更让你有一种高效率、不间断键盘作业的享受。 193 | 194 | 建议参考读物: 195 | 196 | 《GNU Emacs Lisp 编程入门》 -- 让你了解 Emacs 工作的机制,明白那些插件是怎么工作的 197 | 198 | [《Slime 用户手册》] [2] -- 建议看帝归翻译的中文版,省时省力,全面介绍了 Slime 的快捷键 199 | 200 | [2]: https://github.com/JuanitoFatas/slime-user-manual/blob/master/README.md 201 | 202 | 203 | ### [3、第一个简单的 Lisp 程序](id:2.3) 204 | 205 | 开发环境启动后会进入一个 REPL 界面,我们可以直接在 CL-USER> 后面输入 Lisp 代码,然后敲击回车运行代码 206 | 207 | ; SLIME 2012-11-12 208 | CL-USER> 209 | 210 | 第一个程序就沿用传统,向世界打个招呼吧: 211 | 212 | CL-USER> (print "hello,world!") 213 | 214 | "hello,world!" 215 | "hello,world!" 216 | CL-USER> (format t "你好, 世界!") 217 | 你好, 世界! 218 | NIL 219 | CL-USER> 220 | 221 | 这里其实用了两个函数,一个是 **print** 函数,一个是 **format** 函数,都是输出内容到屏幕。 222 | 223 | 不过在 Common Lisp 中更常用 **format** 函数来输出到屏幕多一些,可以把它跟 C 语言的 **printf** 函数对照着来看,注意一下 **format** 中的那个参数 “**t**”,代表的是标准输出流:**\*standard-output\*** ,也就是说如果在 **t** 的位置换一个参数,我们也可以把这段问候语发送到任何一个指定的输出流上。 224 | 225 | (format t "你好, 世界!") 226 | 227 | 这个结构就是一个列表,用括号包围,里面共有 3 个元素,这些元素用空格分隔,不过双引号里的空格作为字符串内容处理,不起分隔作用,可以很明显地看出,**format** 属于比较特殊的符号,它就是一个函数名,后面的两个元素都是它的参数。 228 | 229 | OK,是不是很简单,就跟 Lisp 世界发出了第一声问好! 230 | 231 | (为什么 **print** 输出了两遍呢?说实话我也不清楚,要不自己去查查资料,然后把答案反馈给我 :) ) 232 | 233 | ### [4、为程序增加些复杂性](id:2.4) 234 | 235 | 有朋友说了,这个程序太简单了,而且如果我想重复问好怎么办?难道每都把这段代码拷贝、粘贴吗?那好,让我们把这段代码写成一个函数 **hi** ,这样,每次问好时只需要输入 (**hi**) 就可以了。 236 | 237 | Common Lisp 程序中直接调用函数时一般要用括号把函数名括起来,比如 (hi) 238 | 239 | 我们就直接在 REPL 界面来编辑刚才输入的内容,可是刚才已经执行过这段代码了,现在的 CL-USER> 提示符后面是空的,有朋友说:我就是不喜欢来回拷贝,希望能有一个快捷键来列出我输入过的历史命令,没问题。 240 | 241 | Emacs 中查询历史命令的快捷键是 M-p ,这里的大写 M 表示 Alt 键,M-p 就是同时按下 Alt 键 和 p 键 242 | M-p 是向上翻 243 | M-n 是向下翻 244 | 245 | 这样就可以把你在 REPL 中输入过的历史命令一一查看了,言归正传,历史命令找回来了,可是光标跑到最后了,我们需要把光标移动到最前面,我明白,你不想操作鼠标移动光标,希望有移动光标的快捷键,没问题: 246 | 247 | C-a 是把光标移动到行首的快捷键,这里大写的 C 表示 Ctrl 键,C-a 就是同时按下 Ctrl 键 和 a 键 248 | C-e 是把光标移动到行尾的快捷键 249 | 250 | 恩,看来 Emacs 的键盘快捷键操作起来果然很流畅,那我们继续把代码修改为函数: 251 | 252 | CL-USER> (defun hi () 253 | (format t "你好,世界!")) 254 | HI 255 | CL-USER> (hi) 256 | 你好,世界! 257 | NIL 258 | 259 | 这里涉及到自定义函数的知识点,我假设大家都学过 C 语言,那么我们可以猜测一下 defun 的语法结构: 260 | 261 | 首先是括号,然后是 **defun** ,表示开始定义函数,再后面是 **hi** ,是我们自定义的函数名称,后面的空括号应该是参数吧,不过因为我们这一段程序没有使用参数,所以是空的,接着是函数体,也就是这个函数具体执行的操作,这个函数体要用括号括起来,最后再用一个括号和最前面的括号对应,把所有的内容括起来。 262 | 这里我们发现 REPL 在遇到回车换行时,它不会按行处理,而是按括号来处理,所以你可以增加任意个回车换行,只要没有输入跟第一个左括号匹配的右括号,它都不会认为你的输入结束,只有当你所有的左括号都有一个对应的右括号来匹配时,REPL 才会认为你输入的内容结束了。 263 | 264 | 这里给一个 **defun** 函数定义的标准语法形式吧: 265 | 266 | (defun name (parameter*) 267 | "可选的函数描述" 268 | body-form*) 269 | 270 | parameter* 表示 0 个或者多个 parameter,这里的 * 是正则式语法符号,表示 0 个或多个 271 | body-form* 表示 0个或多个 body-form 272 | 273 | 对应中文就是这样: 274 | 275 | (defun 函数名 (参数*) 276 | "可选的函数描述" 277 | 形式体*) 278 | 279 | 这里解释一下 **body-form** 这个概念,Common Lisp 定义了两个语法黑箱,前者叫**读取器**,将文本转化为 Lisp 对象,后者叫**求值器**,它用前者定义好的 Lisp 对象来实现语言的语义,我们知道直接输入到**读取器**中的 Lisp 程序是由 S-表达式组成的,而**求值器**则定义了一种构建在 S-表达式(符号表达式)之上的 **Lisp形式--form** 的语法。 280 | 281 | 所有的字符序列都是合法的 S-表达式 282 | 283 | 这一点意味着你可以把任意的字符序列交给 Lisp 的读取器来处理,你可以定义任意的语法格式来作为你的程序的文本输入---当然,这需要你做一些相关的设置,不过我们还是建议初学者先了解、熟悉大家都习惯的 Lisp 语法形式,等你真正学会了,就可以创造自己的程序语言了,是不是听起来很鼓舞斗志? 284 | 285 | 但是并非所有的 S-表达式 都是合法的 Lisp形式--form 286 | 287 | 举个例子就清楚了,(hi) 和 ("hi") 都是合法的 S-表达式,但是 (hi) 是一个合法的 Lisp形式--form,而 ("hi") 就不是一个合法的 Lisp形式--form,因为字符串作为列表的第一个元素对于 Lisp形式--form 而言是没有意义的。 288 | 289 | 说了这么多,其实主要是讨论什么才是函数定义中的 body-form(形式体)的合法形式,想搞清这个问题,又不愿意多想的话,就到环境上去试验吧,把你能想到的各种形式都试验一下。:) 290 | 291 | --- 292 | 293 | **留个小作业:** 294 | 295 | 如果函数定义中有多个形式体,应该如何去写?建议大家自己上机试验。 296 | 297 | --- 298 | 299 | * 函数调用的特殊情况 300 | 301 | 既然有一般情况,那就有特殊情况,另一种对函数的调用方式是间接调用,就是把函数1作为参数传递给另一个函数,由另一个函数间接调用函数1,具体到我们这个例子就是用另一个函数间接调用 **hi**,接下来就介绍这两个函数:**funcall** 和 **apply**,它们需要使用这种形式: 302 | 303 | CL-USER> (funcall #'hi) 304 | 你好,世界! 305 | NIL 306 | CL-USER> (apply #'hi nil) 307 | 你好,世界! 308 | NIL 309 | CL-USER> 310 | 311 | 感兴趣的朋友可以试着分析一下 **funcall** 和 **apply** 的区别,另外也可以试着执行一下下面这两种形式,看看会有什么错误提示,从这些错误提示信息也可以了解一些 Common Lisp 的内部处理机制。 312 | 313 | 犯错尝试: 314 | 错误1: (apply #'hi) 315 | 错误2: (funcall #'(hi)) 316 | 错误3: (apply #'(hi)) 317 | 318 | 上面我们提到一个组合符号 **#'** ,它由两个符号组成,前面是井号 **#** ,紧跟着是单引号 **'** ,这个组合符号 **#'** 等价于函数 **function**,前者是后者的“语法糖”,不过两者的使用形式有些区别: 319 | 320 | CL-USER> (funcall (function hi)) 321 | 你好,世界! 322 | NIL 323 | CL-USER> 324 | 325 | 326 | --- 327 | 328 | **【小提示】:** 329 | 330 | 在这些不起眼的处理细节上多想想区别,多试试错误,多看看错误提示信息,有助于我们更好的理解 Common Lisp 的内部处理机制。 331 | 332 | --- 333 | 334 | 关于 funcall 和 apply 更具体的应用场景就不在这里详述了,建议大家阅读《实用 Common Lisp 编程》和 《ANSI Common Lisp》中的相关章节来做更深入的了解。 335 | 336 | 337 | 338 | ### [5、这么好的程序一定要保存起来](id:2.5) 339 | 340 | 哈,经过这么一番学习,终于完成了我们的第一个函数,于是有人就说:这么好的函数能不能保存起来,免得下次想调用它还得重新输入这么多字符,没问题: 341 | 342 | 可以先拷贝程序文本,这里说一下 Emacs 下的拷贝粘贴快捷键: 343 | 344 | Mac 系统 345 | 拷贝是 Command 键 和 c 键同时按下 346 | 粘贴是 C-y : Ctrl 键 和 y 键 同时按下 347 | 是不是感觉有些奇怪,没关系,如果不适应的话可以自己修改配置文件,或者修改 slime.el 文件来重新定义 348 | 349 | MS-Windows 系统 350 | 拷贝是 M-w :Alt 键 和 w 键 同时按下 351 | 粘贴是 C-y :Ctrl 键 和 y 键 同时按下 352 | 353 | 然后创建新文件: 354 | 355 | 使用如下快捷键 356 | C-x C-f 就是先同时按下 Ctrl 键 和 x 键,然后全部松开,再同时按下 Ctrl 键 和 f 键,再松开,Emacs 屏幕底部会显示如下: 357 | Find file: ~/ 358 | 默认保存在当前用户目录下,Mac系统是 /Usrs/admin/ 359 | 360 | 你输入要保存具体要保存的目录,我的文件保存在 ~/ECode/Markdown-doc/hi.lisp 361 | 362 | 可以使用 TAB 键来自动补全,这样就不必一个个手工输入了 363 | 364 | 我输入的文件路径和名称如下: 365 | 366 | Find file: ~/ECode/Markdown-doc/hi.lisp 367 | 368 | 注意文件名后缀要保存为 .lisp 代表这个文件是 Common Lisp 程序。 369 | 370 | Emacs 也有一种用来定制编辑器的 Lisp 语言,叫做 Emacs Lisp,这种文件的后缀是 .el 或 .emacs 371 | 372 | OK,输入上述这些之后,回车,Emacs 就会创建一个名为 hi.lisp 的 Lisp 源程序文件,放在 ~/usrs/admin/Ecode/Markdown-doc/ 目录下。 373 | 374 | 注意,这时这个文件还是一个空文件,把我们之前拷贝好的程序内容,粘贴到这个新建的空文件里。 375 | 376 | 然后就是执行文件保存的快捷命令了: 377 | 378 | Mac 系统 379 | C-x C-s 380 | 或者 Command-s 381 | 382 | MS-Windows 系统 383 | 384 | 385 | 很好,到现在为止,你已经成功地写出了第一个程序,并且对这个程序做了一些扩展,然后又成功地把它保存了起来,那么接下来就要提到如何加载它了,我们可以使用 load 函数来进行加载。 386 | 387 | 这时又有朋友发现了,我们刚才使用的 REPL 界面不见了,被新开启的 hi.lisp 的文本编辑界面所取代了,我想继续回到刚才那个 REPL 界面该怎么办?有多种快捷方法可以调出刚才的 REPL 界面,我们先说一种最适合一边在文本编辑界面写代码,一边用 REPL 来调试的的调用方法,快捷键如下: 388 | 389 | C-c C-z 可以直接调出一个关联到当前文本编辑界面的 REPL 窗口 390 | 391 | 为什么说特别适合调试呢?比如,你在文本编辑区写了一段函数代码,想立刻看看这段代码的执行情况,那你可以把光标放在这个函数代码段内的任意一个位置,然后输入快捷键: 392 | 393 | C-c C-y 把光标所在区域的函数名称发送到对应的 REPL 进程中,非常方便调试代码 394 | 395 | 这个函数名称就自动跑到 REPL 去了,是这个样子: 396 | 397 | CL-USER> (hi ) 398 | 399 | 看看连括号都没拉下,而且函数名后面还自动加了个空格预防你一旦需要有参数输入,然后直接回车就可以在 REPL 中调试你刚写好的函数了,是不是很方便? 400 | 401 | 好了,函数在 REPL 中调试过了,你也看到了执行效果,觉得还需要再加点什么,于是又想切换回到文本编辑缓冲区了,那么快捷键如下: 402 | 403 | C-x o 先同时按下 Ctrl 键 和 x 键,松开,再按下 o 键 404 | 405 | 这样就又切换回刚才的文本编辑缓冲区了。 406 | 407 | 这里我开始使用缓冲区(buffer)这个名词,缓冲区是 Emacs 编辑器的一个概念,文本编辑窗口是一个缓冲区,REPL 是一个缓冲区,消息事件也是一个缓冲区,不同的缓冲区可以来回切换,缓冲区的屏幕布局也可以通过快捷键来设置: 408 | 409 | C-x 1 当前缓冲区占据整个 Emacs 窗口,其他缓冲区全部放到后台; 410 | C-x 0 关闭当前缓冲区 411 | C-x 2 在当前缓冲区上方新打开一个缓冲区 412 | C-x 3 在当前缓冲区右方新打开一个缓冲区 413 | 414 | 最后再介绍一个超级有用的快捷键:查看标准函数、标准宏源代码,我们知道 Common Lisp 的很多实现都是开源的,包括我们推荐使用的 CCL,这就意味着我们可以查看其源代码,既便于深入理解,也便于学习模仿,比如对于自定义函数的宏 **defun** ,我们想查看它的源代码,想了解它的具体实现细节,可以把光标放在 **defun** 上,然后按 M-. 415 | 416 | M-. 同时按 Alt 键 和 点键 . ,查看当前光标所在位置的函数的源代码 417 | 418 | 大家可能注意到我介绍了不少的 Emacs 快捷命令,这正是我想要大力推荐的,就我的使用经验而言,这些快捷操作能够极大地提升 Emacs 开发环境下编程、调试的效率,所以希望初学者能熟悉这些快捷操作,其实多用几次就熟悉了,慢慢就习惯了,不知不觉工作效率就提高了,可以早点完成工作了,可以早点下班回家了,于是有了更多的自由时间,可以多看看书、多运动运动、多陪陪家人、多发展下个人的兴趣爱好……然后你的整个人生就改变了 :) 419 | 420 | 美好的未来真值得期待啊,现在让我们言归正传,继续讨论如何加载 Lisp 源程序,有多种方式可以加载,我们先介绍在 REPL 界面的方式: 421 | 422 | CL-USER> (load "~/ecode/markdown-doc/hi.lisp") 423 | #P"/Users/admin/ECode/Markdown-doc/hi.lisp" 424 | CL-USER> (hi) 425 | 你好,世界! 426 | NIL 427 | CL-USER> 428 | 429 | [说明 #P"/Users/admin/ECode/Markdown-doc/hi.lisp" 这种返回形式表示返回结果是一个路径对象] 430 | 431 | 悲剧了,精心编写的问候语成了一堆乱码,是什么原因?该怎么办呢?原因也简单,所有的文件操作函数(比如 load open等)默认的字符编码格式类型都是 **:latin-1** ,这种编解码类型对应英文字符,遇到中文内容自然就乱码了。 432 | 433 | 没错,你没看错,我也没写错,这个类型名称就是以冒号打头的一个符号,这种类型的符号在 Lisp 中被称为关键字 keyword ,对这种类型的符号求值会得到冒号后面的符号名称,从现在开始初学者要逐步适应 Lisp 对符号的使用习惯。 434 | 435 | 而我们的中文使用的编码恰恰不是这个,而是 **:utf-8** ,那么就需要手工指定了,如下: 436 | 437 | CL-USER> (load "~/ecode/markdown-doc/hi.lisp" :external-format :utf-8) 438 | #P"/Users/admin/ECode/Markdown-doc/hi.lisp" 439 | CL-USER> (hi) 440 | 你好,世界! 441 | NIL 442 | CL-USER> 443 | 444 | 好了,问题解决了,可是有些朋友觉得很麻烦,每次加载文件都得输入一长串额外的参数,这里介绍一个稍微简单点的办法,可以通过修改系统的全局变量 \*default-external-format\* 来把默认的文件格式改成我们需要的 **:utf-8** ,先查看一下当前的值,如下: 445 | 446 | CL-USER> *default-external-format* 447 | :UNIX 448 | CL-USER> 449 | 450 | 用 **setq** 函数修改,第一个参数是要修改的全局变量,第二个参数是希望修改成的值,修改然后查看: 451 | 452 | CL-USER> (setq *default-external-format* :utf-8) 453 | :UTF-8 454 | CL-USER> *default-external-format* 455 | :UTF-8 456 | CL-USER> 457 | 458 | `说明:全局变量 *default-external-format* 在 CCL 和 CLisp 中可以用,但是在 SBCL 中不支持,因此如果你的编程环境是 SBCL 的话,那么想要支持中文就需要每次手动指定编码格式了---SBCL 是否有类似的全局变量?我不太清楚,知道的朋友可以指点一下。` 459 | 460 | 之所以介绍使用 **setq** 函数,是因为这个函数在 Common Lisp 和 Emacs Lisp 中都可以使用,都可以用于赋值,也就是说你可以在 Emacs 的配置文件中使用 **setq** 这个函数来修改一些全局配置量,而且 **setq** 在 Common Lisp 中更是一个特殊操作符,据说现代风格一般使用宏 **setf** 来实现赋值功能,**setf** 封装了对 **setq** 的调用。更详细的使用方法可以查询 HyperSpec 。 461 | 462 | 好了,再试一下,看看效果: 463 | 464 | CL-USER> (load "~/ecode/markdown-doc/hi.lisp") 465 | #P"/Users/admin/ECode/Markdown-doc/hi.lisp" 466 | CL-USER> (hi) 467 | 你好,世界! 468 | NIL 469 | CL-USER> (setq *default-external-format* :utf-8) 470 | :UTF-8 471 | CL-USER> (load "~/ecode/markdown-doc/hi.lisp") 472 | #P"/Users/admin/ECode/Markdown-doc/hi.lisp" 473 | CL-USER> (hi) 474 | 你好,世界! 475 | NIL 476 | CL-USER> 477 | 478 | 修改生效!击掌庆祝一下! 479 | 480 | 赋值语句 **setq** 的各种例子: 481 | 482 | CL-USER> (setq a (print "hello world!")) 483 | 484 | "hello world!" 485 | "hello world!" 486 | CL-USER> (setq a '(hello world!)) 487 | (HELLO WORLD!) 488 | CL-USER> (setq b (print "hello world!")) 489 | 490 | "hello world!" 491 | "hello world!" 492 | CL-USER> b 493 | "hello world!" 494 | CL-USER> (setq c '()) 495 | NIL 496 | CL-USER> c 497 | NIL 498 | 499 | CL-USER> (setq c ()) 500 | NIL 501 | CL-USER> c 502 | NIL 503 | CL-USER> (setq c ( )) 504 | NIL 505 | CL-USER> c 506 | NIL 507 | CL-USER> (setq c ( )) 508 | NIL 509 | CL-USER> c 510 | NIL 511 | CL-USER> (setq c ( nil )) 512 | ; Evaluation aborted on #. 513 | CL-USER> (setq c (nil)) 514 | ; Evaluation aborted on #. 515 | CL-USER> (setq c '(nil)) 516 | (NIL) 517 | CL-USER> c 518 | (NIL) 519 | CL-USER> (setq c (nil)) 520 | ; Evaluation aborted on #. 521 | CL-USER> 522 | 523 | 524 | 【补充说明】事实上字符编解码会涉及一系列知识点,我这里只是大致提一下,只简单介绍部分用法,不做详细讲解,感兴趣的朋友可以自己搜索相关资料,好好看看,把这些弄懂了基本上就清楚在各种不同场景下程序支持中文的机制了。 525 | 526 | ### [6、补充阅读:让程序支持中文符号](id:2.6) 527 | 528 | #### 中文符号使用场景 529 | 530 | 写到这里,就自然而然地涉及到了在程序中使用中文符号的话题,就我的理解,在程序中对中文符号的使用可分为如下场景: 531 | 532 | * **中文符号作为字符串来使用** 533 | 534 | 这个是最常见的一种使用方式,也是最容易实现的一种方式,我们前面的问好程序 hi 就是把中文当字符串使用的 535 | 536 | * **中文符号作为自定义变量名称、自定义函数名称、自定义宏名称来使用** 537 | 538 | 这个需要对开发环境做一些配置,因为你的 Lisp 的读取器也好,求值器也好,都需要专门指定编解码来识别双字节的中文,而且使用 Slime 这种交互接口,还涉及一个客户端和服务端通信的编解码,我们接下来也主要讲解在这种方式下使用中文符号需要进行的配置工作 539 | 540 | * **中文符号作为语法关键字来使用** 541 | 542 | 这种场景下连 “**if**” 这样的关键字都可以写成中文 “**如果**” 了,这样使用中文符号的结果是:源程序完全由中文、数字和其他符号组成,也就是说你可以自由地使用中文进行编程,需要编译器识别这些关键字,因此这种使用方式需要做更进一步的配置,幸运的是 Lisp 可以通过一些简单的设置完美地支持。 543 | 544 | 我正在投入的一个开源项目:【开源母语编程】 就是希望能在这方面做一些开拓性的工作,提供一个试验性质的平台,可以让对此感兴趣的开发者以此项目为基础继续深入研究。 545 | 546 | #### 第二种场景的配置方式 547 | 548 | 需要修改 Emacs 的配置文件,在 LispBox 环境中是通过 lispbox.el 文件进行配置的,在该文件中增加如下内容: 549 | 550 | (set-language-environment "utf-8") 551 | (set-buffer-file-coding-system 'utf-8) 552 | (set-terminal-coding-system 'utf-8) 553 | (set-keyboard-coding-system 'utf-8) 554 | (set-selection-coding-system 'utf-8) 555 | (set-default-coding-systems 'utf-8) 556 | (set-clipboard-coding-system 'utf-8) 557 | 558 | (setq ansi-color-for-comint-mode t) 559 | (setq-default pathname-coding-system 'utf-8) 560 | (setq default-process-coding-system '(utf-8 . utf-8)) 561 | (setq locale-coding-system 'utf-8) 562 | (setq file-name-coding-system 'utf-8) 563 | (setq default-buffer-file-coding-system 'utf-8) 564 | (setq slime-net-coding-system 'utf-8-unix) 565 | 566 | (modify-coding-system-alist 'process "*" 'utf-8) 567 | (prefer-coding-system 'utf-8) 568 | 569 | 说实话,除了少数几条配置的作用比较明确,我也不是很确定上述这些配置具体哪条会起什么作用,大家就根据名称自己顾名思义一下吧,当然如果有人能够详细整理一下每条的确切作用共享出来,那就最好不过了。 :) 570 | 571 | 修改完成后,重启 LispBox,你的开发环境就支持使用中文字符做变量名和函数名了===没错,我的环境就是这样设置的,现在我们再把刚才那个问好函数修改完善一下,增加下面这个函数: 572 | 573 | (defun 你好 () 574 | (format t "你好,世界!--我使用了中文函数名称!")) 575 | 576 | 然后使用 C-c C-k 在文本编辑缓冲区完成编译,再使用 C-c C-y 把函数名称发送到 REPL 进程上的输入区域,回车,全部显示如下: 577 | 578 | CL-USER> 579 | ;Compiling "/Users/admin/ECode/Markdown-doc/hi.lisp" 580 | CL-USER> (你好 ) 581 | 你好,世界!--我使用了中文函数名称! 582 | NIL 583 | CL-USER> 584 | 585 | 哈,恭喜你,终于可以使用中文来自定义函数名称了,话说我实在是受够那些无比冗长的英文函数名称了。 586 | 587 | **英文是一种一维的线性文字,适合听觉处理而不适合视觉处理,想表达清楚一个含义,必须使用长长的一串字母组合,而中文的优势就是它是一种非线性的二维文字,相对来说更适合视觉处理(其实中文也适合听觉处理,虽然有不少同音字,但是根据上下文可以清晰确定其确切含义)。** 588 | 589 | **一般来说:2个中文字符占据4个字节,4个字节对应4个英文字母,4个英文字母所能表述的内容跟2个中文字符所能表述的内容一对照就逊色很多了,在不影响清晰表达的前提下,用中文来做函数名可以比英文缩短一半以上。** 590 | 591 | 当然也有其他类型的使用不同字节的中文编码,还有可变字节的编码,这些就不一一细述了,感兴趣的朋友可以自行查阅相关资料。 592 | 593 | 正是基于上述原因,我建议写代码时更多使用中文字符,这样可以有效缩短你源代码文件的长度,而且你的项目越大,这种效果越明显,越有利于节能减排。 :) 594 | 595 | 【注意】: 596 | ***切记!千万不要在中文输入状态下输入各种符号,比如汉字的圆括号--》 () ,如果在程序中误用了汉字符号,也就是全角符号,会产生编译错误!!因为半角符号和全角符号对应的内部编码是不同的!!因此 Common Lisp 程序代码中所有的中文字符以外的其他标点符号必须使用英文方式输入,也就是要使用半角符号,而不是全角符号。*** 597 | 598 | 这里的中文字符以外的符号指的是半角括号、逗号、单引号、反引号、双引号等符号,使用本教程的 **.emacs** 配置文件,会自动把中文符号和英文符号设置为不同的颜色: 599 | 600 | 半角的英文符号一律为蓝色 601 | 全角的中文符号一律为绿色 602 | 603 | 可以来这里下载 Emacs 配置文件: 604 | 605 | https://github.com/FreeBlues/PwML/blob/master/.emacs 606 | 607 | 608 | ### [7、本章内容小结](id:2.7) 609 | 610 | * 初学者最好使用一键式开发环境,可以节省很多不必要的投入,推荐 LispBox 611 | * 各种 Emacs 快捷键使用技巧可以大幅提升你的工作效率 612 | * 几种特殊键的代表符号: 613 | * C Ctrl 键 614 | * M Meta 键(也就是 Alt 键) 615 | * Command Command 键 ( Mac 机型的特殊键) 616 | * 交互编程最重要的几个快捷命令: 617 | * C-c C-z 从代码编辑区切换到 REPL 区; 618 | * C-c C-y 把正在编写的函数名称发送到 REPL 区进行调试; 619 | * C-x o 从 REPL 区 切换到代码编辑区; 620 | * M-p 在 REPL 区查找历史命令,向前翻页 621 | * M-n 在 REPL 区查找历史命令,向后翻页 622 | * M-. 同时按 Alt 键 和 点键 . ,查看当前光标所在位置的函数的源代码 623 | 624 | * 忽然发现这章的小结写不下去了,因为内容比较散,就留给感兴趣的读者自己去做小结,写完了可以发给我,我们共同署名更新 :) 625 | 626 | 627 | ## [三、适用于初学者的 Lisp 基本概念](id:3) 628 | 629 | 本章内容主要是对 Common Lisp 的程序结构和语法形式做一个直观易懂的描述,希望能在初学者脑海里迅速建立一些关于 Common Lisp 程序的初步观念,帮助初学者对于 Common Lisp 这门程序语言尽快有一个粗略的印象。 630 | 631 | 在建立这种粗略印象后,再结合前一章讲解过的在开发环境上调试程序的实践知识,初学者就拥有了理论 + 实践的双重工具(当然是尚不完善的理论),具备了继续深入学习的基础,能够以最小障碍跨越 Common Lisp 入门阶段,有了这个基础,初学者就可以真正自由地去探索 Common Lisp 那复杂博大的体系(确实够复杂,”标准函数“就有978个,再加上各种不同实现的一些函数就更多了)。 632 | 633 | 当初学者学到这种程度,再回过头来看这些入门阶段的基本概念,可能觉得它们的表述不够确切、不够严谨----看来时机已经成熟,新世界即将在你面前打开: 634 | 635 | 欢迎进入新世界,尼欧! 636 | 637 | 既然你已经能够从更深层次来对 Common Lisp 进行理解和表述,那就把这些入门阶段的基本概念都刷新一遍吧! 638 | (忽然发现,“尼欧” 的中文拼音 ”niou“ 在搜狗输入法中会对应到汉字“牛”,哈,估计之前没有人发现过这个神秘巧合吧!--“牛”字正规的汉语拼音应该是 “niu”) 639 | 640 | 就像大多数人最初学习数学时,数学老师会告诉你:对负数开平方是非法的,但是一旦学到复数的层次,负数也可以开平方了,而且还有特别的含义--旋转量,处于不同阶段对于知识的理解自然也不尽相同,这是一种螺旋式上升(迭代开发的另一种表述),在哲学上这就是否定之否定。 641 | 642 | 【补充一句:】 643 | 644 | 初学者首次阅读本章时,本章内容能看懂多少算多少,不必过于深究,其实有个大概的印象就可以了,然后就可以直接开始第四章的学习+实践,在实践的过程中可能有些概念就自然而然地理解了。毕 645 | 646 | 647 | ### [1、Lisp 程序由 S-表达式组成,表达式是列表或单个原子](id:3.1) 648 | 649 | 基本概念:Common Lisp 程序在语言处理器的不同处理阶段有不同的形式体现,在读取器中识别的是 S-表达式,在求值器中识别的是 Lisp形式--form ,S-表达式的基本元素是列表(list)和原子(atom)。关于 S-表达式 和 Lisp形式 form 的区别之前我们稍微讨论过一些。 650 | 651 | 对于初学者来说,Common Lisp 程序就是用无数括号括起来的各种符号表达式,括号里的 S-表达式 可以有这么几种组合:纯粹的原子,纯粹的列表,列表和原子,如下: 652 | 653 | (原子1 原子2 原子3) 654 | (列表1 列表2 列表3) 655 | (原子1 列表1 原子2 原子3 列表2) 656 | 657 | 但是前面也说了,并非所有的 **S-表达式** 都是合法的 **Lisp形式--form** ,这里我们似乎可以给 **Lisp形式--form** 下一个简单的定义: 658 | 659 | 能够在 REPL 求值器里正常求值的 S-表达式 才是合法的 Lisp形式--form 660 | 661 | 有了这个定义,我们就可以直接在 REPL 中试验了,把你想要验证的 **S-表达式** 输入到 REPL 中,然后回车 662 | 663 | 理论上说,lisp程序形式可以由任何符号形式组成,不过对于初学者而言,暂时还没必要深究这些,就老老实实地使用括号语法吧。 664 | 665 | 关于 Lisp 括号的笑话:话说一名黑客冒死窃取到美国核弹控制程序的最后一页,打开一看,满满一页右括号。。。 666 | 667 | :) 668 | 669 | ### [2、Lisp 中的列表是什么样的?](id:3.2) 670 | 671 | 《GNU Emacs Lisp 编程入门》中是这么说的: 672 | 673 | “列表由 0 个或者更多的原子或者内部列表组成,原子或者列表之间由空格分隔开,并由括号括起来。列表可以是空的”。 674 | 675 | 《实用 Common Lisp 编程》中是这么说的: 676 | 677 | “S-表达式的基本元素是列表和原子。列表由括号所包围,并可包含任何数量的由空格所分隔的元素。列表元素本身也可以是 678 | S-表达式--也就是原子嵌套的列表” 679 | 680 | “任何原子(非列表或空列表)都是一个合法的 Lisp形式” 681 | 682 | 这就是列表的句法规则(syntax) 683 | 684 | 685 | ### [3、Lisp 中的原子又是什么样的?](id:3.3) 686 | 687 | 《GNU Emacs Lisp 编程入门》中是这么说的: 688 | 689 | 原子是多字符的符号(如 forward-paragraph)、单字符符号(如 + 号)、双引号之间的字符串、或者数字。 690 | 691 | 这里补充一下,Emacs Lisp 和 Common Lisp 的原子概念有所不同,Common Lisp 中有一个名为 **atom** 的函数,可以用来判断是否原子,使用方式如下: 692 | 693 | CL-USER> (atom 'sss) 694 | T 695 | CL-USER> (atom (cons 1 2)) 696 | NIL 697 | CL-USER> (atom nil) 698 | T 699 | CL-USER> (atom '()) 700 | T 701 | CL-USER> (atom 3) 702 | T 703 | CL-USER> (atom +) 704 | NIL 705 | CL-USER> (atom "qwert qwer") 706 | T 707 | CL-USER> (atom -) 708 | NIL 709 | CL-USER> 710 | 711 | 实际试验一下就会发现,在 Common Lisp 中,单字符符号,如 + 号,是不被判断为原子类型的。 712 | 713 | (atom object) 等价于 (typep object 'atom) 等价于 714 | (not (consp object)) 等价于 715 | (not (typep object 'cons)) 等价于 716 | (typep object '(not cons)) 717 | 718 | 说明:(typep object 'atom) 这条语句的含义是: object 是否为类型 atom,typep 就是一个关于类型的谓词判断函数,因为 **atom** 既是一个 **函数**,又是一种 **类型** ,在这条语句中 **atom** 作为 **类型** 来使用。 719 | 720 | 721 | ### [4、Lisp 中求值的概念:对数字、符号、字符串和列表求值](id:3.4) 722 | 723 | 前面一再提到“求值”,那么什么是求值?在这一点上 Emacs Lisp 和 Common Lisp 的差异较大,前者相对简单,使用解释方式,后者相对复杂,既可以使用解释方式,也可以采用编译方式,很多实现都采用编译方式。 724 | 725 | 《Emacs Lisp 编程入门》中的描述如下: 726 | 727 | 当 Lisp 解释器处理一个表达式时,这个动作被称作“求值”。我们称,解释器计算表达式的值。 728 | 对数字求值就是它本身 729 | 对双引号之间的字符串求值也是其本身 730 | 当对一个符号求值时,将返回它的值 731 | 当对一个列表求值时,lisp解释器查看列表中的第一个符号以及绑定在其上的函数定义。然后这个函数定义中的指令被执行。(这里指的是列表中第一个符号是一个函数的场景) 732 | 733 | 734 | 《实用 Common Lisp 编程》中给出一种便于理解讨论的描述如下 : 735 | 736 | 为了便于讨论,你可以将求值器想象成一个函数,它接受一个句法良好定义的 Lisp形式 作为参数并返回一个值,我们称之为这个形式的值。当然,当求值器是一个编译器时,情况会更加简化一些----在那种情况下,求值器被给定一个表达式,然后生成在其运行时可以计算出相应值的代码。 737 | 738 | 原子可分为符号和所有其他类型,符号在作为 Lisp形式 被求值时会被视为一个变量名,并且会被求值为该变量的当前值(符号宏 symbol macro 有不同的求值方式,不过新手可以暂不不去理会)。 739 | 740 | 所有非符号类型的原子,比如数字和字符串,都是自求值对象,这就意味着当这样的表达式被传递给我们假想的求值函数时,它会简单地直接返回自身。 741 | 742 | 当我们开始考虑列表的求值方式时,事情变得更加有趣了。所有合法的列表形式均以一个符号开始,但是有三种类型的列表形式,它们会以三种相当不同的方式进行求值。为了确定一个给定的列表是哪种形式,求值器必须检测列表开始处的那个符号是(列表的第一个符号)什么类型:是函数、宏、还是特殊操作符的名字。如果该符号尚未定义,比如说当你正在编译一段含有对尚未定义函数的引用代码时,它会被假设成一个函数的名字。这三种类型的形式称为函数调用形式、宏形式和特殊形式。 743 | 744 | 简单地说,你在 REPL 中输入一个 Lisp 形式--form,然后敲回车,就启动了一个求值过程,如果你输入的是一个符号原子,那么 Lisp 会把其当做一个变量处理,返回该变量的当前值,如果你输入的是一个非符号原子(自求职对象),那么 Lisp 会直接返回该对象自身。 745 | 746 | 这里再对“对自求值对象求值时,它会简单地返回自身”补充一点说明: 747 | 748 | 《实用 Common Lisp 编程》中提到: 749 | 750 | 对于一个给定类型的数字来说,它可以有多种不同的字面表示方式,所有这些都将被 Lisp 读取器转化成相同的对象表示。例如,你可以将整数 10 写成 10、20/2、#xA 或是其他形式的任何数字,但读取器将把所有这些转化成同一个对象。当数字被打印回来时,比如在 REPL中,它们将以一种可能与输入该数字时不同的规范化文本语法被打印出来。如下: 751 | 752 | CL-USER> 10 753 | 10 754 | CL-USER> 20/2 755 | 10 756 | CL-USER> #xa 757 | 10 758 | CL-USER> 759 | 760 | 761 | ### [5、对列表中函数调用形式、宏形式和特殊形式求值](id:3.5) 762 | 763 | 《GNU Emacs Lisp 编程入门》中是这么说的: 764 | 765 | 当对一个函数求值时总是返回一个值(除非得到一个错误消息)。另外,它也可以完成一些被称作附带效果的操作。在许多情况下,一个函数的主要目的是产生一个附带效果。 766 | 767 | 《实用 Common Lisp 编程》中说得更详细一些 : 768 | 769 | 函数调用形式的求值规则很简单 ,对以 Lisp形式 存在的列表其余元素进行求值并将结果传递到命名函数中(也就是列表的第一个元素)。 770 | 当列表的第一个元素是一个由特殊操作符所命名的符号时(简单说就是一个特殊操作符),表达式的其余部分将按照该操作符的规则进行求值。 771 | 先说一下宏,宏是一个以 S-表达式 为其参数的函数,并返回一个 Lisp形式,然后对其求值并利用该值取代宏形式。 772 | 宏形式求值过程包括两个阶段:首先,宏形式的元素不经求值即被传递到宏函数里;其次,由宏函数所返回的形式(称其为展开式)按照正常的求值规则进行求值。 773 | 774 | 775 | ### [6、单引号的特殊作用--构建宏的基础](id:3.6) 776 | 777 | 《GNU Emacs Lisp 编程入门》中是这么说的: 778 | 779 | 单引号告诉lisp解释器返回后续表达式的书写形式,而不是像没有单引号那样对其求值。 780 | 781 | 《实用 Common Lisp 编程》中是这么说的: 782 | 783 | 单引号是 quote 语句的语法糖。 784 | 785 | 也就是说,形如 '(1 2 3) 的语句实际上就是 (quote (1 2 3 )),实际执行效果一样,如下: 786 | 787 | CL-USER> (quote (1 2 3 )) 788 | (1 2 3) 789 | CL-USER> '(1 2 3) 790 | (1 2 3) 791 | CL-USER> 792 | 793 | 现在我们使用的 SBCL 和 CCL 中,都把单引号设置为 **quote** 的语法糖,也就是说,在这两种实现中,我们可以很方便地用单引号来代替 **quote** ,通过上面的例子可以很清楚地看到,使用语法糖可以简化代码,所以我们推荐初学者在代码中多使用语法糖。 794 | 795 | Common Lisp 实际上提供了修改这种对应关系的宏,也就是说你可以为 **quote** 设置其他不同的符号来做语法糖,不过对于常见的程序开发来说,没必要修改,而且要尽量避免修改这种对应关系。 796 | 797 | ### [7、本章内容小结](id:3.7) 798 | 799 | * Lisp 程序由 S-表达式组成,表达式是列表或单个原子 800 | * 列表由 0 个或者更多的原子或者内部列表组成,原子或者列表之间由空格分隔开,并由括号括起来。列表可以是空的 801 | * 原子是多字符的符号(如 forward-paragraph)、单字符符号(如 + 号)、双引号之间的字符串、或者数字 802 | * 对数字求值就是它本身(自求值对象都直接返回其自身) 803 | * 对双引号之间的字符串求值也是其本身(自求值对象都直接返回其自身) 804 | * 当对一个符号求值时,将返回它的值(作为变量看待,则返回该变量的当前值) 805 | * 当对一个列表求值时,lisp解释器查看列表中的第一个符号以及绑定在其上的函数定义。然后这个函数定义中的指令被执行 806 | * 单引号告诉lisp解释器返回后续表达式的书写形式,而不是像没有单引号那样对其求值 807 | * 参量是传递给函数的信息。除了作为列表的第一个元素的函数之外,通过对列表的其余元素求值来计算函数的参量(这里的参量就是我们常说的参数,《GNU Emacs Lisp 编程入门》统一把它翻译成参量,我感觉参量的翻译更准确一些,不过因为用习惯了参数,后面我还是使用参数) 808 | * 当对一个函数求值时总是返回一个值(除非得到一个错误消息)。另外,它也可以完成一些被称作附带效果的操作。在许多情况下,一个函数的主要目的是产生一个附带效果(不过纯函数式编程是希望杜绝这种附带效果的) 809 | 810 | 【说明】:这10条小结是 《GNU Emacs Lisp 编程入门》第一章总结出来的,该书主要讲解 Emacs Lisp 的基本概念和应用,客观地说 Emacs Lisp 跟 Common Lisp 虽然都是从 Lisp 演化而来,但还是存在着很大差异的,不过我在学习过程中发现对于 Common Lisp 初学者而言,可以拿 Emacs Lisp 一起来对照学习,尤其是对一些基本概念的理解,非常有帮助,而且如果 Common Lisp 初学者选择了 Emacs 作为开发环境,那你肯定需要熟悉 Emacs Lisp 的使用,否则就无法充分利用 Emacs 的高效作业。因此专门拿了一个章节来介绍 Emacs Lisp 的这些基本概念,希望初学者们能从中得到助益。 811 | 812 | 813 | ## [四、一个简单实践:文本数据库-藏书阁](id:4) 814 | 815 | 本章以一个 Common Lisp 的实际例程为主要内容,开发过程也尽量采用最适合 Common Lisp 的逐步完善、反复迭代的开发方式。 816 | 这个例子基本上全部代码都照搬了《实用 Common Lisp 编程》中的第三章“实践:简单的数据库”的内容,不过我按照自己的讲解方式稍作修改,看看这种讲述风格是否能被大家接受,同时把 CD 数据库改为书籍数据库,因为我觉得对于喜欢阅读的中国读者来说,书籍数据库可能更实用一些。 817 | 818 | 在此要感谢作者 **Peter Seibel** 和译者 **田春** 的努力,为我们提供了这么好的教程,如果作者或译者对我大量直接引用他们二位的例程有异议,请告知,我会删除重写一个例程--不过还是希望能得到二位的同意 :)。 819 | 820 | 摘录一句作者 Peter Seibel 的原文: 821 | 822 | “本章的重点和意图也不在于讲解如何用 Lisp 编写数据库,而在于让你对 Lisp 编程有个大致的印象,并能看到即便相对简单的 Lisp 程序也可以有着丰富的功能” 823 | 824 | ### [1、项目简单说明](id:4.1) 825 | 826 | 很多朋友都喜欢买实体书阅读,久而久之家里的藏书就越来越多,占满了书架,不得不把一些旧书打包到箱子里放到床下,但是有时忽然想查找某本书的内容时才发现一时找不到这本书了,于是只好翻箱倒柜一个箱子一个箱子查看,最后终于找到了,但是费了半天劲,而且搞得满身灰尘,然后坐在电脑前小憩时忽然发现原来 F:\ 盘的电子书目录里有这本书的电子版,嘿,是不是觉得特别坑爹。 827 | 828 | 很好,我们这个小型项目就是教你如何去建立一个藏书数据库,把你的所有藏书信息都输入到电脑里,包括书名、作者、内容简介、价格、购买时间、实体书保存位置、是否有电子版、电子版保存位置等等信息,同时提供查询的功能,可以根据关键字进行检索,拥有这样一个藏书数据库是不是会提高你对藏书的使用效率呢? 829 | 830 | 那就让我们一起开始吧! 831 | 832 | ### [2、定义你的原型数据结构](id:4.2) 833 | 834 | 我们已经了解了自己的软件需求,那么接下来就是选择相应的数据结构了,需要选择一种方式来表示每一条数据库记录,而每条数据库记录要包含上述提到的内容: 835 | 836 | (书名、作者、内容简介、购买时间、价格、实体书保存位置、是否有电子版、电子版保存位置) 837 | 838 | 上面的内容怎么看怎么像个列表啊,把分隔每个项目的顿号去掉换成空格,它不就是一个列表吗? 839 | 840 | (书名 作者 内容简介 购买时间 价格 实体书保存位置 是否有电子版 电子版保存位置) 841 | 842 | 不过为了简化程序输入,我们把上述列表项目稍作缩减,程序中使用如下列表: 843 | 844 | (书名 作者 价格 是否有电子版) 845 | 846 | 既然天意让它看起来这么像一个列表,那我们就使用列表作为基本的数据结构。 847 | 848 | 列表知识:我们可以使用 list 函数来生成一个列表,正常执行后,它会返回一个尤其参数组成的列表,如下: 849 | 850 | CL-USER> (list 1 2 3 4) 851 | (1 2 3 4) 852 | CL-USER> 853 | 854 | 不过鉴于我们希望在使用每条记录的每个字段时都能有对该字段的一个明确的描述,而不是必须使用数字索引来访问,所以我们选择一种被称为属性表(property list,plist)的列表,这种列表中的第1个元素用来描述第2个元素,第3个元素用来描述第4个元素,以此类推,第奇数个元素都是用来描述相邻的第偶数个元素的,换句话说就是:从第一个元素开始的所有相间元素都是一个用来描述接下来那个元素的符号(原文引用 :)),在 plist 里奇数个元素的写法使用一种特殊的符号--关键字符号(keyword)。 855 | 856 | 关键字符号是任何以冒号开始的名字,例如---》 :书名 857 | 858 | 下面是一个使用了关键字符号作为属性名的示例 plist : 859 | 860 | CL-USER> (list :书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 t) 861 | (:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T) 862 | CL-USER> 863 | 864 | 这里要提到一个属性表的函数 **getf** ,它可以根据一个 plist 中的某个字段名(属性名)来查询对应的属性值,如下所示,我们想要查询刚才建立的 plist 中的 :书名 属性名所对应的属性值: 865 | 866 | CL-USER> (getf (list :书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 t) :书名) 867 | "人间词话" 868 | CL-USER> 869 | 870 | 如果想查 :作者 是什么,输入如下: 871 | 872 | CL-USER> (getf (list :书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 t) :作者) 873 | "王国维" 874 | CL-USER> 875 | 876 | 有了上述这些基本知识,我们就可以写出一个简单的名为 建立书籍信息 的函数了,它以参数的形式接受 4 个属性字段,然后返回一个代表该书的 plist: 877 | 878 | (defun 建立书籍信息 (书名 作者 价格 是否有电子版) 879 | (list :书名 书名 :作者 作者 :价格 价格 :是否有电子版 是否有电子版)) 880 | 881 | 我们定义了这个函新数,函数名是 **建立书籍信息** ,跟在函数名后面的是形参列表,这个函数有 4 个形参,分别是: 书名、作者、价格、是否有电子版,这个示例中的函数体只有一个 Lisp形式--form ,这个唯一的 Lisp形式 就是对函数 list 的调用。当函数 **建立书籍信息** 被调用时,传递给该调用的 4 个实参将被绑定到形参列表中的变量上。比如为了建立刚才那本《人间词话》的书籍信息,我们可以这样调用这个函数: 882 | 883 | CL-USER> (建立书籍信息 "人间词话" "王国维" 100 t) 884 | (:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T) 885 | CL-USER> 886 | 887 | 列表实际上有 8 个元素,第奇数个元素是 **字段名** ,第偶数个元素是 **字段值**,奇数位置的 **字段名** 使用关键字符号表示(以冒号打头),偶数位置的 **字段值** 则根据实际类型来选择 **字符串**、**数字**、**布尔值** 来表示。 888 | 889 | 为了例程书写方便,我们只使用了 4 个字段来记录书籍信息,但是对于一本书来说,4 个字段的信息当然有些不足,后续大家可以自行增加其他字段。 890 | 891 | ### [3、正式开工:首先是数据录入模块](id:4.3) 892 | 893 | 只有一条记录的数据库对应一个 plist 列表,只能记录一本书的信息,显然无法满足我们的实际需求,因此我们准备使用全局变量来记录多个列表的信息,每个列表就是一条记录,保存一本书的信息。 894 | 895 | 在 Common Lisp 中,全局变量的命名约定是名字前后各加一个星号 * ,这样的形式: 896 | 897 | *书籍数据库* 898 | 899 | 全局变量可以使用宏 defvar 来定义,初值为 nil,如下: 900 | 901 | (defvar *书籍数据库* nil) 902 | 903 | 我们可以使用宏 **push** 来为全局变量 \*书籍数据库\* 增加新的记录,但是这里希望能稍微做得抽象一些,于是就要定义一个函数: **增加记录**,具体定义如下: 904 | 905 | (defun 增加记录 (书籍信息) 906 | (push 书籍信息 *书籍数据库*)) 907 | 908 | 很好,现在就可以把两个函数结合在一起使用了,先用 **建立书籍信息** 建立一条书籍信息的记录,该函数返回一个 plist,再把这个 plist 作为函数 **增加记录** 的输入参数,由函数 **增加记录** 把该条数据添加到用全局变量 \*书籍数据库\* 中。 909 | 910 | CL-USER> (增加记录 (建立书籍信息 "人间词话" "王国维" 100 t)) 911 | ((:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 912 | CL-USER> (增加记录 (建立书籍信息 "说文解字" "许慎" 100 t)) 913 | ((:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 914 | (:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 915 | CL-USER> (增加记录 (建立书籍信息 "难忘的书与插图" "汪家明" 38 t)) 916 | ((:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 917 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 918 | (:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 919 | CL-USER> 920 | 921 | 为什么每次执行完后,会把整个数据库的内容都返回呢?因为每次执行这段增加记录的代码实际上执行的是 **push** 这个宏,而 **push** 所修改的全局变量 \*书籍数据库\* ,其实是这样一个大列表 ((plist1) (plist2) (pist3)),push 会把它正在修改的变量的新值返回,对 **push** 来说,它修改的变量就是这个大列表---它每次在里面增加一个小列表,因此每次执行后都会把它修改后的大列表整个返回。 922 | 923 | ### [4、其次是数据显示模块](id:4.4) 924 | 925 | 此时我们可以在 REPL 中输入全局变量 **\*书籍数据库\*** 来查看它的当前值如下: 926 | 927 | CL-USER> *书籍数据库* 928 | ((:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 929 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 930 | (:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 931 | CL-USER> 932 | 933 | 但是很显然,这种查看输出的方式有些凌乱,我们可以新一个名为 **转储显示** 的函数来把数据库内容稍微整理一下显示格式,然后再输出,希望效果如下: 934 | 935 | 书名: 难忘的书与插图 936 | 作者: 汪家明 937 | 价格: 38 938 | 是否有电子版: T 939 | 940 | 书名: 说文解字 941 | 作者: 许慎 942 | 价格: 100 943 | 是否有电子版: T 944 | 945 | 书名: 人间词话 946 | 作者: 王国维 947 | 价格: 100 948 | 是否有电子版: T 949 | 950 | 该函数如下所示: 951 | 952 | (defun 转储显示 () 953 | (dolist (单条书籍记录 *书籍数据库*) 954 | (format t "~{~a: ~20t~a~%~}~%" 单条书籍记录))) 955 | 956 | 该函数的工作原理是使用 **dolist** 宏在 **\*书籍数据库\*** 的所有元素上循环,依次绑定每个元素到变量 **书籍字段信息** 上,然后再用 **format** 函数打印出每个 **书籍字段信息** 的值。 957 | 958 | 这里稍微介绍一下 **format** 的语法,它就像 C 语言中的函数 **printf** 一样,使用格式控制字符来实现格式化输出。 959 | 960 | **format** 函数的第一个实参是它的输出目的地,这里是 **t** ,是一个简称,表示标准输出流 **\*standard-output\*** ,它的第二个实参是一个格式字符串,格式字符串也是一个用双引号引起来的字符串,为了区别于一般的字符串,它使用 **~** 符号来标识格式指令(类似于 **printf** 函数的格式指令 **%**)。 961 | 962 | 下面针对函数 **转储显示** 中使用的这条 **format** 进行解析: 963 | 964 | (format t "~{~a: ~20t~a~%~}~%" 单条书籍记录))) 965 | 966 | 首先明确一点,所有的格式指令都以 **~** 符号开始,各指令具体含义如下所示: 967 | 968 | ~{ format 的循环语法,表示下一个对应的实参是一个列表的开始,然后 format 会在该列表上进行循环操作,处理位于 ~{ 和 ~} 之间的指令,每轮循环处理多少个实参取决于 ~{ 和 ~} 之间有多少个对应实参的指令,执行多少轮循环取决于 “单挑书籍记录” 中的元素的个数(确切说:循环轮数 = 元素个数 除以 每轮循环处理实参个数),所以可以通过使用 ~{ 和 ~} 来实现循环, 969 | ~} 同上,和 ~{ 配合使用 970 | ~a 美化指令,该指令对应一个实参,会把这个实参的显示形式输出为更适合阅读的形式,具体说就是形如 :书名 的关键字在输出时会被去掉冒号,形如 "人间词话" 的字符串在输出时会被去掉双引号 971 | ~t 表示制表指令,不对应实参,只移动光标,~20t 告诉 format 把光标向后移动 20 列 972 | ~% 表示换行,不对应实参 973 | 974 | 另外要注意 格式指令字符串中所有的非格式指令均以原样输出,比如 ~a 后面的冒号 : 和空格就直接原样输出 975 | 976 | 再对照我们上述的代码,就比较清楚了,首先是一个大循环: 977 | 978 | (dolist (单条书籍记录 *书籍数据库*) 979 | ( 。。。))) 980 | 981 | 每轮大循环都从 **\*书籍数据库\*** 里取出一条记录,把该条记录的内容赋值给(绑定到)变量 **单条书籍记录** ,其内容如下: 982 | 983 | (:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T) 984 | 985 | 然后再进入函数 **format** 内的小循环,我们看到在表示小循环的 ~{ 和 ~} 之间,有多个格式指令,但是只有两个 ~a 指令需要对应两个实参,其他格式指令分别用于移动光标和换行,所以每轮小循环处理两个字段,像这个例子就是: 986 | 987 | 1)当 **format** 看到 **~{** 就进入第一轮小循环; 988 | 989 | 2)先处理 **:书名** 和 **"人间词话"** 这两个字段,第一个 **~a** 指令把 **:书名** 字段的冒号 **:** 去掉,输出 **书名** ; 990 | 991 | 直观演示一下: 992 | 993 | CL-USER> (format t "~{~a: ~20t~a~%~}" (list :书名 "人间词话")) 994 | 书名: 人间词话 995 | NIL 996 | CL-USER> 997 | 998 | 3)紧跟着 **~a** 指令的 **冒号** 和 **空格** 原样输出; 999 | 1000 | 换个符号,把第一个 ~a 后面的冒号空格换成 ====》试试: 1001 | 1002 | CL-USER> (format t "~{~a====》 ~20t~a~%~}" (list :书名 "人间词话")) 1003 | 书名====》 人间词话 1004 | NIL 1005 | CL-USER> 1006 | 1007 | 4)指令 **~20t** 则把光标右移20列; 1008 | 1009 | 把 ~20t 换成 ~50t 试试: 1010 | 1011 | CL-USER> (format t "~{~a: ~50t~a~%~}" (list :书名 "人间词话")) 1012 | 书名: 人间词话 1013 | NIL 1014 | CL-USER> 1015 | 1016 | 1017 | 5)第二个 **~a** 指令把 **"人间词话"** 字段的双引号 **""** 去掉,输出 **人间词话** ; 1018 | 1019 | 6)指令 **~%** 则执行换行; 1020 | 1021 | 7)然后 **format** 看到 **~}** ,知道本轮循环结束; 1022 | 1023 | 8)此时因为 **单条书籍记录** 中还剩下后面 6 个字段,于是启动第二轮小循环; 1024 | 1025 | 9)这次处理 **:作者** 和 **"王国维"** 这两个字段; 1026 | 1027 | 10)接下来的处理跟上述的处理类似 。。。 1028 | 1029 | 11)第三轮小循环处理 **:价格** 和 **100** 这两个字段; 1030 | 1031 | 11)一直到第四轮小循环,处理完 **:是否有电子版** 和 **T** 这两个字段; 1032 | 1033 | 12)这时 **单条书籍记录** 的所有元素都已经完成处理,就结束小循环,执行最后的 **~%** ,换行。 1034 | 1035 | 然后就是下一轮大循环,再从 **\*书籍数据库\*** 里取出第二条记录,然后把第二条记录的内容赋值给变量 **\*书籍数据库\*** ,然后就再次进入小循环,。。。就这样反复循环,直到把 **\*书籍数据库\*** 中的所有记录都循环一遍,这时就完成了大循环。 1036 | 1037 | 看了上述的分析,你就会发现,其实那个大循环并不是一定要有的,完全可以把所有的循环操作都放在 **format** 中处理,让 **format** 直接在 **\*书籍数据库\*** 这个大列表上循环处理其中每个小列表中的字段信息,代码如下: 1038 | 1039 | (defun 转储显示 () 1040 | (format t "~{~{~a: ~20t~a~%~}~%~}" *书籍数据库*))) 1041 | 1042 | 修改很简单,首先是在 **format** 原来的格式字符串最外围增加一对 ~{ 和 ~} ,其次就是循环的对象由原来的 **单条书籍记录** 改为 **\*书籍数据库\*** ,两种形式都可以,不过就我个人而言,比较推荐第一种,因为看起来更清晰,更具备可读性。 1043 | 1044 | 1045 | ### [5、充分发挥迭代的优势:改进一下输入方式](id:4.5) 1046 | 1047 | 程序写到这里,已经能够接受信息输入、把信息储存到数据库、显示数据库的信息到屏幕,可以说初具规模了,可是有些朋友可能会觉得那个输入方式的界面太不友好,什么提示也没有,而且如果一旦需要大量输入时,这种操作不太方便,也可能出错,所以提出希望能在这里迭代一下---把旧的输入函数改造成一个更好用的、有提示的输入界面,很好,下面我们先写一个带提示信息的输入接口: 1048 | 1049 | (defun 提示输入 (提示信息) 1050 | (format *query-io* "~a: " 提示信息) 1051 | (force-output *query-io*) 1052 | (read-line *query-io*)) 1053 | 1054 | 首先使用 **format** 产生一个提示,然后用 **force-output** 保证在不同 Common Lisp 实现中都表现出相同的效果---确保 Lisp 在打印提示信息前不会等待用户输入换行。 1055 | 1056 | 然后使用函数 **read-line** 来读取单行文本。变量 **\*query-io\*** 是一个含有关联到当前终端的输入流的全局变量。 1057 | 1058 | 把这个 **提示输入** 函数和我们前面的 **建立书籍信息** 函数组合起来,构造出一个新函数,每次输入前都会提示应该输入哪个字段,如下: 1059 | 1060 | (defun 提示书籍信息 () 1061 | (建立书籍信息 1062 | (提示输入 "书名") 1063 | (提示输入 "作者") 1064 | (提示输入 "价格") 1065 | (提示输入 "是否有电子版[y/n]"))) 1066 | 1067 | 这样在输入每个字段都增加一个对应的字段内容提示信息,用起来就不容易输错了。 1068 | 1069 | 在这里《实用 Common Lisp 编程》的作者专门提及用户输入验证的问题,并据此对 **价格** 字段和 **是否有电子版** 字段的输入函数做了针对性的修改。 1070 | 1071 | 我觉得作者在此处的讲解表现出相当不凡的专业素养!每一个初学者都应该把这一页内容反复理解,尽量培养自己的这种对于用户输入验证精益求精的态度,这是评价一个软件是究竟是一个玩具软件还是商用级别的工业软件的关键标准! 1072 | 1073 | 养成这种良好的习惯,你编写的哪怕是一个最小规模的的软件从一开始就不会因为用户的各种错误输入而意外崩溃,健壮性是非常、非常重要的! 1074 | 1075 | 而这种细节习惯的养成也将会节省你大量的返工时间---虽然在开始时要多花一些时间去考虑各种输入场景,不过我建议如果做商业开发可以把用户输入验证这部分功能代码做成统一的模块,由专人负责维护,其他人直接重用就行了,这样可以兼顾健壮性和效率,如果是个人开发者也可以专门投入一定时间把这一部分模块化,以后每次直接使用就可以了。 1076 | 1077 | 健壮的格式如下: 1078 | 1079 | (defun 提示书籍信息 () 1080 | (建立书籍信息 1081 | (提示输入 "书名") 1082 | (提示输入 "作者") 1083 | (or (parse-integer (提示输入 "价格") :junk-allowed t) 0) 1084 | (y-or-n-p "是否有电子版[y/n]: "))) 1085 | 1086 | 1087 | **小作业:** 1088 | 1089 | **:价格** 字段在上面的输入数据验证中是当做整数处理的,实际实际的价格非常可能不是整数,现在虽然大多数图书标价都是整数,但是一打折不就有小数出来了?所以这里真正需要的是能满足整数和小数的输入验证,就当做作业,自己去思考怎么验证吧。 1090 | 1091 | 上面修改后的输入函数可以很好地提示和验证,但是有个问题,就是每输入一本书的信息就需要执行一次,批量输入时岂不是很繁琐?那我们就把它作成循环的输入接口好了。 1092 | 1093 | 批量输入代码如下: 1094 | 1095 | (defun 批量输入 () 1096 | (loop (增加记录 (提示书籍信息)) 1097 | (if (not (y-or-n-p "还要继续输入下一本书籍的信息吗?[y/n]: ")) (return)))) 1098 | 1099 | 执行效果如下: 1100 | 1101 | CL-USER> (批量输入 ) 1102 | 书名: 血色黄昏 1103 | 作者: 老鬼 1104 | 价格: 25 1105 | 是否有电子版[y/n]: n 1106 | 1107 | 还要继续输入下一本书籍的信息吗?[y/n]: (y or n) n 1108 | 1109 | NIL 1110 | CL-USER> 1111 | 1112 | 1113 | ### [6、保存和加载已经录入的数据](id:4.6) 1114 | 1115 | 我们上面建立的数据库依赖于全局变量 **\*书籍数据库\*** ,所有的数据库信息都储存在内存里,一旦重启 Common Lisp 环境,这些数据就全部丢失了,因此为了能在重启后保持数据库不丢失,我们准备把建立在内存里的数据库以一个文本文件的形式保存到硬盘上,这样就可以在重启后加载这个文本文件形式的数据库到内存,避免了每次都要重新输入的烦恼,代码如下: 1116 | 1117 | (defun 保存数据库 (带路径的保存文件名) 1118 | (with-open-file (文件绑定变量 带路径的保存文件名 1119 | :direction :output 1120 | :if-exists :supersede) 1121 | (with-standard-io-syntax 1122 | (print *书籍数据库* 文件绑定变量)))) 1123 | 1124 | 该函数的实参 **带路径的保存文件名** 是一个含有用户打算用来保存数据库的文件名字符串,在 MS-Windows 和 Mac OSX 操作系统上,应该携带文件路径,比如在 Mac OSX 系统下应该这样调用: 1125 | 1126 | CL-USER> (保存数据库 "~/ecode/markdown-doc/book-db.txt") 1127 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 "25" :是否有电子版 "n") 1128 | (:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 1129 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 1130 | (:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 1131 | CL-USER> 1132 | 1133 | 在 MS-Windows 系统下应该这样调用: 1134 | 1135 | CL-USER> (保存数据库 "F://ecode//markdown-doc//book-db.txt") 1136 | 1137 | 至于数据库文件名,我这里使用了 **book-db.txt** ,之所以选择保存为 txt 格式的文件,是为了方便查看其内容,随便找一个文本编辑器就可以打开 **txt** 文本文件,其实你可以使用任何一种后缀名,甚至可以自定义一种后缀名,专门作为这个程序的数据库文件格式。 1138 | 1139 | 这里用到了 **print** 函数,它会将 **Lisp对象** 打印成一种可以被 **Lisp读取器** 读回来的形式。 1140 | 1141 | 这段代码的具体操作就是: 1142 | 1143 | 1)首先,宏 **with-open-file** 根据我们输入的参数 **带路径的保存文件名** 打开一个文件,然后将文件流绑定到 **文件绑定变量** 上; 1144 | 1145 | 2)接着会执行一组表达式,就是这个: 1146 | 1147 | (with-standard-io-syntax 1148 | (print *书籍数据库* 文件绑定变量) 1149 | 1150 | 3)这组表达式执行的操作如下:宏 **with-standard-io-syntax** 确保对函数 **print** 的一致性使用---有些特定的变量的值可能会影响函数 **print** 的行为,现在由宏 **with-standard-io-syntax** 把这些特定变量全部设置为标准值,代码 **(print \*书籍数据库\* 文件绑定变量)** 则把 **\*书籍数据库\*** 的内容打印到 **文件绑定变量** ,因为 **文件绑定变量** 绑定到了我们新打开的文件上,所以实际上就把 **\*书籍数据库\*** 的内容写入到文件中了; 1151 | 1152 | 4)执行完这组表达式,再由宏 **with-open-file** 关闭文件。 1153 | 1154 | 看到这里就会发现这个宏 **with-open-file** 有个好处,就是不需要我们手动关闭文件,它会自动关闭,非常环保啊,以后一定要多用这个宏! :) 1155 | 1156 | 1157 | 既然写好了保存函数,那就再写一个加载函数,代码如下: 1158 | 1159 | (defun 加载数据库 (带路径的加载文件名) 1160 | (with-open-file (文件绑定变量 带路径的加载文件名) 1161 | (with-standard-io-syntax 1162 | (setf *书籍数据库* (read 文件绑定变量))))) 1163 | 1164 | 加载代码的操作跟保存代码相反,不过使用了类似的宏和函数,就不再详述了。 1165 | 1166 | 值得注意的是这两个函数 **print** 和 **read** 1167 | 1168 | **print** 可以打印 Lisp对象,以一种 **Lisp读取器** 可以读取的形式。 1169 | 1170 | **read** 可以从流中读入数据,它使用与 REPL 相同的 **读取器**,可以读取我们在 REPL 提示符下输入的任何表达式。 1171 | 1172 | 1173 | 1174 | ### [7、查询数据库](id:4.7) 1175 | 1176 | 有了方便、友好的批量输入函数,意味着我们的藏书数据库中的书籍信息记录可能会越来越多,这样如果每次使用函数 **转储查看** 想查看有哪些书时就不得不面对满屏的信息了,是不是感觉不太方便?记得好像有本书,讲的主题就是《数量一多,一切就都不一样了》,我们也遇到了第一个瓶颈---数量带来的麻烦。 1177 | 1178 | 那就想办法解决这个小瓶颈吧,再次进入我们的迭代流程,我们需要实现的就是一个能够按照给出条件进行筛选查找的函数,比如你这次查书籍数据库只是想找一找 **王国维** 写的书,换句话说,就是根据 **:作者** **"王国维"** 这组数据进行查找,写成函数就是: 1179 | 1180 | (查找 :作者 "王国维") 1181 | 1182 | Common Lisp 提供了这样一个函数 **remove-if-not** ,它有两个参数,第一个参数是一个 **谓词**,第二个参数是一个 **列表**,它会返回一个仅包含 **原始列表** 中匹配该 **谓词** 的所有元素的新列表,举个简单的数字例子,有一个由一些自然数组成的列表,我们想把其中所有的偶数取出来,如下所示: 1183 | 1184 | CL-USER> (remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10)) 1185 | (2 4 6 8 10) 1186 | CL-USER> 1187 | 1188 | 这里的 **谓词** 是函数 **evenp** ,当它的参数是偶数时返回真,符号 **#'** 在前面提到过,是一个表示后续符号是函数的符号,等价于函数 **function** ,表示要把函数 **evenp** 作为参数,也可以写成这样: 1189 | 1190 | CL-USER> (remove-if-not (function evenp) '(1 2 3 4 5 6 7 8 9 10)) 1191 | (2 4 6 8 10) 1192 | CL-USER> 1193 | 1194 | 如果没有函数 **evenp**,或者你不知道这个函数,也可以自己写一个匿名函数,如下: 1195 | 1196 | CL-USER> (remove-if-not #'(lambda (x) (= 0 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10)) 1197 | (2 4 6 8 10) 1198 | CL-USER> 1199 | 1200 | 在这里我们首次提到了 **匿名函数**,它是这样一种形式:**lambda** 跟 **defun** 的语法非常接近,**lambda** 后面紧跟着形参列表,然后是函数体。 1201 | 1202 | 也就是说,我们现在需要写的函数 **查找** ,它会去逐条对比数据库中的记录,遇到 **:作者** 字段的字段值为 **"王国维"** 时就返回真,前面介绍过的 **getf** 函数,现在可以拿来使用了,可以用它来获取 **单条记录** 中 **:作者** 字段的值,也就是列表 **(:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)** 中的第二个元素,语句如下: 1203 | 1204 | (getf 单条记录 :作者) 1205 | 1206 | 实际上等价于这条语句: 1207 | 1208 | (getf (:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T) :作者) 1209 | 1210 | 再用一个比较函数 **equal** 拿它返回的值跟一个我们输入的包含作者名字的字符串参数进行比较,比如我们想拿作者名字是 **"王国维"** 的字符串进行比较,代码如下: 1211 | 1212 | (equal (getf 单条记录 :作者) "王国维") 1213 | 1214 | 那么完整的代码如下: 1215 | 1216 | CL-USER> (remove-if-not #'(lambda (单条记录) (equal (getf 单条记录 :作者) "王国维")) *书籍数据库*) 1217 | ((:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 1218 | CL-USER> 1219 | 1220 | 我们可以把上面这段代码包装一下,做成一个可以用 **作者** 作为输入参数的的函数里,如下: 1221 | 1222 | (defun 用作者名查找 (查找字符串-作者) 1223 | (remove-if-not 1224 | #'(lambda (单条记录) 1225 | (equal (getf 单条记录 :作者) 查找字符串-作者)) 1226 | *书籍数据库*)) 1227 | 1228 | 这个函数涉及到 Common Lisp 的一个据说是比较有趣的特性---闭包,不过奇怪的是我从没觉得闭包有什么特别。。。 1229 | 1230 | 这样我们完成一个可以通过 **作者** 来查询的函数,但是很可能你还会需要通过 **价格** 查询、通过 **书名** 查询、通过任意一个字段查询,怎么办呢?难道要把这些函数都写一遍吗?感觉好像有很多重复代码,先写一个通过书名查询的函数看看: 1231 | 1232 | (defun 用书名查找 (查找字符串-书名) 1233 | (remove-if-not 1234 | #'(lambda (单条记录) 1235 | (equal (getf 单条记录 :书名) 查找字符串-书名)) 1236 | *书籍数据库*)) 1237 | 1238 | 果然不出所料,整个函数体中只有匿名函数体中这条语句 **(getf 单条记录 :书名)** 里的 **:书名** 跟上一个函数的 **:作者** 不一样。 1239 | 1240 | 那么很显然,针对每个字段编写这么一个函数是一种比较愚蠢的行为,我们现在还只有 4 个字段,编 4 个基本类似的查找函数还勉强行得通,可如果将来扩展到 100 个字段怎么办? 难道要编 100 个极其相似的查找函数? 1241 | 1242 | 这种疯狂、低效的行为我们是绝不提倡的,那么就想办法把这个功能再抽象一下,用一种通用的方法来实现,因为上述两段代码唯一的区别在匿名函数,我们可以把匿名函数抽象出来。 1243 | 1244 | 假设用 **根据?查找函数** 这个名称来代替匿名函数,其中的 **?** 可以换成 **书名**、**作者** 乃至任何一个字段,我们再定义一个通用的 **查找** 函数,它以函数 ***根据?查找函数** 为参数,伪码如下: 1245 | 1246 | (defun 查找 (根据?查找函数) 1247 | (remove-if-not 1248 | 根据?查找函数 1249 | *书籍数据库*)) 1250 | 1251 | 对比一下,就会发现这个通用的函数 **查找** 的结构跟前面的函数 **用作者名查找** 和 **用书名查找** 基本一样,唯一不同的地方就是用 **根据?查找函数** 替换了原来的匿名函数 **#'(lambda (单条记录) (equal (getf 单条记录 :书名) 查找字符串-书名))** 。 1252 | 1253 | 为什么 **remove-if-not** 的第一个参数 **根据?查找函数** 没有使用 **#'** ?因为对于函数 **remove-if-not** 来说,它不希望得到一个固定的名为 **根据?查找函数** 的函数,实际上它也无法得到这个函数,因为这个函数不存在,符号 **根据?查找函数** 只是一个变量,是一个用于参数传递的变量,这个变量先接收一个匿名函数保存起来,再把它保存的匿名函数作为函数 **查找** 的实参传递给函数 **查找** 的相应位置。 1254 | 1255 | 当你真正执行函数 **查找** 时,你还是会在它的输入参数(也就是那个匿名函数)前加上 **#'** ,如下: 1256 | 1257 | CL-USER> (查找 #'(lambda (单条记录) (equal (getf 单条记录 :作者) "王国维"))) 1258 | ((:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 1259 | CL-USER> 1260 | 1261 | 不过不加 **#'** 也可以,如下: 1262 | 1263 | CL-USER> (查找 (lambda (单条记录) (equal (getf 单条记录 :作者) "王国维"))) 1264 | ((:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 1265 | CL-USER> 1266 | 1267 | 这是因为 Common Lisp 对于匿名函数 **lambda** 的处理机制如此,不带 **#'** 的 **lambda** 匿名函数,当它出现在一个会被求值的上下文时,会被展开成一个带 **#'** 的 **lambda** 匿名函数,比如 **(lambda () 42)** 会被展开成 **#'(lambda () 42)** 。 1268 | 1269 | 想要深究的朋友可以尝试一下这个[【错误试验】](#7e) 1270 | 1271 | 感觉这种调用方法看起来有些不太清爽,长长的一串,我们再用一个函数把匿名函数也包装一下,如下: 1272 | 1273 | (defun 选择器-选作者 (作者) 1274 | #'(lambda (单条记录) 1275 | (equal (getf 单条记录 :作者) 作者))) 1276 | 1277 | 现在对函数 **查找** 的调用看起来清爽多了,如下: 1278 | 1279 | CL-USER> (查找 (选择器-选作者 "王国维")) 1280 | ((:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 1281 | CL-USER> 1282 | 1283 | 那么接下来就要定义其他的选择器函数了,比如 **选择器-书名**、**选择器-价格** 等等,可这些工作同样会有大量重复代码,于是我们希望继续抽象,把共同的部分提炼出来,干脆搞一个通用的选择器函数生成器,它可以根据传递的参数,自动生成可用于不同字段甚至字段组合的选择器函数。 1284 | 1285 | 这个选择器函数生成器需要我们增加一点关于函数的知识储备:**关键字形参** **&key**: 1286 | 1287 | 目前我们所使用过的函数都是比较简单的形参列表,形参和实参一一对应地进行绑定,函数定义了几个形参,在调用时就必须输入几个实参,否则就会报错。但是很多时候,我们都希望函数能够提供一种灵活的参数输入方式,比如可以指定对特定参数的输入,同时有些参数如果没有输入就由函数自动设置一个默认值。 1288 | 1289 | **关键字形参** **&key** 可以实现上述这些需求,它与普通形参的唯一区别就是在形参列表开始处有一个 **&key** ,如下: 1290 | 1291 | (defun 示例函数 (&key a b c ) (list a b c)) 1292 | 1293 | 执行效果如下: 1294 | 1295 | CL-USER> (defun 示例函数 (&key a b c ) (list a b c)) 1296 | 示例函数 1297 | CL-USER> (示例函数 :a 1 :b 2 :c 3) 1298 | (1 2 3) 1299 | CL-USER> (示例函数 :c 3 :b 2 :a 1) 1300 | (1 2 3) 1301 | CL-USER> (示例函数 :c 3 :a 1) 1302 | (1 NIL 3) 1303 | CL-USER> (示例函数 ) 1304 | (NIL NIL NIL) 1305 | CL-USER> 1306 | 1307 | 还可以判断某个形参的值是从实参传进去的还是由函数自己指定的 1308 | 1309 | 。。。。。 1310 | 1311 | 好,知识储备更新完毕,现在继续研究我们的通用选择器函数生成器,我们给它起个名字就叫 **筛选条件** ,它应该可以接受对应于我们的书籍记录字段的 4 个关键字形参,然后生成一个选择器函数,我们希望是这样的形式: 1312 | 1313 | (查找 (筛选条件 :作者 "王国维")) 1314 | (查找 (筛选条件 :书名 "人间词话")) 1315 | 1316 | 具体的代码如下: 1317 | 1318 | (defun 筛选条件 (&key 书名 作者 价格 (是否有电子版 nil 是否有电子版-p)) 1319 | #'(lambda (单条记录) 1320 | (and 1321 | (if 书名 1322 | (equal (getf 单条记录 :书名) 书名) t) 1323 | (if 作者 1324 | (equal (getf 单条记录 :作者) 作者) t) 1325 | (if 价格 1326 | (equal (getf 单条记录 :价格) 价格) t) 1327 | (if 是否有电子版-p 1328 | (equal (getf 单条记录 :是否有电子版) 是否有电子版) t)))) 1329 | 1330 | 这个函数根据你输入的参数来构造匿名函数,首先判断某个参数是否有输入,如果有就生成该字段的选择器函数,如果没有就不生成该字段的选择器函数,而且不论是否的返回一个匿名函数,匿名函数的返回是。 1331 | 1332 | 仔细分析就会发现函数 **筛选条件** 中有两种参数,一种是需要显式输入的 **关键字形参**: 书名、作者等查询条件,这些参数由函数 **筛选条件** 接收,然后传递给其内部的匿名函数 **Lambda** 对应的位置,另一种就是匿名函数 **lambda** 的形参 **单条记录**,在这里看不到有明显的传递,因为函数 **查找** 包装了函数 **remove-if-not**,层次关系如下: 1333 | 1334 | (remove-if-not (筛选条件 (关键字形参) (lambda (单条记录) 匿名函数体)) *书籍数据库*) 1335 | 1336 | 所以形参 **单条记录** 实际是通过函数 **remove-if-not** 提供的变量---列表 **\*书籍数据库\*** 传递的,它会在列表的元素上挨个循环,也就是说会把列表中的所有元素依次绑定到 **单条记录** 上,所以看起来这个参数的传递不是很直观 1337 | 1338 | 执行效果如下: 1339 | 1340 | 1、根据作者查找: 1341 | 1342 | CL-USER> (查找 (筛选条件 :作者 "王国维")) 1343 | ((:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 1344 | CL-USER> 1345 | 1346 | 1347 | 2、根据作者和书名组合查找: 1348 | 1349 | CL-USER> (查找 (筛选条件 :作者 "王国维" :书名 "人间词话")) 1350 | ((:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 1351 | CL-USER> 1352 | 1353 | 1354 | 3、根据作者和是否有电子版组合查找: 1355 | 1356 | CL-USER> (查找 (筛选条件 :作者 "王国维" :是否有电子版 T)) 1357 | ((:书名 "人间词话" :作者 "王国维" :价格 100 :是否有电子版 T)) 1358 | CL-USER> 1359 | 1360 | 4、如果不输入任何筛选条件,会是什么结果呢?也就是这样: (查找 (筛选条件 )) 1361 | 如果你能自行在头脑里把这个结果推出来,那就说明你对这个函数的逻辑真正理解了,也真正明白 remove-if-not 函数的处理逻辑,一时搞不清楚也没关系,到环境里跑一下程序就清楚了。 1362 | 1363 | --- 1364 | 1365 | * [【错误试验】](id:7e) 1366 | 1367 | 你可以自己试试在 **remove-if-not** 后面使用 **#'根据?查找函数** ,然后在调用函数 **查找** 时不在它的输入参数前加 **#'** ,看看结果如何,试错代码如下: 1368 | 1369 | 用于试错的函数定义: 1370 | 1371 | (defun 查找 (根据?查找函数) 1372 | (remove-if-not 1373 | #'根据?查找函数 1374 | *书籍数据库*)) 1375 | 1376 | 实际上如果这样定义这个函数,那个作为实参的匿名函数是没办法正确传递到我们所期望的位置上的,你试着先编译一下新定义,再带一个匿名函数实参执行一次 **查找** 函数就知道了。 1377 | 1378 | 1379 | 用于试错的函数调用: 1380 | 1381 | (查找 (lambda (单条记录) (equal (getf 单条记录 :作者) "王国维"))) 1382 | 1383 | (查找 #'(lambda (单条记录) (equal (getf 单条记录 :作者) "王国维"))) 1384 | 1385 | 在我使用的 Clozure CL 环境下,这两种调用方式都无法正确传递实参。 1386 | 1387 | --- 1388 | 1389 | ### [8、更新记录](id:4.8) 1390 | 1391 | 经过持续的努力,我们获得了相当完美的通用函数 **查看** 和 **筛选条件** ,现在已经具备编写一个所有数据库都需要的重要函数--**更新** 函数了,在关系数据库查询语言 SQL 中,它一般叫 **update** ,**更新** 函数在数据库中的作用非常大,有了它我们可以方便地修改部分数据,而不需要把错误的记录先删除再重新输入。 1392 | 1393 | 因为有了前面准备的基础,我们可以很迅速地整理出 **更新** 函数的思路: 1394 | 1395 | 使用一个通过参数传递的选择器函数来选取需要更新的记录,再使用关键字形参来指定需要改变的值。 1396 | 1397 | 代码如下: 1398 | 1399 | (defun 更新记录 (根据?查找函数 &key 书名 作者 价格 (是否有电子版 nil 是否有电子版-p)) 1400 | (setf *书籍数据库* 1401 | (mapcar 1402 | #'(lambda (单条记录) 1403 | (when (funcall 根据?查找函数 单条记录) 1404 | (if 书名 1405 | (setf (getf 单条记录 :书名) 书名)) 1406 | (if 作者 1407 | (setf (getf 单条记录 :作者) 作者)) 1408 | (if 价格 1409 | (setf (getf 单条记录 :价格) 价格)) 1410 | (if 是否有电子版-p 1411 | (setf (getf 单条记录 :是否有电子版) 是否有电子版))) 1412 | 单条记录) 1413 | *书籍数据库*))) 1414 | 1415 | 这段代码的主体部分由这两个函数:**setf** 和 **mapcar** 的具体代码组成,简单说就是先用 **mapcar** 在原来的数据库变量 **\*书籍数据库\*** 的基础上生成一个结构完全相同,但是部分字段值发生更新的新列表作为返回值,伪码如下: 1416 | 1417 | (mapcar #'把后面的列表参数中的指定字段按照指定值进行更新 *书籍数据库*)==>新列表 1418 | 1419 | 然后再使用赋值函数 **setf** 把这个新列表赋值给全局变量 **\*书籍数据库\*** ,伪码如下: 1420 | 1421 | (setf *书籍数据库* mapcar返回的新列表) 1422 | 1423 | 注意函数 **mapcar** 使用的那个匿名函数 **lambda** ,它执行的操作实际也是一个小循环,每次从全局变量 **\*书籍数据库\*** 列表中取一条记录,执行这条语句: 1424 | 1425 | (when (funcall 根据?查找函数 单条记录) 。。。 1426 | 1427 | 如果记录有效则返回真值(因为我们在这里调用的 **根据?查找函数** 函数是 **筛选条件**,所以也就是根据你输入的筛选参数针对每条 **单条记录** 进行对照查找),继续执行内部的判断、赋值语句。 1428 | 1429 | 实例如下:如果用户在 **筛选条件** 函数中输入了关键字形参 **:书名** 的实参值 **"人间词话"** 作为筛选参数,然后在后面更新字段的关键字形参中输入 **:价格** 的实参值 **24**,也就是说用户输入的筛选条件为 **:书名** = **"人间词话"**,更新字段为 **:价格** ,更新值为 **24**, 形如: 1430 | 1431 | (更新记录 (筛选条件 :书名 "人间词话") :价格 24) 1432 | 1433 | 则根据用户的输入值更新该条记录中对应的字段值。 1434 | 1435 | 大致说一下 **mapcar** 这个函数,这是一个操作列表的函数,它的返回结果也是一个列表,它的第一个参数是一个函数,后续的参数都是列表。 1436 | 1437 | 它利用第一个函数参数对指定列表的对应元素进行操作,如果后续参数是一个数字列表,它可以给列表中每个元素加个1,然后返回新列表: 1438 | 1439 | CL-USER> (mapcar #'(lambda (x) (+ 1 x)) (list 1 2 3 4 5 6 7 8 9)) 1440 | (2 3 4 5 6 7 8 9 10) 1441 | CL-USER> 1442 | 1443 | 如果后续参数是两个长度相同的列表,它也可以把这两个长度相同的数字列表中的每个元素相加求和,把所有的和作为新列表中的元素,然后返回新列表: 1444 | 1445 | CL-USER> (mapcar #'+ (list 1 2 3 4 5 6 7 8 9) (list 2 3 4 5 6 7 8 9 10)) 1446 | (3 5 7 9 11 13 15 17 19) 1447 | CL-USER> 1448 | 1449 | 看了这两个例子大家想必都清楚了:**mapcar** 的第一个参数是一个函数,后续的参数类型由这个被调用的函数决定。 1450 | 1451 | 【小作业】试着用 **mapcar** 把一个数字列表中所有元素求和,然后返回和值: 1452 | 1453 | 开始编写这个程序时我们输入的第一条关于王国维的 《人间词话》的记录,那个价格 100 其实是不确切的,现在我们查到了正确的价格,是 24 ,希望能修改一下数据库,正好试试我们刚写好的 **更新** 函数,执行效果如下: 1454 | 1455 | CL-USER> (更新 (筛选条件 :作者 "王国维") :价格 24) 1456 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 "25" :是否有电子版 "n") 1457 | (:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 1458 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 1459 | (:书名 "人间词话" :作者 "王国维" :价格 24 :是否有电子版 T)) 1460 | CL-USER> 1461 | 1462 | 虽然执行之后的返回结果已经显示成功修改了数据库,不过我们还是可以再用 **查找** 函数单独看一下: 1463 | 1464 | CL-USER> (查找 (筛选条件 :作者 "王国维")) 1465 | ((:书名 "人间词话" :作者 "王国维" :价格 24 :是否有电子版 T)) 1466 | CL-USER> 1467 | 1468 | 显示没问题,我们已经成功修改了这条记录的价格字段的内容!再次庆祝一下! 1469 | 1470 | 顺便再写一个删除记录的函数 **删除记录** 1471 | 1472 | (defun 删除记录 (根据?查找函数) 1473 | (setf *书籍数据库* 1474 | (remove-if 根据?查找函数 *书籍数据库*))) 1475 | 1476 | 这里使用了一个跟函数 **remove-if-not** 形式类似的一个函数 **remove-if** ,在它所返回的列表中,所有匹配谓词的元素都被删除。 1477 | 1478 | 有的朋友可能会拿这个 **删除记录** 函数跟上一个 **更新** 函数进行比较,发现 **删除记录** 函数的形参中没有那些关键字形参,分别如下: 1479 | 1480 | (defun 更新记录 (根据?查找函数 &key 书名 作者 价格 (是否有电子版 nil 是否有电子版-p)) 1481 | 1482 | (defun 删除记录 (根据?查找函数) 1483 | 1484 | 比较一下这两个函数的实际调用代码就清楚了: 1485 | 1486 | (更新记录 (筛选条件 :作者 "王国维") :价格 24) 1487 | 1488 | (删除记录 (筛选条件 :作者 "王国维")) 1489 | 1490 | 前者需要输入两次关键字形参,第一次是为 **筛选条件** 函数准备的,用来筛选出符合条件的记录,第二次是为更新内容准备的,用来取代记录中原来的值。 1491 | 1492 | 后者只需要输入一次关键字形参,而且被包装在 **筛选条件** 函数里了,不需要在函数 **删除记录** 的定义中出现,因为你调用 **筛选条件** 时自然会输入它需要的关键字形参。 1493 | 1494 | 下意识地觉得这个 **删除记录** 的函数是一个危险的函数,尤其当它的 **筛选条件** 不带任何参数的时候,所以我们在试验这个函数之前,先把我们辛辛苦苦输入的数据库保存一下,就用我们前面完成的函数 **保存数据库** ,代码如下: 1495 | 1496 | CL-USER> (保存数据库 "~/ecode/markdown-doc/book-db.txt") 1497 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 "25" :是否有电子版 "n") 1498 | (:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 1499 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 1500 | (:书名 "人间词话" :作者 "王国维" :价格 24 :是否有电子版 T)) 1501 | CL-USER> 1502 | 1503 | 做好了万全的准备,开始试验新的危险函数,先不带任何筛选条件试试,如下: 1504 | 1505 | CL-USER> (删除记录 (筛选条件)) 1506 | NIL 1507 | CL-USER> *书籍数据库* 1508 | NIL 1509 | CL-USER> 1510 | 1511 | 果然预感成真,内存里的全局变量 **\*书籍数据库\*** 的内容被彻底清空了,好在我们有文件备份,先把它恢复,如下: 1512 | 1513 | CL-USER> (加载数据库 "~/ecode/markdown-doc/book-db.txt") 1514 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 "25" :是否有电子版 "n") 1515 | (:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 1516 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 1517 | (:书名 "人间词话" :作者 "王国维" :价格 24 :是否有电子版 T)) 1518 | CL-USER> *书籍数据库* 1519 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 "25" :是否有电子版 "n") 1520 | (:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 1521 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 1522 | (:书名 "人间词话" :作者 "王国维" :价格 24 :是否有电子版 T)) 1523 | CL-USER> 1524 | 1525 | 非常好,数据库信息又全部恢复了,这次再尝试一下带筛选条件删除记录,就删除 **:作者** 是 **"王国维"** 的记录,如下: 1526 | 1527 | CL-USER> (删除记录 (筛选条件 :作者 "王国维")) 1528 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 "25" :是否有电子版 "n") 1529 | (:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 1530 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T)) 1531 | CL-USER> (查找 (筛选条件 :作者 "王国维")) 1532 | NIL 1533 | CL-USER> 1534 | 1535 | 很好,干净利落地删掉了这条记录,函数的基本测试通过,收工,准备下一节,即将登场的可是 Common Lisp 的一个非常重要的特性---宏! 1536 | 1537 | 1538 | ### [9、再次迭代:用宏来消除重复](id:4.9) 1539 | 1540 | 太激动人心了,终于写到最后一节,实际上我们这个小型的藏书数据库程序已经基本完成了,在正式开始本节内容前先把我们写过的代码做一个简单的回顾,全部的代码如下: 1541 | 1542 | (defun 建立书籍信息 (书名 作者 价格 是否有电子版) 1543 | (list :书名 书名 :作者 作者 :价格 价格 :是否有电子版 是否有电子版)) 1544 | 1545 | (defvar *书籍数据库* nil) 1546 | 1547 | (defun 增加记录 (书籍信息) 1548 | (push 书籍信息 *书籍数据库*)) 1549 | 1550 | (defun 转储显示 () 1551 | (dolist (单条书籍记录 *书籍数据库*) 1552 | (format t "~{~a: ~20t~a~%~}~%" 单条书籍记录))) 1553 | 1554 | (defun 提示输入 (提示信息) 1555 | (format *query-io* "~a: " 提示信息) 1556 | (force-output *query-io*) 1557 | (read-line *query-io*)) 1558 | 1559 | (defun 提示书籍信息-旧版 () 1560 | (建立书籍信息 1561 | (提示输入 "书名") 1562 | (提示输入 "作者") 1563 | (提示输入 "价格") 1564 | (提示输入 "是否有电子版[y/n]"))) 1565 | 1566 | (defun 提示书籍信息 () 1567 | (建立书籍信息 1568 | (提示输入 "书名") 1569 | (提示输入 "作者") 1570 | (or (parse-integer (提示输入 "价格") :junk-allowed t) 0) 1571 | (y-or-n-p "是否有电子版[y/n]: "))) 1572 | 1573 | (defun 批量输入 () 1574 | (loop (增加记录 (提示书籍信息)) 1575 | (if (not (y-or-n-p "还要继续输入下一本书籍的信息吗?[y/n]: ")) (return)))) 1576 | 1577 | 1578 | (defun 保存数据库 (带路径的保存文件名) 1579 | (with-open-file (文件绑定变量 带路径的保存文件名 1580 | :direction :output 1581 | :if-exists :supersede) 1582 | (with-standard-io-syntax 1583 | (print *书籍数据库* 文件绑定变量)))) 1584 | 1585 | (defun 加载数据库 (带路径的加载文件名) 1586 | (with-open-file (文件绑定变量 带路径的加载文件名) 1587 | (with-standard-io-syntax 1588 | (setf *书籍数据库* (read 文件绑定变量))))) 1589 | 1590 | (defun 用作者名查找 (作者名) 1591 | (remove-if-not 1592 | #'(lambda (单条记录) 1593 | (equal (getf 单条记录 :作者) 作者名)) 1594 | *书籍数据库*)) 1595 | 1596 | (defun 查找 (根据?查找函数) 1597 | (remove-if-not 1598 | 根据?查找函数 1599 | *书籍数据库*)) 1600 | 1601 | (defun 选择器-选作者 (作者) 1602 | #'(lambda (单条记录) 1603 | (equal (getf 单条记录 :作者) 作者))) 1604 | 1605 | (defun 筛选条件 (&key 书名 作者 价格 (是否有电子版 nil 是否有电子版-p)) 1606 | #'(lambda (单条记录) 1607 | (and 1608 | (if 书名 1609 | (equal (getf 单条记录 :书名) 书名) t) 1610 | (if 作者 1611 | (equal (getf 单条记录 :作者) 作者) t) 1612 | (if 价格 1613 | (equal (getf 单条记录 :价格) 价格) t) 1614 | (if 是否有电子版-p 1615 | (equal (getf 单条记录 :是否有电子版) 是否有电子版) t)))) 1616 | 1617 | (defun 更新记录 (根据?查找函数 &key 书名 作者 价格 (是否有电子版 nil 是否有电子版-p)) 1618 | (setf *书籍数据库* 1619 | (mapcar 1620 | #'(lambda (单条记录) 1621 | (when (funcall 根据?查找函数 单条记录) 1622 | (if 书名 1623 | (setf (getf 单条记录 :书名) 书名)) 1624 | (if 作者 1625 | (setf (getf 单条记录 :作者) 作者)) 1626 | (if 价格 1627 | (setf (getf 单条记录 :价格) 价格)) 1628 | (if 是否有电子版-p 1629 | (setf (getf 单条记录 :是否有电子版) 是否有电子版))) 1630 | 单条记录) 1631 | *书籍数据库*))) 1632 | 1633 | (defun 删除记录 (根据?查找函数) 1634 | (setf *书籍数据库* 1635 | (remove-if 根据?查找函数 *书籍数据库*))) 1636 | 1637 | 不知不觉中已经写了这么多代码,真有成就感啊! 1638 | 1639 | 原型系统已经完成了,现在就在原型系统的基础上针对我们已经完成的程序做进一步的分析和优化: 1640 | 1641 | 前面在写函数 **筛选条件** 时,我们为了避免每针对一个字段都写一个对应的选择器函数而做了一些有益的抽象,写了一个选择器生成器函数 **筛选条件** ,避免了一定程度的代码重复,但是在 **筛选条件** 的代码中实际上还是不可避免地出现不少重复,我们必须为所有打算列为筛选条件的字段都写一条类似的语句放在 **筛选条件** 的函数体中,如下: 1642 | 1643 | (if 书名 (equal (getf 单条记录 :书名) 书名) t) 1644 | 1645 | 如果有 100 个准备列为筛选条件的字段就需要写出 100 条这样的语句,如下: 1646 | 1647 | (if 字段1 (equal (getf 单条记录 :字段1) 字段1) t) 1648 | (if 字段2 (equal (getf 单条记录 :字段2) 字段2) t) 1649 | (if 字段3 (equal (getf 单条记录 :字段3) 字段3) t) 1650 | (if 字段4 (equal (getf 单条记录 :字段4) 字段4) t) 1651 | 。。。。。 1652 | (if 字段100 (equal (getf 单条记录 :字段100) 字段100) t) 1653 | 1654 | 是不是发现我们前面所做的抽象还不是很彻底?这样的代码不仅会造成重复,而且在编译之后的执行代码中会产生多条无用的分支判断---你写多少个 **if** 它就会生成多少个分支判断,哪怕你最终调用时只带一个筛选参数,它也会把其余 99 条分支判断一一遍历。 1655 | 1656 | 也就是说我们花了太多力气去检查用户是否输入某个关键字形参。 1657 | 1658 | 这种无用的分支判断带来的不仅是代码的重复,更有性能上的损失,当然体现我们这个小程序上可能前后性能差异很小,不过我们一会儿可以稍微度量一下,Common Lisp 提供了简单的性能分析函数 **time** 可以用来做这种对比,真正对性能感兴趣的朋友也可以用不同方式试着写一个 100 个字段的数据库,比较一下。 1659 | 1660 | 言归正传,现在开始考虑如何把 **筛选条件** 做得更抽象一些,只生成我们实际执行的代码,根本不去生成那些可能执行、但是没有执行的代码。 1661 | 1662 | 让我们从用户调用函数 **筛选条件** 时输入的调用形式入手看看,用户可能会输入如形式的调用代码: 1663 | 1664 | (查找 (筛选条件 :作者 "王国维" :是否有电子版 T)) 1665 | 1666 | 我们目前已经实现的代码是这样的: 1667 | 1668 | (查找 1669 | #'(lambda (单条记录) 1670 | (and 1671 | (if 书名 1672 | (equal (getf 单条记录 :书名) 书名) t) 1673 | (if 作者 1674 | (equal (getf 单条记录 :作者) 作者) t) 1675 | (if 价格 1676 | (equal (getf 单条记录 :价格) 价格) t) 1677 | (if 是否有电子版-p 1678 | (equal (getf 单条记录 :是否有电子版) 是否有电子版) t)))) 1679 | 1680 | 但是我们实际只需要执行这样的代码即可: 1681 | 1682 | (查找 1683 | #'(lambda(单条记录) 1684 | (and 1685 | (equal (getf 单条记录 :书名) 书名) 1686 | (equal (getf 单条记录 :是否有电子版) t)))) 1687 | 1688 | 对比发现,后者比前者少了 4 条 if 判断,而且代码的处理逻辑看起来也更清晰了。 1689 | 1690 | 很好,我们希望每次都能根据用户输入的筛选字段来生成必要的代码,而不是把所有的可能性都一一列举,然后傻乎乎地一个分支一个分支跑一遍。 1691 | 1692 | 这个优化目标如果在 C 语言里提出,我觉得实现起来会比较困难,可能为此编写的辅助代码都要大大超过我们整个程序了,没准你为此增加的辅助代码可以编一个小型专用编译器出来了---也可能因为我自己的 C 语言水平比较有限,反正我暂时想不出什么既简单又有效的 C 算法,当然这么比较可能确实对 C 不太公平,C++倒是可以考虑考虑。 1693 | 1694 | 注意了,这里 Common Lisp 的一个非常非常重要的特性终于在万众瞩目中登场了--- **宏 Macro**,毫不夸张地说,我学习 Common Lisp 有一多半的原因就是因为它的宏,强大到逆天的能力! 1695 | 1696 | 什么叫强大到逆天的能力?我们知道,计算机程序语言中,天大地大,规则最大,所有的程序语言都要服从它们的语法规则,否则编译器就直接把你咔嚓掉了,根本没机会运行,也没法运行。 1697 | 1698 | 但是 Common Lisp 却不一样,因为它有 **宏** ,Common Lisp 的 **宏** 赋予程序员改写规则的能力,所有的 Common Lisp 程序员都可以按照自己的想法去创造自己的规则!就好比世间万物都要进入生死轮回,你却掌握了生死簿,可以逆天改命! 1699 | 1700 | 其实说实话,有些人真要用汇编或者 C 去实现这样的功能,也不是不可能,只不过没那么方便而已,而且当你真的成功了,你会发现,你自己写了一个 Common Lisp 的新实现 :) 1701 | 1702 | 写到这里,大家可以把前面的 8 节内容都看做是专门为这一节而做的铺垫,我也会尽量用我自己的理解来讲述 **宏** 这一利器,我个人的看法: 1703 | 1704 | 对于初学者来说,Common Lisp 的其他特性可以暂时放着,慢慢去熟悉,但是 宏 一定要从开始就理解、就学会,然后再在不断的编程实践中去运用,这样才会真正改变你的编程思维! 1705 | 1706 | 我们知道,编程语言的发展,其实就是抽象程度被不断提升的过程,从机器语言到汇编语言还只是简单的从机器指令到助记符的对应,但是很快宏汇编就开始提出抽象,各种高级语言分别提供各种不同角度、不同层次的抽象能力。 1707 | 1708 | 所谓的抽象就是提取那些共性的东西,然后用一种通用的形式去表述,Common Lisp 中的 **宏** 机制的本质也是如此,所以我们也没有必要把它看得有多么艰难,被它吓住,只要是程序中发现有共性的代码,都可以以各种形式抽象成 **宏**,最简单的就是内容重复的代码,这个很好判断,我们这次打算优化的内容就是这种类型的代码。 1709 | 1710 | 很容易看出,我们的 **筛选条件** 函数中最多的重复就是这种代码,如下: 1711 | 1712 | (equal (getf 单条记录 :书名) 书名) 1713 | 1714 | 抽象一下就是: 1715 | 1716 | (equal (getf 单条记录 字段名) 字段值) 1717 | 1718 | 先做成最简单的抽象---函数化,我们把它编写成一个根据输入字段名和字段值返回表达式的函数,因为表达式本身只是列表,因此可以先构思成这样的伪码: 1719 | 1720 | (defun 域值->表达式 (域 值) 1721 | (list equal (list getf 单条记录 域) 值) 1722 | 1723 | 说明一下,这个定义使用的语法是错误的,因为 Common Lisp 遇到 **域**、**值** 这种符号形式没有出现在列表首位时会去求值,这个没问题,因为你在实际调用时会把实际的参数值传给 **域** 和 **值** 这两个形参,但是对于列表中出现的其他类似的符号形式,如 **equal**、**getf**、**单条记录**,它也会去求值,这就麻烦了,马上就会出错,不信你就把这段代码拷贝到 REPL 中去执行一下,看看结果如何。 1724 | 1725 | 不过我们在前面的基本概念中学过:防止 Common Lisp 对一个符号求值的办法就是在符号前面加一个单引号 **'** 所以真正行得通的代码如下: 1726 | 1727 | (defun 域值->表达式 (域 值) 1728 | (list 'equal (list 'getf '单条记录 域) 值) 1729 | 1730 | 一般来说,写这种代码,只有函数的形参会希望被求值,其他的符号都不希望会求值,也就是说除了形参,其他符号都需要加一个单引号,是不是觉得很麻烦? 1731 | 1732 | 还好我们还有一种反过来的方法---先设置对整个表达式不求值,然后再设置对少数几个符号求值,这就是反引号 **`**(键盘位置:ESC键下方,TAB键上方,数字键1的左方)的作用,在一个表达式前面放一个反引号可以避免对整个表达式求值,在表达式中的子表达式前放一个逗号 **,** 可以只让该子表达式求值,因此可以写成更好的形式,把 **list** 函数也去掉了,如下: 1733 | 1734 | (defun 域值->表达式 (域 值) 1735 | `(equal (getf 单条记录 ,域) ,值) 1736 | 1737 | 执行效果如下: 1738 | 1739 | CL-USER> (域值->表达式 :作者 "王国维") 1740 | (EQUAL (GETF 单条记录 :作者) "王国维") 1741 | CL-USER> 1742 | 1743 | 很好,跟我们设想的一模一样,不过有个小问题,实际调用 **筛选条件** 函数时输入的形参可能不止这一对,所以我们这里需要一个函数,它能够从一个列表中成对地提取元素,分别作为 **域** 和 **值** 来使用,并且需要收集在每对参数上调用 **域值->表达式** 函数生成的结果,最后再把这些结果用一个 **and** 函数封装起来,这样就实现了对 **筛选条件** 函数的全面改写。 1744 | 1745 | 实现刚才提到的这些功能需需要使用一点新知识 **loop** 宏,先使用再解释,代码如下: 1746 | 1747 | (defun 域值->列表 (域值参数对列表) 1748 | (loop while 域值参数对列表 1749 | collecting (域值->表达式 (pop 域值参数对列表) (pop 域值参数对列表)))) 1750 | 1751 | 执行效果如下: 1752 | 1753 | CL-USER> (域值->列表 '(:作者 :书名)) 1754 | ((EQUAL (GETF 单条记录 :作者) :书名)) 1755 | CL-USER> (域值->列表 '(:作者 "王国维" :书名 "人间词话")) 1756 | ((EQUAL (GETF 单条记录 :作者) "王国维") (EQUAL (GETF 单条记录 :书名) "人间词话")) 1757 | CL-USER> 1758 | 1759 | 非常好,距离我们的目标又近了一步,现在要做的就是把函数 **域值->列表** 返回的列表用 **and** 函数封装起来,具体来说就是把它构造出来的所有 **equal** 语句都用 **and** 组装起来,最后再放入一个匿名函数中,代码如下: 1760 | 1761 | (defmacro 筛选条件 (&rest 域值参数对列表) 1762 | `#'(lambda (单条记录) 1763 | (and ,@(域值->列表 域值参数对列表)))) 1764 | 1765 | 1766 | 为避免跟前面定义过的同名函数发生冲突,建议在编译前先把前面的 **筛选条件** 函数改名为 **筛选条件-函数版本** 1767 | 1768 | 这里初步解释一下 Common Lisp 的 **宏** 的一些基础知识---凭借这些基础知识可以实现非常强悍的抽象功能。 1769 | 1770 | Common Lisp 的 **宏** 和我们曾经学过的 C 语言的 **宏** 是两个完全不同的概念,后者只是一些简单的替换。 1771 | 1772 | 首先是符号 **,@** 它会把紧挨着它的列表表达式的括号去掉,并把这个列表表达式的元素插入到外围的列表中,《实用 Common Lisp 编程》中的表述为: **,@** 会将接下来的表达式(必须求值成一个列表)的值嵌入到其外围的列表里,看看例子就明白了: 1773 | 1774 | CL-USER> `(and ,(list 1 2 3)) 1775 | (AND (1 2 3)) 1776 | CL-USER> `(and ,@(list 1 2 3) 4 5) 1777 | (AND 1 2 3 4 5) 1778 | CL-USER> 1779 | 1780 | **宏** 的另一个重要基础知识是剩余形参符号 **&rest** ,当参数列表里带有 **&rest** 时,一个函数或宏可以接受任意数量的实参,所有这些实参都将被收集到一个列表中,并且会成为那个 **&rest** 后面的参数所对应的变量的值成。 1781 | 1782 | 还有一个必须提到的关于 **宏** 的函数 **macroexpand-1** 它会把一个宏调用展开,也就是说它执行的参数是一个合法的宏调用,包括宏名和必须的参数,它返回的结果正是这个宏未来执行时所生成的实际代码,我们完成一个新的 **宏** 定义之后,想要看看它是否能按我们预期的方式工作,就可以用这个函数来检查。 1783 | 1784 | 执行效果如下: 1785 | 1786 | CL-USER> (macroexpand-1 '(筛选条件 :作者 "王国维" :书名 "人间词话")) 1787 | #'(LAMBDA (单条记录) 1788 | (AND 1789 | (EQUAL (GETF 单条记录 :作者) "王国维") 1790 | (EQUAL (GETF 单条记录 :书名) "人间词话"))) 1791 | T 1792 | CL-USER> 1793 | 1794 | 看起来不错,用到我们的 **查找** 函数中实际试一下, 如下: 1795 | 1796 | CL-USER> (查找 (筛选条件 :作者 "王国维")) 1797 | NIL 1798 | CL-USER> 1799 | 1800 | 怎么没查到?难道写错了?看看数据库的数据 1801 | 1802 | CL-USER> *书籍数据库* 1803 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 "25" :是否有电子版 "n") 1804 | (:书名 "难忘的书与插图" :作者 "汪家 明" :价格 38 :是否有电子版 T) 1805 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T)) 1806 | CL-USER> 1807 | 1808 | 哦,原来那条记录被我们删除了,换个筛选条件试试 1809 | 1810 | CL-USER> (查找 (筛选条件 :作者 "老鬼")) 1811 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 "25" :是否有电子版 "n")) 1812 | CL-USER> 1813 | 1814 | 大功告成!我们成功地实现了根据用户输入的具体筛选条件动态生成执行代码的抽象,避免了一大堆无用的分支。 1815 | 1816 | 不过且慢,虽然宏 **筛选条件** 实现了高度抽象,好像函数 **更新记录** 也存在类似的问题,目前只能更新 4 个指定字段,如果字段一多就要写很多对应的分支,让我们继续沿用刚才的分析方法,针对 **更新记录** 再做一次抽象优化,这也是我们对自己所学内容的一个尝试和检验。 1817 | 1818 | 分析的顺序依旧是自底向上,先模拟一个用户输入的函数调用场景,假设语句如下: 1819 | 1820 | (更新 (筛选条件 :作者 "老鬼") :价格 55 :是否有电子版 T) 1821 | 1822 | 那么对应的实际执行的代码如下: 1823 | 1824 | (更新记录 (筛选条件 ) 更新字段 更新值 1825 | (setf *书籍数据库* 1826 | (mapcar 1827 | #'(lambda (单条记录) 1828 | (when (funcall 筛选条件 单条记录) 1829 | (progn 1830 | (setf (getf 单条记录 :价格) 55) 1831 | (setf (getf 单条记录 :是否有电子版) T)) 1832 | 单条记录) 1833 | *书籍数据库*))) 1834 | 1835 | 同样地,先抽象一个 **更新域值->表达式** 的辅助函数出来,如下: 1836 | 1837 | (defun 更新域值->表达式 (域 值) 1838 | `(setf (getf 单条记录 ,域) ,值)) 1839 | 1840 | 执行效果如下: 1841 | 1842 | CL-USER> (更新域值->表达式 :价格 55) 1843 | (SETF (GETF 单条记录 :价格) 55) 1844 | CL-USER> 1845 | 1846 | 很好,再抽象一个可以处理域值参数对列表的辅助函数出来,如下: 1847 | 1848 | (defun 更新域值->列表 (域值参数对列表) 1849 | (loop while 域值参数对列表 1850 | collecting (更新域值->表达式 (pop 域值参数对列表) (pop 域值参数对列表)))) 1851 | 1852 | 执行效果如下: 1853 | 1854 | CL-USER> (更新域值->列表 '(:价格 55 :是否有电子版 T)) 1855 | ((SETF (GETF 单条记录 :价格) 55) (SETF (GETF 单条记录 :是否有电子版) T)) 1856 | CL-USER> 1857 | 1858 | 非常好,现在开始构造我们的宏 **更新记录** ,代码如下: 1859 | 1860 | (defmacro 更新记录 (根据?查找函数 &rest 待更新域值对列表) 1861 | `(setf *书籍数据库* 1862 | (mapcar 1863 | #'(lambda (单条记录) 1864 | (when (funcall ,根据?查找函数 单条记录) 1865 | (progn ,@(更新域值->列表 待更新域值对列表)) 1866 | 单条记录) 1867 | *书籍数据库*)))) 1868 | 1869 | 执行效果如下: 1870 | 1871 | CL-USER> (macroexpand-1 '(更新记录 (筛选条件 :作者 "老鬼") :价格 55 :是否有电子版 t)) 1872 | (SETF *书籍数据库* 1873 | (MAPCAR #'(LAMBDA (单条记录) 1874 | (WHEN (FUNCALL (筛选条件 :作者 "老鬼") 单条记录) 1875 | (PROGN (SETF (GETF 单条记录 :价格) 55) 1876 | (SETF (GETF 单条记录 :是否有电子版) T)) 1877 | 单条记录)) 1878 | *书籍数据库*)) 1879 | T 1880 | CL-USER> 1881 | 1882 | 试验一下效果如何: 1883 | 1884 | CL-USER> (更新记录 (筛选条件 :作者 "老鬼") :价格 55) 1885 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 55 :是否有电子版 "n") NIL NIL) 1886 | CL-USER> *书籍数据库* 1887 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 55 :是否有电子版 "n") NIL NIL) 1888 | CL-USER> 1889 | 1890 | 坏了,这条记录是更新了,但是其他另外两条记录却消失了,看来有些地方出错了,很显然,函数 **mapcar** 返回了这样的结果 1891 | 1892 | (待更新记录 nil nil) 1893 | 1894 | 那么它为什么要把其余两条不满足筛选条件的记录设置为空值呢?看来问题还是出在匿名函数,经过检查,发现有一个括号位置搞错了,正确的代码应如下: 1895 | 1896 | (defmacro 更新记录 (根据?查找函数 &rest 待更新域值对列表) 1897 | `(setf *书籍数据库* 1898 | (mapcar 1899 | #'(lambda (单条记录) 1900 | (when (funcall ,根据?查找函数 单条记录) 1901 | (progn ,@(更新域值->列表 待更新域值对列表))) 1902 | 单条记录) 1903 | *书籍数据库*))) 1904 | 1905 | 再次试验一下,先展开,检查展开形式,一切正常: 1906 | 1907 | CL-USER> (macroexpand-1 '(更新记录 (筛选条件 :作者 "王国维") :价格 55 :是否有电子版 t)) 1908 | (SETF *书籍数据库* 1909 | (MAPCAR #'(LAMBDA (单条记录) 1910 | (WHEN (FUNCALL (筛选条件 :作者 "王国维") 单条记录) 1911 | (PROGN (SETF (GETF 单条记录 :价格) 55) 1912 | (SETF (GETF 单条记录 :是否有电子版) T))) 1913 | 单条记录) 1914 | *书籍数据库*)) 1915 | T 1916 | CL-USER> 1917 | 1918 | 再执行一次更新操作,结果如下: 1919 | 1920 | CL-USER> (更新记录 (筛选条件 :作者 "老鬼") :价格 55) 1921 | ((:书名 "血色黄昏" :作者 "老鬼" :价格 55 :是否有电子版 "n") 1922 | (:书名 "难忘的书与插图" :作者 "汪家明" :价格 38 :是否有电子版 T) 1923 | (:书名 "说文解字" :作者 "许慎" :价格 100 :是否有电子版 T) 1924 | (:书名 "人间词话" :作者 "王国维" :价格 24 :是否有电子版 T)) 1925 | CL-USER> 1926 | 1927 | 这次好了,没有任何问题,现在我们终于可以说大功告成了! 1928 | 1929 | 其实那个函数 **progn** 不是必须的,可以去掉,还可以少些括号,减少犯错误的几率,写成如下的形式: 1930 | 1931 | (defmacro 更新记录 (根据?查找函数 &rest 待更新域值对列表) 1932 | `(setf *书籍数据库* 1933 | (mapcar 1934 | #'(lambda (单条记录) 1935 | (when (funcall ,根据?查找函数 单条记录) 1936 | ,@(更新域值->列表 待更新域值对列表)) 1937 | 单条记录) 1938 | *书籍数据库*))) 1939 | 1940 | 现在经过我们再次抽象定义出来的新**宏** **更新记录** ,不仅消除了重复代码,避免执行空分支的判断,而且可以更新任意字段,不论数据库的字段怎么调整,我们的代码都不需要做任何修改就能够适应。而这正是我不惜花费大量篇幅希望能让初学者领略到的东西,经过最后对两个函数 **筛选条件** 和 **更新记录** 的重写,我们初步体会到 Common Lisp 语言强大的 **宏** , 1941 | 1942 | 【程序扩展】 1943 | 1944 | 有些人比较喜欢更深入的探索,比如这段程序,经过我们的一再优化,基本的功能已经实现了,不过还有不少方向值得去扩展,下面我列两个出来,供感兴趣的朋友研究参考: 1945 | 1946 | 1、对数据库字段的扩充,可以增加我们一开始讨论时设想到的那些字段; 1947 | 2、和智能终端结合,现在的智能手机都有条码扫描的功能,而且提供了相应的操作函数,每本书都有一个 ISBN 条码,根据 1948 | 这个条码可以在网络上获取该书的很多信息,这样就不需要我们一一输入了,能减轻很多工作量; 1949 | 1950 | 1951 | ### [10、本章内容小结](id:4.10) 1952 | 1953 | * 简单函数 1954 | * 非常基本的数据结构:**列表** 的使用 1955 | * 以函数作为参数的函数 funcall mapcar 1956 | * 宏的抽象可以带来更紧凑、更高效、更通用的代码 1957 | 1958 | 1959 | ## [五、跨越初学者阶段](id:5) 1960 | 1961 | 经过上述 4 个章节的学习和实践,,如果前述每段例程代码你都完全理解了,我相信作为初学者的你,不仅可以顺利启动开发环境,学会不少提升开发、调试效率的快捷操作,而且也掌握了 Common Lisp 语言的一些基本概念和用法,以及部分高级内容---**宏**,应该具备继续深入探索 Common Lisp 其他特性的能力了,恭喜你! 1962 | 1963 | ### [1、其实说实话我也是初学者…](id:5.1) 1964 | 1965 | 其实我也是跟大家一样的初学者,开始只是想以教程的形式总结一下学过的内容,结果不知不觉把这个新手教程写了这么多,有不少细节在自己单独看书的时候其实没想那么多,但是一旦开始写教程的时候,才发现这些以前没有想过的地方,所以我觉得初学者在学习 Common Lisp 时,如果能尝试着把自己学到的内容以教程的形式写出来,这样不仅可以及时地对学过的内容进行总、复习,同时也可以自己在试着表述的过程中发现以往学习的疏漏,另外还可以让其他初学者多一份参考学习的资料。 1966 | 1967 | 因此疏漏、错误在所难免,希望能其他朋友能不吝赐教,指出谬误之处,我核实后会刷新版本,同时你的名字也会出现在 [**“贡献者列表”**](#6) 中。 1968 | 1969 | 一个人的力量终究是有限的,希望有更多的人共同参与进来 1970 | 1971 | ### [2、HyperSpec:Common Lisp 有哪些“标准库函数”?](id:5.2) 1972 | 1973 | 学过其他编程语言的朋友,在了解了 Common Lisp 初步知识后,肯定会有一个疑问:Common Lisp 有哪些 **“标准库函数”** ?如何去查询?这也是我当初的一个疑问,因为 Common Lisp 不这么叫,它的称呼是 “扩展符号”,写在 **HyperSpec** 中,由 **LispWorks** 维护,有在线版,也可以下载回去慢慢查,一共有 978 个,建议初学者把它下载回来经常翻翻,因为可以看到这些 **“标准库函数”** 的源代码,它们绝大多数也是用 Common Lisp 写的。 1974 | 1975 | HyperSpec 下载地址: http:// 1976 | 1977 | 这里再重复一遍最最简单的查看 Common Lisp 的 **“标准库函数”** 源代码的办法,当然,你得先知道函数名,在 REPL 或者在 编辑区 里输入函数名,然后把光标移动到函数名上面,按如下快捷键: 1978 | 1979 | M-. Alt 键 和 点键 . Emacs就会自动把该函数或宏的源文件打开 1980 | 1981 | 其实 Emacs 里还有不少快捷键可以查询某个函数相关的信息,不过我觉得那些帮助信息其实不如看代码清楚,所以就不多介绍了,感兴趣就自己去查吧。 1982 | 1983 | ### [3、如何查找各种具体实现(如SBCL、CCL、LispWorks)的帮助信息](id:5.3) 1984 | 1985 | 目前的 LispBox 里只使用了一种 Common Lisp 实现 Clozure CL 作为编译器,但是其他实现也各有其独到之处,比如 SBCL,比如 LispWorks,还有 Allegro CL,这些不同实现在其网站都有相关的文档,包括了最权威的帮助信息。 1986 | 各实现官网地址如下: 1987 | Clozure CL: http://ccl.clozure.com/ 1988 | SBCL: http://www.sbcl.org 1989 | LispWorks: http://www.lispworks.com/ 1990 | Allegro CL: http://www.franz.com/products/allegrocl/ 1991 | 1992 | 1993 | 1994 | 如果遇到问题,首选是看文档,看看官方的 FAQ 里有没有你的问题,其次是使用搜索引擎看看有没有其他人遇到类似问题,如果这两步都没有找到相关的答案,那就到 Email-List 或者论坛去提问,不过个人感觉 Email-List 上高人更集中,毕竟你不可能把所有的论坛都逛遍,我有几个问题就是在 Email-List 求助,然后得到其他朋友的帮助而解决的。 1995 | 1996 | 在 Email-List 上求助一定要详细描述你的问题,最好同时把你搜索答案的过程和结果也描述一下,否则别人就算想帮你也无从下手,另外就是一些简单的问题、或者自己可以通过搜索找到答案的问题就不要在 Email-List 上提了,自己从来不肯动脑子、只知道要完整解决方案的伸手党是最不受欢迎的。 1997 | 1998 | 最后就是希望所有的初学者都能养成帮助别人的习惯,高手、新手是相对的,你从一个高手那里获得了帮助,同时有些比你学习晚的新手也可以从你这里得到帮助,这样正能量才能流通起来,最终受益的是网络上的每个使用者---也就是你我。 1999 | 2000 | 2001 | ### [4、更深一步的学习](id:5.4) 2002 | 2003 | 如果你认真地把这份教程从头读到尾,而且自己验证了其中所有的例程,也理解了所有的代码,那你就可以开始更进一步的学习了,具体的建议我就不提了,只提几个原则性的: 2004 | 2005 | * 1)、以自己的兴趣为主,对哪个方面感兴趣就先学哪个方面的; 2006 | * 2)、学了什么内容一定要试着写写相关的代码,用写代码来验证你是否真正掌握; 2007 | * 3)、阅读学习的时间和写代码验证的时间至少为 1:1 (这是我对自己粗略的估计,因人而异,不过一定要保证构思代码、编写代码的时间); 2008 | * 4)、学习过程最好能有所记录---包括犯的错误; 2009 | * 5)、学完一个小阶段后要有所总结,而且最好能把自己的总结分享出来; 2010 | 2011 | ### [5、本章内容小结](id:5.5) 2012 | 2013 | * 貌似本章自己就是一个小结性质的章节 :) 2014 | 2015 | ## [六、贡献者列表](id:6) 2016 | 2017 | 因为本教程作者也是一名初学者,所以本文必然会有各种错漏,因此希望更多的初学者或高手能参与进来,共同完善此教程,此教程会放在 GitHub 上,方便大家查阅,这里专门开辟一个章节来列出参与发现错误的朋友,表示感谢! 2018 | 2019 | ### [1、修订记录](id:6.1) 2020 | 2021 | * 2013-02-17 初稿完成 2022 | * 2013-04-21 修改更新第三章 2023 | * 2013-05-11 修改更新第一章 2024 | * 2013-05-17 全面修改更新--作为发布版,版本号 V0.9 2025 | 2026 | ### [2、贡献者列表](id:6.2) 2027 | 2028 | 以参与先后顺序列出各位贡献者,先把我的名字列出来,做个表率 :) 2029 | 2030 | * 贡献者: 2031 | * FreeBlues 2032 | 2033 | --------------------------------------------------------------------------------