├── .gitignore ├── face.jpg ├── assets ├── images │ ├── cover.jpg │ ├── emacs.jpg │ ├── woman.jpg │ ├── drracket.png │ ├── sublime.jpg │ ├── weixin10.jpg │ ├── hash_table.png │ ├── game-2048-win.png │ └── drracket-editor.jpg └── custom.css ├── docs ├── 02-basics │ ├── 03-module.scrbl │ ├── 04-oop.scrbl │ ├── index.scrbl │ ├── 01-grammar.scrbl │ └── 02-data.scrbl ├── 04-advanced-racket │ ├── 01-concepts.scrbl │ ├── 03-debug.scrbl │ ├── 04-look-into-executable.scrbl │ ├── index.scrbl │ └── 02-functional-programming.scrbl ├── 12-web.scrbl ├── 14-misc.scrbl ├── 05-plotting.scrbl ├── 06-scribble.scrbl ├── 11-server.scrbl ├── 10-continuation.scrbl ├── preface.scrbl ├── 07-package-system.scrbl ├── 09-advanced-macro.scrbl ├── 13-real-world.scrbl ├── 15-typed-racket.scrbl ├── 16-further-readings.scrbl ├── postscript.scrbl ├── 01-begin │ ├── index.scrbl │ ├── 02-hello-world.scrbl │ ├── 03-install.scrbl │ ├── 01-why-racket.scrbl │ ├── 04-first-impression.scrbl │ └── 05-dance-with-racket.scrbl ├── 03-practical-programs │ ├── index.scrbl │ ├── 01-editors.scrbl │ ├── 02-program-crop.scrbl │ └── 03-program-2048.scrbl └── 08-macro.scrbl ├── Makefile ├── README.md ├── code ├── shared │ └── utility.rkt └── my-2048.rkt ├── bin └── add-to-head.rkt ├── index.scrbl └── util └── common.rkt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | html/ 3 | tmp/ 4 | assets/highlight/ 5 | -------------------------------------------------------------------------------- /face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/face.jpg -------------------------------------------------------------------------------- /assets/images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/cover.jpg -------------------------------------------------------------------------------- /assets/images/emacs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/emacs.jpg -------------------------------------------------------------------------------- /assets/images/woman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/woman.jpg -------------------------------------------------------------------------------- /assets/images/drracket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/drracket.png -------------------------------------------------------------------------------- /assets/images/sublime.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/sublime.jpg -------------------------------------------------------------------------------- /assets/images/weixin10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/weixin10.jpg -------------------------------------------------------------------------------- /assets/images/hash_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/hash_table.png -------------------------------------------------------------------------------- /assets/images/game-2048-win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/game-2048-win.png -------------------------------------------------------------------------------- /assets/custom.css: -------------------------------------------------------------------------------- 1 | img.cover { 2 | width: 700px; 3 | } 4 | 5 | .strike { 6 | text-decoration: line-through; 7 | } 8 | -------------------------------------------------------------------------------- /assets/images/drracket-editor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/racket-book/HEAD/assets/images/drracket-editor.jpg -------------------------------------------------------------------------------- /docs/02-basics/03-module.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "basics-module"]{撰写模块} -------------------------------------------------------------------------------- /docs/04-advanced-racket/01-concepts.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "advanced-racket-concepts"]{基本概念} -------------------------------------------------------------------------------- /docs/04-advanced-racket/03-debug.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "advanced-racket-debug"]{调试Racket程序} -------------------------------------------------------------------------------- /docs/12-web.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "web"]{用Racket做Web开发} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/14-misc.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "misc"]{Racket的其它特性} 8 | 9 | 本章讲述slideshow 10 | -------------------------------------------------------------------------------- /docs/05-plotting.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "plotting"]{用Racket作图} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/06-scribble.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "scribble"]{用Racket写作} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/11-server.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "server"]{用Racket编写服务器程序} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/10-continuation.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "continuation"]{延迟计算} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/preface.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "preface"]{前言 - hello world!} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/07-package-system.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "package-system"]{Racket包管理} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/09-advanced-macro.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "advanced-macro"]{用宏来设计你的语言} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/13-real-world.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "real-world"]{使用Racket撰写复杂的系统} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/04-advanced-racket/04-look-into-executable.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "advanced-racket-executable"]{探秘打包后的可执行文件} -------------------------------------------------------------------------------- /docs/15-typed-racket.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "typed-racket"]{下一站:Typed Racket} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/16-further-readings.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "further-readings"]{下一步该学什么?} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/postscript.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "postscript"]{结语 - goodbye my friend} 8 | 9 | Hello world! 10 | -------------------------------------------------------------------------------- /docs/02-basics/04-oop.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "basic-oop"]{面向对象编程} 8 | 9 | 10 | 现在你已经对Racket的主要语法有所掌握,课后作业:@rh["http://learnxinyminutes.com/docs/zh-cn/racket-cn/" "阅读Learn X in Y minutes并尝试理解和执行其中涉及到的例子"]。Good luck! -------------------------------------------------------------------------------- /docs/02-basics/index.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "basics"]{Racket语言概要} 8 | 9 | @table-of-contents[] 10 | 11 | @; ------------------------------------------------- 12 | @include-section["01-grammar.scrbl"] 13 | @include-section["02-data.scrbl"] 14 | @include-section["03-module.scrbl"] 15 | @include-section["04-oop.scrbl"] 16 | -------------------------------------------------------------------------------- /docs/04-advanced-racket/index.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "advanced-racket"]{Racket语言进阶} 8 | 9 | @table-of-contents[] 10 | 11 | @; ------------------------------------------------- 12 | @include-section["01-concepts.scrbl"] 13 | @include-section["02-functional-programming.scrbl"] 14 | @include-section["03-debug.scrbl"] 15 | @include-section["04-look-into-executable.scrbl"] 16 | -------------------------------------------------------------------------------- /docs/01-begin/index.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "begin"]{开始} 8 | 9 | 10 | 11 | @table-of-contents[] 12 | 13 | @; ------------------------------------------------- 14 | @include-section["01-why-racket.scrbl"] 15 | @include-section["02-hello-world.scrbl"] 16 | @include-section["03-install.scrbl"] 17 | @include-section["04-first-impression.scrbl"] 18 | @include-section["05-dance-with-racket.scrbl"] 19 | -------------------------------------------------------------------------------- /docs/03-practical-programs/index.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "practical-racket"]{写点有意义的代码} 8 | 9 | 学一门语言,如果不是为了使用它,必不长久。技术不能是为了技术而钻研,应该是为了应用而钻研。这一章我们通过写点有意义的代码,一起探索Racket的世界。 10 | 11 | @table-of-contents[] 12 | 13 | @; ------------------------------------------------- 14 | @include-section["01-editors.scrbl"] 15 | @include-section["02-program-crop.scrbl"] 16 | @include-section["03-program-2048.scrbl"] 17 | -------------------------------------------------------------------------------- /docs/01-begin/02-hello-world.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "begin-hello"]{从Hello world开始} 8 | 9 | 让计算机输出 "Hello world" 基本上是一门语言入门的第一步,我们来看看C语言如何实现: 10 | 11 | @code-hl[#:lang "c"]{ 12 | #include 13 | 14 | int main(void) 15 | { 16 | printf("Hello world!\n"); 17 | return 0; 18 | } 19 | } 20 | 21 | Java略微复杂一些: 22 | 23 | @code-hl[#:lang "java"]{ 24 | public class HelloWorld { 25 | public static void main(String[] args) { 26 | System.out.println("Hello world"); 27 | } 28 | } 29 | } 30 | 31 | 而Python要简单得多: 32 | 33 | @code-hl[#:lang "python"]{ 34 | print "Hello world!" 35 | } 36 | 37 | 对于Racket来说,Hello world也仅仅需要一行代码: 38 | 39 | @rb[ 40 | (displayln "Hello world!") 41 | ] 42 | 43 | 就语言的表现力来说,Racket和Python这样动态执行的语言明显占了上风。 44 | -------------------------------------------------------------------------------- /docs/04-advanced-racket/02-functional-programming.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | 8 | @title[#:tag "advanced-racket-fp"]{函数式编程} 9 | 10 | 11 | 12 | @section[#:tag "advanced-racket-fp-func"]{高阶函数(High-ordered Function)} 13 | 14 | @section[#:tag "advanced-racket-fp-pure-func"]{纯函数(Pure Function)} 15 | 16 | @section[#:tag "advanced-racket-fp-closure"]{闭包} 17 | 18 | @section[#:tag "advanced-racket-fp-recursion"]{递归} 19 | 20 | @section[#:tag "advanced-racket-fp-curry"]{柯里化(Currying)} 21 | 22 | @rb[ 23 | (define rdoc (curry (lambda (libname name) (secref name #:doc libname)))) 24 | (define rdoc-ref (rdoc '(lib "scribblings/reference/reference.scrbl"))) 25 | (define rdoc-teachpack (rdoc '(lib "teachpack/teachpack.scrbl"))) 26 | ] 27 | 28 | @section[#:tag "advanced-racket-fp-lazy-eval"]{惰性求值(Lazy Evaluation)} -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SCRBL=index.scrbl 2 | ASSETS=assets 3 | HL_CSS=$(ASSETS)/highlight/github.css 4 | CUST_CSS=$(ASSETS)/custom.css 5 | HL_JS=$(ASSETS)/highlight/highlight.pack.js 6 | ADD_HEAD=bin/add-to-head.rkt 7 | 8 | .PHONY: local clean html html-single html-multi publish 9 | 10 | local: html 11 | 12 | html: html-multi 13 | racket $(ADD_HEAD) 14 | 15 | html-single: $(SCRBL) 16 | raco scribble \ 17 | --html \ 18 | --dest html \ 19 | --dest-name all.html \ 20 | ++style $(HL_CSS) \ 21 | ++extra $(HL_JS) \ 22 | ++style $(CUST_CSS) \ 23 | ++main-xref-in \ 24 | --redirect-main http://docs.racket-lang.org/ \ 25 | \ 26 | $(SCRBL) 27 | 28 | html-multi: $(SCRBL) 29 | raco scribble \ 30 | --htmls \ 31 | --dest-name html \ 32 | ++style $(HL_CSS) \ 33 | ++extra $(HL_JS) \ 34 | ++style $(CUST_CSS) \ 35 | ++main-xref-in \ 36 | --redirect-main http://docs.racket-lang.org/ \ 37 | \ 38 | $(SCRBL) 39 | 40 | deploy: 41 | cd html; make; cd ../.. 42 | 43 | publish: html deploy 44 | -------------------------------------------------------------------------------- /docs/08-macro.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../util/common.rkt") 6 | 7 | @title[#:tag "macro"]{可爱的宏} 8 | 9 | 有时候,一门语言的语法让你感觉不爽,如果能够做点微调,该多好啊?比如说Racket提供了 @r[lambda] 和 @r[λ] 来生成匿名函数,lambda对我来说太长,而λ在我的mbp的键盘上无法直接输入(不像ƒ这样的字符可以 @bold{ALT+f} 输入,而是需要打开greek语言),如果我想用 @bold{ƒ} 定义函数,该怎么做? 10 | 11 | 在Python中我还还真不知道该怎么做,也没这么想过,C可以用宏替换: 12 | 13 | @code-hl[#:lang "C"]{ 14 | #define ƒ λ 15 | } 16 | 17 | @margin-note{ 18 | 所以在C里,很多时候宏的写法需要特殊处理,比如说: 19 | @code-hl[#:lang "C"]{ 20 | #define MACRO(x, y) do { \ 21 | // macro body \ 22 | while(0) 23 | } 24 | 很丑很暴力。 25 | } 26 | } 27 | 28 | 不过我们知道,C的宏替换仅仅是预处理阶段基本不太考虑语法的情况下直接对目标做字符串替换,所以这么做有很多潜在的风险。那么Racket呢?能不能直接用 @r[define] 定义一下? 29 | 30 | @re[ 31 | (define ƒ λ) 32 | ] 33 | 34 | 好吧,语法错误。@r[define] 实际上是把后一个表达式求值然后绑定到 @r[_ƒ] 变量上,而这里 @r[_λ] 不是个可求值的表达式。都说lisp强大到可以生成任何编程语言,那么这么个小问题该怎么解决?简单: 35 | 36 | @re[ 37 | (define-syntax-rule (ƒ x y) 38 | (λ x y)) 39 | (define area (ƒ (r) (* pi r r))) 40 | (area 10) 41 | ] 42 | 43 | 这就是我们这一章要讲的宏。 -------------------------------------------------------------------------------- /docs/01-begin/03-install.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "begin-install"]{安装和运行Racket} 8 | 9 | @margin-note{REPL: Read Evaluate Print Loop,详情见 @rh["http://zh.wikipedia.org/zh/%E8%AF%BB%E5%8F%96%EF%B9%A3%E6%B1%82%E5%80%BC%EF%B9%A3%E8%BE%93%E5%87%BA%E5%BE%AA%E7%8E%AF" "wikipedia:REPL"]} 10 | 11 | 作为一门解释型语言,Racket标配一个REPL解释器;除此之外,它还提供一个非常强大的IDE:DrRacket。下载和安装Racket非常简单,在 @rh["http://download.racket-lang.org/" "官方下载页面"] 选择合适的操作系统下的版本,按提示安装即可。对于OSX的用户,安装后的程序位于 @bold{/Applications/Racket v6.1.1/} 下,如果想在shell下直接运行 @r[racket] 或者 @(drr),请将 @bold{/Applications/Racket v6.1.1/bin} 添加到 @r[$PATH] 下。 12 | 13 | 之后,可以运行 @(drr): 14 | 15 | @code-hl[#:lang "shell"]{ 16 | $ drracket 17 | } 18 | 19 | 你将会看到如下界面: 20 | 21 | @image["assets/images/drracket.png" #:scale 0.8] 22 | 23 | 正如你所看到的那样,@(drr) 允许你使用任意对象,包括图片。 24 | 25 | @(drr) 的窗口分为上下两个部分,上部是一个编辑器,用来输入大段代码的,可以通过点击工具栏上的 @bold{Run} 查看运行结果;下部是一个REPL解释器,可以即时输入代码,查看运行结果。当点击 @bold{Run} 时,REPL解释器将会刷新并显示编辑器里面代码的运行结果。本章接下来的小节,如无特殊说明,代码都是输入在 @(drr) 里的REPL解释器中。 26 | -------------------------------------------------------------------------------- /docs/03-practical-programs/01-editors.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "practical-editor"]{找个称手的编辑器} 8 | 9 | @(drr) 本身就是一个很棒的编辑器,可以用 @bold{ctrl+/} 自动补齐。在其右上角还有函数原型的提示,当你光标移动到某个函数上时,就会显示,非常方便。 10 | 11 | @image["assets/images/drracket-editor.jpg" #:style "cover"] 12 | 13 | 如果已经是Vim或者Emacs的用户,那么可以安装 @rh["https://github.com/wlangstroth/vim-racket" "对应的插件"]。由于Emacs本身和Lisp有着千丝万缕的联系,建议如果是撰写Racket代码,可以优先考虑Emacs。我自己比较喜欢的Emacs配置是 @rh["https://github.com/purcell/emacs.d" "purcell大神的配置"],你可以按照提示安装,打开Emacs后使用 @bold{M-x}(注:alt+x),输入 @bold{package-install},回车后,输入 @bold{racket-mode} 或者 @bold{geiser} 就可以安装Racket的插件了。当然,你也可以通过下载安装 @rh["https://github.com/greghendershott/racket-mode" "racket-mode"] 或者 @rh["http://www.nongnu.org/geiser/geiser_3.html" "Geiser"]。更多详情,可以参考 @rdoc-guide{Emacs}。 14 | 15 | @image["assets/images/emacs.jpg" #:style "cover"] 16 | 17 | 此外,sublime-text也是一个不错的文本编辑工具,它也提供了 @rh["https://sublime.wbond.net/packages/Racket" "对应的插件"]。 18 | 19 | @image["assets/images/sublime.jpg" #:style "cover"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Racket](assets/images/cover.jpg) 2 | 3 | ## 关于Racket book 4 | 5 | 这是一本关于racket的入门书。程序君在学习racket的过程中,发现racket的中文资料几乎为零,于是萌生了撰写这本书的想法。写这本书,某种程度上是出于私利,因为我一直认为最好的学习方法就是将自己学到的东西教授出去。在教授的过程中,自己能够学得更扎实。 6 | 7 | 于是,在学了racket也就一个周末之后,我开了这个repo,来记录和传授我学习racket的心得。撰写这本书,并不意味着我对racket的掌握有什么过人之处,恰恰相反,我和打算起步的你一样,不断挣扎于对这门语言的理解。 8 | 9 | 由于racket提供了 [scribble](http://docs.racket-lang.org/scribble/) 这门专门用于撰写文档的语言,所以这本书也一反我的习惯,没用 markdown 或 asciidoc 撰写,而是全部用 scribble 完成。使用scribble的体验很好,在这个过程中,它也激励我使用racket去解决一些实际的问题。是的,如果你浏览这个repo的源码,你会发现,racket并非一个「花瓶」语言,只能用于去理解一些高深的宏编程或者函数式编程的思想,而是一门很实用的工具,可以做几乎任何通用语言(如python)能做的事情。 10 | 11 | 由于本书面向初学者,所以,如果你顺着读下来发现有些概念或者知识没有解释清楚,请向我提出,以便我修订。文中出现的任何问题,也欢迎大家提bug。 12 | 13 | 你可以通过 https://tyrchen.github.io/racket-book 访问本书的最新版本。 14 | 15 | 如果你觉得这本书对你有帮助,你可以扫描下面的二维码「打赏」程序君 ^_^ 16 | 17 | ![打赏10元](assets/images/weixin10.jpg) 18 | 19 | ### 贡献者 20 | 21 | 以下github用户为本书的疏漏贡献了很多,他们是(排名不分先后): 22 | 23 | longhua 24 | 25 | ### 资助者 26 | 27 | 以下微信用户资助了本书的撰写,他们是(排名不分先后): 28 | 29 | Z张明峰,海东,黄龙华,叶翔Timo,守望者,solu 30 | 31 | ### 版权声明 32 | 33 | 版权归作者所有。你可以免费阅读本书的在线电子版,也可以自行编译本书,在自己的私人电脑中阅读。本书的内容可以被引用,引用时请注明出处(github repo的链接及本书的在线地址)。 34 | -------------------------------------------------------------------------------- /code/shared/utility.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | (require (rename-in 2htdp/image [rotate rotate-1])) 3 | 4 | (provide (all-defined-out)) 5 | 6 | (define (choice lst) 7 | (if (list? lst) (list-ref lst (random (length lst))) 8 | (vector-ref lst (random (vector-length lst))))) 9 | 10 | ; http://stackoverflow.com/questions/23177388/rotate-a-list-of-lists 11 | (define (rotate lsts) 12 | (apply map list lsts)) 13 | 14 | ; #aabbcc -> '(170 187 204) 15 | (define (hex->rgb hex [alpha 255]) 16 | (define r (regexp-match #px"^#(\\w{2})(\\w{2})(\\w{2})$" hex)) 17 | (define (append-hex s) (string-append "#x" s)) 18 | (define (color-alpha c) (apply color (append c (list alpha)))) 19 | (if r 20 | (color-alpha (map (compose1 string->number append-hex) (cdr r))) 21 | #f)) 22 | 23 | (define (image-append images get-pos overlap) 24 | (if (<= (length images) 1) 25 | (car images) 26 | (let* ([a (first images)] 27 | [b (second images)] 28 | [img (apply overlay/xy 29 | (append (list a) (get-pos a overlap) (list b)))]) 30 | (image-append (cons img (drop images 2)) get-pos overlap)))) 31 | 32 | (define (hc-append images [overlap 0]) 33 | (image-append images 34 | (λ (img o) (list (- (image-width img) o) 0)) 35 | overlap)) 36 | 37 | (define (vc-append images [overlap 0]) 38 | (image-append images 39 | (λ (img o) (list 0 (- (image-height img) o))) 40 | overlap)) -------------------------------------------------------------------------------- /bin/add-to-head.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | (require racket/runtime-path 4 | "../util/common.rkt") 5 | 6 | (define ga-code 7 | #< 9 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 10 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 11 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 12 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 13 | 14 | ga('create', 'UA-57258984-1', 'auto'); 15 | ga('send', 'pageview'); 16 | 17 | 18 | 19 | EOF 20 | ) 21 | 22 | (define (meta k v) 23 | (format "" k v)) 24 | 25 | (define metas 26 | (string-append 27 | (meta "keywords" (book-keyword racket-book)) 28 | (meta "description" (book-name racket-book)) 29 | (meta "author" (book-author racket-book)) 30 | (meta "charset" "utf-8"))) 31 | 32 | (define "") 33 | 34 | (define all (string-append metas ga-code )) 35 | (define subst (regexp-replace* "\n" all "")) ;minify 36 | 37 | (define (do-file path) 38 | (define old (file->string path)) 39 | (define new (regexp-replace old subst)) 40 | (with-output-to-file path 41 | (lambda () (display new)) 42 | #:mode 'text 43 | #:exists 'replace)) 44 | 45 | 46 | (define-runtime-path here "../html") 47 | (for ([path (find-files (lambda (path) 48 | (regexp-match? #rx"\\.html" path)) 49 | here)]) 50 | (do-file path)) 51 | -------------------------------------------------------------------------------- /docs/01-begin/01-why-racket.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/doc 2 | 3 | @(require (for-label racket) 4 | scribble/manual 5 | "../../util/common.rkt") 6 | 7 | @title[#:tag "begin-why"]{为什么是Racket} 8 | 9 | @margin-note{Lisp/scheme的链接指向wikipedia,可能需要翻墙。建议读者在阅读本书的时候自行进入翻墙模式,以便减少不必要的麻烦} 10 | 11 | 想一句话说清楚什么是Racket很困难。Racket是 @rh["http://zh.wikipedia.org/wiki/Scheme" "Scheme"] 的一种方言,而Scheme又是 @rh["http://zh.wikipedia.org/zh-cn/LISP" "Lisp"] 的一种方言。作为一门古老的计算机语言,Lisp一直被许多人视为史上最非凡的编程语言。50多年前诞生的时候,它就带来了诸多革命性的创新,并且极大地影响了后来编程语言的发展,即使在一大批现代语言不断涌现的今天,Lisp的诸多特性仍然未被超越。Paul Graham在他的 @rh["http://book.douban.com/subject/6021440/" "「黑客与画家」"] 中写到Lisp诞生时,就包含了9种思想,而这九种思想至今在其它语言中还只实现了一部分: 12 | 13 | @margin-note{本段内容引用自阮一峰的博文:@rh["http://www.ruanyifeng.com/blog/2010/10/why_lisp_is_superior.html" "为什么Lisp语言如此先进?(译文)"]} 14 | 15 | @verbatim[#:indent 4]{ 16 | 1. 条件结构(即"if-then-else"结构)。现在大家都觉得这是理所当然的,但是Fortran I就没有这个结构,它只有基于底层机器指令的goto结构。 17 | 18 | 2. 函数也是一种数据类型。在Lisp语言中,函数与整数或字符串一样,也属于数据类型的一种。它有自己的字面表示形式(literal representation),能够储存在变量中,也能当作参数传递。一种数据类型应该有的功能,它都有。 19 | 20 | 3. 递归。Lisp是第一种支持递归函数的高级语言。 21 | 22 | 4. 变量的动态类型。在Lisp语言中,所有变量实际上都是指针,所指向的值有类型之分,而变量本身没有。复制变量就相当于复制指针,而不是复制它们指向的数据。 23 | 24 | 5. 垃圾回收机制。 25 | 26 | 6. 程序由表达式(expression)组成。Lisp程序是一些表达式区块的集合,每个表达式都返回一个值。这与Fortran和大多数后来的语言都截然不同,它们的程序由表达式和语句(statement)组成。 27 | 区分表达式和语句,在Fortran I中是很自然的,因为它不支持语句嵌套。所以,如果你需要用数学式子计算一个值,那就只有用表达式返回这个值,没有其他语法结构可用,因为否则就无法处理这个值。 28 | 后来,新的编程语言支持区块结构(block),这种限制当然也就不存在了。但是为时已晚,表达式和语句的区分已经根深蒂固。它从Fortran扩散到Algol语言,接着又扩散到它们两者的后继语言。 29 | 30 | 7. 符号(symbol)类型。符号实际上是一种指针,指向储存在哈希表中的字符串。所以,比较两个符号是否相等,只要看它们的指针是否一样就行了,不用逐个字符地比较。 31 | 32 | 8. 代码使用符号和常量组成的树形表示法(notation)。 33 | 34 | 9. 无论什么时候,整个语言都是可用的。Lisp并不真正区分读取期、编译期和运行期。你可以在读取期编译或运行代码;也可以在编译期读取或运行代码;还可以在运行期读取或者编译代码。 35 | } 36 | 37 | 目前,整个Lisp社区被主流软件公司接受的程度还很低,而Lisp中最受人瞩目的当属Clojure和Racket。作为Lisp的一种方言,Racket包含了几乎所有Lisp的优点,同时也提供了大量有用的库,降低初学者学习的成本 —— 值得一提的是,Racket在很多高校中都作为程序语言的入门语言用于教学。 38 | 39 | 说点关于我学这门语言前的状态: 40 | 41 | @itemlist[ 42 | @item{两三年前我接触过Clojure,浅尝辄止,兴趣没维持超过一周,各种example的代码量不超过500行,各种宏根本看不懂。} 43 | @item{略懂emacs,强迫自己在sublime和pycharm之外只能使用emacs,能简单修改其配置(基于Emacslisp)。} 44 | @item{对无穷无尽嵌套的括号没有太大反感,语法对我来说不是个事(如果在这个世界上你能忍受javascript/node.js,那么没什么语法你不能忍受的)。} 45 | @item{最熟悉的语言是Python和C,从未用过任何一门函数式编程语言写过一个有意义的项目。} 46 | ] 47 | 48 | 所以,从写这本书起我和你基本上是在同步学习,让我们一起努力,共同成长,进入Racket的奇妙世界吧! -------------------------------------------------------------------------------- /index.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/manual 2 | 3 | @(require (for-label racket) 4 | scribble/racket 5 | "util/common.rkt") 6 | 7 | 8 | 9 | @title{@(book-name racket-book)} 10 | 11 | @author[@(book-author racket-book)] 12 | 13 | 14 | @image[@(book-cover racket-book) #:style "cover"] 15 | @para[@smaller{@(book-copyright racket-book)}] 16 | @para[@smaller[@(book-last-update racket-book)]] 17 | @para{如果您发现本书的任何问题,请在 @hyperlink["https://github.com/tyrchen/racket-book/issues" "该书的github项目上提交问题单,多谢!"].} 18 | 19 | 这是一本关于racket的入门书。程序君在学习racket的过程中,发现racket的中文资料几乎为零,于是萌生了撰写这本书的想法。写这本书,某种程度上是出于私利,因为我一直认为最好的学习方法就是将自己学到的东西教授出去。在教授的过程中,自己能够学得更扎实。 20 | 21 | 于是,在学了racket也就一个周末之后,我开了这个repo,来记录和传授我学习racket的心得。撰写这本书,并不意味着我对racket的掌握有什么过人之处,恰恰相反,我和打算起步的你一样,不断挣扎于对这门语言的理解。 22 | 23 | 由于racket提供了 @hyperlink["http://docs.racket-lang.org/scribble/" "scribble"]这门专门用于撰写文档的语言,所以这本书也一反我的习惯,没用 markdown 或 asciidoc 撰写,而是全部用 scribble 完成。使用scribble的体验很好,在这个过程中,它也激励我使用racket去解决一些实际的问题。是的,如果你浏览这个repo的源码,你会发现,racket并非一个「花瓶」语言,只能用于去理解一些高深的宏编程或者函数式编程的思想,而是一门很实用的工具,可以做几乎任何通用语言(如python)能做的事情。 24 | 25 | 由于本书面向初学者,所以,如果你顺着读下来发现有些概念或者知识没有解释清楚,请向我提出,以便我修订。文中出现的任何问题,也欢迎大家提bug。 26 | 27 | 你可以通过 https://tyrchen.github.io/racket-book 访问本书的最新版本。 28 | 29 | 如果你觉得这本书对你有帮助,你可以扫描下面的二维码「打赏」程序君 ^_^ 30 | 31 | @image["assets/images/weixin10.jpg"] 32 | 33 | @bold{@larger{贡献者}} 34 | 35 | 以下github用户为本书的疏漏贡献了很多,他们是(排名不分先后): 36 | 37 | longhua 38 | 39 | @bold{@larger{资助者}} 40 | 41 | 以下微信用户资助了本书的撰写,他们是(排名不分先后): 42 | 43 | Z张明峰,海东,黄龙华,叶翔Timo,守望者,solu 44 | 45 | @bold{@larger{版权声明}} 46 | 47 | 版权归作者所有。你可以免费阅读本书的在线电子版,也可以自行编译本书,在自己的私人电脑中阅读。本书的内容可以被引用,引用时请注明出处(github repo的链接及本书的在线地址)。 48 | 49 | @; table-of-contents[] 50 | 51 | @; ------------------------------------------------- 52 | @; include-section["docs/preface.scrbl"] 53 | @include-section["docs/01-begin/index.scrbl"] 54 | @include-section["docs/02-basics/index.scrbl"] 55 | @include-section["docs/03-practical-programs/index.scrbl"] 56 | @include-section["docs/04-advanced-racket/index.scrbl"] 57 | @include-section["docs/05-plotting.scrbl"] 58 | @include-section["docs/06-scribble.scrbl"] 59 | @include-section["docs/07-package-system.scrbl"] 60 | @include-section["docs/08-macro.scrbl"] 61 | @include-section["docs/09-advanced-macro.scrbl"] 62 | @include-section["docs/10-continuation.scrbl"] 63 | @include-section["docs/11-server.scrbl"] 64 | @include-section["docs/12-web.scrbl"] 65 | @include-section["docs/13-real-world.scrbl"] 66 | @include-section["docs/14-misc.scrbl"] 67 | @include-section["docs/15-typed-racket.scrbl"] 68 | @include-section["docs/16-further-readings.scrbl"] 69 | @include-section["docs/postscript.scrbl"] 70 | -------------------------------------------------------------------------------- /util/common.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | (require scribble/core 4 | scribble/racket 5 | scribble/eval 6 | racket/syntax 7 | racket/date 8 | scribble/manual 9 | db 10 | 2htdp/image 11 | 2htdp/universe 12 | plot/utils 13 | (for-label racket 14 | 2htdp/image 15 | 2htdp/universe 16 | plot/utils 17 | db)) 18 | 19 | (provide (all-defined-out) 20 | (all-from-out plot/utils) 21 | (all-from-out scribble/eval) 22 | (all-from-out db) 23 | (all-from-out 2htdp/image) 24 | (all-from-out 2htdp/universe) 25 | (for-label (all-from-out racket 26 | 2htdp/image 27 | 2htdp/universe 28 | db 29 | plot/utils))) 30 | 31 | ;; for the document meta data 32 | ;; 33 | (struct book (name author cover copyright last-update keyword)) 34 | 35 | (define racket-book (book "Racket语言入门" 36 | "Tyr Chen" 37 | "assets/images/cover.jpg" 38 | "Copyright (c) 2014-2015 by Tyr Chen. All rights reserved." 39 | (string-append "最后更新于:" (date->string (current-date) #t)) 40 | "racket,scheme,lisp,racket入门")) 41 | 42 | (define (drr) (racket DrRacket)) 43 | 44 | (define (bq) (racketvalfont "'")) 45 | 46 | ;; shortcut for racketblock 47 | ;; 48 | 49 | (define-syntax-rule (rb body ...) 50 | (racketblock body ...)) 51 | 52 | ;; shortcut for examples 53 | (define basic-eval (make-base-eval)) 54 | (interaction-eval #:eval basic-eval 55 | (require racket/base 56 | racket/math 57 | racket/string 58 | racket/vector 59 | racket/list)) 60 | ;; Here we can actually define more eval for different situation, such as plot-eval... 61 | (define-syntax re 62 | (syntax-rules () 63 | [(_ #:eval . rest) 64 | (interaction #:eval . rest)] 65 | [(_ . rest) 66 | (interaction #:eval basic-eval . rest)])) 67 | 68 | ;; shortcut for racket 69 | ;; 70 | (define-syntax-rule (r body ...) 71 | (racket body ...)) 72 | 73 | ;; shortcut for hyperlink 74 | ;; 75 | (define-syntax-rule (rh body ...) 76 | (hyperlink body ...)) 77 | 78 | (require (only-in scribble/html-properties attributes alt-tag)) 79 | 80 | ;; For use in Scribble source file. Lets you create a
 class="code"
 81 | ;; block with a language tag that can be syntax highlighted by highlight.js.
 82 | ;;
 83 | ;; Example usage:
 84 | ;;
 85 | ;; @code-hl[#:lang "js"]{function foo() {return 1;}}
 86 | ;;
 87 | (define (code-hl #:lang lang . xs)
 88 | 	(element (style "code" (list (alt-tag "pre")))
 89 | 		(element (style lang (list (alt-tag "code")))
 90 | 		xs)))
 91 | 
 92 | (define reduces (make-element #f (list 'rarr)))
 93 | 
 94 | ;; For use in Scribble source file. Lets you highlight code on source code enclosed with @rb[].
 95 | ;;
 96 | ;; Example usage:
 97 | ;;
 98 | ;; @rb[(define #,(hl name) "Tyr Chen")]
 99 | ;;
100 | (define *hl (lambda (c)
101 |                   (make-element highlighted-color (list c))))
102 | (define-syntax hl
103 |    (syntax-rules () [(_ a) (*hl (racket a))]))
104 | 
105 | ;; link back to official documents
106 | (define rdoc (curry (lambda (libname name) (secref name #:doc libname))))
107 | (define rdoc-ref (rdoc '(lib "scribblings/reference/reference.scrbl")))
108 | (define rdoc-teachpack (rdoc '(lib "teachpack/teachpack.scrbl")))
109 | (define rdoc-guide (rdoc '(lib "scribblings/guide/guide.scrbl")))
110 | 


--------------------------------------------------------------------------------
/docs/03-practical-programs/02-program-crop.scrbl:
--------------------------------------------------------------------------------
  1 | #lang scribble/doc
  2 | 
  3 | @(require (for-label racket)
  4 |           scribble/manual
  5 |           "../../util/common.rkt")
  6 | 
  7 | @title[#:tag "practical-crop"]{照片裁剪}
  8 | 
  9 | @margin-note{当然,osx本身就提供了 @r[_sips] 这个程序让你可以很方便地写个bash脚本(或者Python脚本)就能完成这个任务。}
 10 | 
 11 | 现在进入正题。假设你正为你的孩子做一本画册,为了排版方便,里面的很多素材都需要正方形大小的800x800的图片(或者其它的什么尺寸),而你平日照下来的照片都不符合此要求,需要裁剪。当然,有一些工具可以帮助你完成这一要求,但如果你有成百上千张这样的照片需要处理,你就不得不考虑写个程序来完成这一任务了。
 12 | 
 13 | @margin-note{这里假设你依旧使用 @(drr)}
 14 | 
 15 | 我们先来实验一下算法。使用你喜欢的编辑器创建一个新的文件,并将其存放在 @bold{~/study/racket/face.rkt} 下。接下来你需要准备一幅图片,我们随便下载一张 @rh["https://c1.staticflickr.com/9/8064/8257151659_fa29614b17_z.jpg" "无版权的照片"],存为 @bold{face.jpg},放在和你当前工作目录相同的地方,然后在编辑器中输入:
 16 | 
 17 | 
 18 | @re[
 19 | (require 2htdp/image
 20 |          racket/cmdline
 21 |          (only-in racket/draw read-bitmap))
 22 | (define perfect-woman (read-bitmap "face.jpg"))
 23 | perfect-woman
 24 | ]
 25 | 
 26 | 运行后,你会看到这幅图被加载出来了。
 27 | 
 28 | @image["assets/images/woman.jpg" #:style "cover"]
 29 | 
 30 | 在Racket里,@r[crop] 可以用来裁剪图片,我们试验一下:
 31 | 
 32 | @re[
 33 | (crop 100 100 300 300 perfect-woman)
 34 | ]
 35 | 
 36 | 嗯,从图片的 @bold{(100, 100)} 起(左上角为 @bold{(0, 0)}),剪切宽300,长300的图片,正是我们需要的!了解了 @r[crop] 的运作方式,我们便可以尝试撰写第一个版本的函数:
 37 | 
 38 | @re[
 39 | (define (image-to-box1 img width)
 40 |   (define (f x y) (/ (- x y) 2))
 41 |   (let*-values ([(w) (image-width img)]
 42 |                 [(h) (image-height img)]
 43 |                 [(∂w ∂h) (if (> w h) (values (f w h) 0) (values 0 (f h w)))]
 44 |                 [(∂w1 ∂h1) (values (+ ∂w (f w width)) (+ ∂h (f h width)))])
 45 |     (crop ∂w1 ∂h1 width width img)))
 46 | (image-to-box1 perfect-woman 400)
 47 | ]
 48 | 
 49 | 感觉还不错,是不是挺简单的?这里 @r[image-width] 和 @r[image-height] 用于获取图片的长和宽。算法很简单:如果图片的长宽不一致,先计算长或者宽额外需要略过的像素,然后再加上长宽分别要略过的像素。
 50 | 
 51 | 接下来我们稍作修改,让其能接受一个文件:
 52 | 
 53 | @re[
 54 | (define (image-to-box img/file width)
 55 |   (define (f x y) (/ (- x y) 2))
 56 |   (let*-values ([(img) (if (string? img/file) (read-bitmap img/file) img/file)]
 57 |                 [(w) (image-width img)]
 58 |                 [(h) (image-height img)]
 59 |                 [(∂w ∂h) (if (> w h) (values (f w h) 0) (values 0 (f h w)))]
 60 |                 [(∂w1 ∂h1) (values (+ ∂w (f w width)) (+ ∂h (f h width)))])
 61 |     (crop ∂w1 ∂h1 width width img)))
 62 | (image-to-box "face.jpg" 400)
 63 | ]
 64 | 
 65 | 越来越接近我们的目标了。如果要让这段代码能在命令行下运行,接受用户传入的参数,比如说:
 66 | 
 67 | @code-hl[#:lang "bash"]{
 68 | $ racket face.rkt face.jpg 400
 69 | }
 70 | 
 71 | @margin-note{
 72 | 注意第一行要声明语言:
 73 | 
 74 | @code-hl[#:lang "racket"]{
 75 | #lang racket
 76 | }
 77 | 
 78 | 否则,会报错。
 79 | }
 80 | 那么我们需要 @r[command-line] 的支持。它定义在:@r[racket/cmdline] 下。我们继续在编辑器里工作,最终完工的代码如下:
 81 | 
 82 | @rb[
 83 | (require 2htdp/image
 84 |          racket/cmdline
 85 |          (only-in racket/draw read-bitmap))
 86 | 
 87 | (define women (read-bitmap "face.jpg"))
 88 | 
 89 | (define (image-to-box img/file width)
 90 |   (define (f x y) (/ (- x y) 2))
 91 |   (let*-values ([(img) (if (string? img/file) (read-bitmap img/file) img/file)]
 92 |                 [(w) (image-width img)]
 93 |                 [(h) (image-height img)]
 94 |                 [(∂w ∂h) (if (> w h) (values (f w h) 0) (values 0 (f h w)))]
 95 |                 [(∂w1 ∂h1) (values (+ ∂w (f w width)) (+ ∂h (f h width)))])
 96 |     (crop ∂w1 ∂h1 width width img)))
 97 | 
 98 | (define (normalize-name filename width)
 99 |   (string-replace filename "." (format "-~a." width)))
100 | 
101 | 
102 | (command-line
103 |  #:args (filename width)
104 |  (save-image (image-to-box filename (string->number width))
105 |              (normalize-name filename width)))
106 | 
107 | ]
108 | 
109 | 试着在命令行下运行:
110 | 
111 | @code-hl[#:lang "bash"]{
112 | $ racket face.rkt face.jpg 400
113 | #t
114 | }
115 | 
116 | 我们发现,这个目录下生成了一个新的图片 @bold{face-400.jpg}。一切如我们所期望的那样。
117 | 
118 | 目前这段代码有个问题,如果 @r[_width] 大于图片的长或者宽呢?显然会出错。这就留给读者自行修改吧。
119 | 
120 | 细心的读者会发现,生成的图片大小怎么这么大?让我们看看为什么:
121 | 
122 | @code-hl[#:lang "bash"]{
123 | $ file face-400.jpg
124 | face-400.jpg: PNG image data, 400 x 400, 8-bit/color RGBA, non-interlaced
125 | }
126 | 
127 | 哈。@r[save-image] 生成的竟然是PNG。看来它没有根据扩展名进行判断处理。为什么 @r[save-image] 生成的是PNG呢?如果你打开 @bold{/Applications/Racket v6.1.1/share/pkgs/htdp-lib/2htdp/private} 里的 @bold{image-more.rkt},会发现它最终调了 @r[(send bm save-file filename 'png)],所以不管扩展名怎么设置,都只能生成PNG。建议读者可以修改或者重写这个函数,让它更符合自己的需要。


--------------------------------------------------------------------------------
/docs/01-begin/04-first-impression.scrbl:
--------------------------------------------------------------------------------
  1 | #lang scribble/doc
  2 | 
  3 | @(require (for-label racket)
  4 |           scribble/manual
  5 |           "../../util/common.rkt")
  6 | 
  7 | @title[#:tag "begin-first-impression"]{Racket初印象}
  8 | 
  9 | 在任何一个「正常」的编程语言中,最简单的求值看起来是这个样子的:
 10 | 
 11 | @code-hl[#:lang "python"]{
 12 | > 1 + 1 # python
 13 | > 2
 14 | }
 15 | 
 16 | 在Racket的世界里,它被写成这样:
 17 | 
 18 | @rb[
 19 | (+ 1 1)
 20 | ]
 21 | 
 22 | @margin-note{比如说在Python中,有这些关键字:
 23 | @verbatim[#:indent 0]{
 24 | and      del     from   not
 25 | as       elif    global or
 26 | assert   else    if     pass
 27 | break    except  import print
 28 | class    exec    in     raise
 29 | continue finally is     return
 30 | def      for     lambda try
 31 | while    with    yield}
 32 | 关键字是语言本身内置的语法单元,每个关键字有自己的特定含义。}
 33 | 
 34 | Racket没有其它编程语言中的「关键字」(或者「保留字」的概念,语言的一切细节都可以由表达式 @bold{(...)} 完成,而表达式的基本构成单元是函数。Racket一个表达式大概是这个样子的:
 35 | 
 36 | @rb[
 37 | (function-name args1 ...)
 38 | ]
 39 | 
 40 | 函数的参数也可以是表达式。在上面的 @r[(+ 1 1)] 的例子中,@bold{+} 是一个函数,随后的两个 @bold{1} 是函数的参数。作为初学者,我们暂且先放下这些语法细节,写一些更多的代码耍耍吧。
 41 | 
 42 | 
 43 | @#reader scribble/comment-reader(racketblock
 44 | > (+ 2 4 6)
 45 | 12
 46 | > (* 2 (+ 3 4))
 47 | 14
 48 | > (expt 2 3) @; 指数函数
 49 | 8
 50 | > (quotient 5 2) ; 求商
 51 | 2
 52 | > (remainder 5 2) ; 求余
 53 | 1
 54 | > (/ 35 7) ; 除
 55 | 5
 56 | > (/ 4 6) ; 注意不能整除时,Racket用分数形式表示
 57 | 2/3
 58 | > (exact->inexact 2/3)
 59 | ; 转换成非精度实数,exact->inexact是一个函数,尽管看起来比较怪异
 60 | 0.6666666666
 61 | > (* 1+2i 3+4i) ; 你还能手算虚数的乘除么?
 62 | > -5+10i
 63 | > (not #t) ; true/false用#t, #f表示,not是一个函数,表示「非」
 64 | #f
 65 | > (and -1 #f)
 66 | ; 与函数,只要有参数不为#t,就返回#f。在and运算时,任何非#f的数据均相当于#t。
 67 | #f
 68 | > (and -1 2)
 69 | ; 由于任何非#f的数据均相当于#t,所以and的结果在为#t时,会返回一个比#t更有意义的结果。
 70 | 2
 71 | > (or -1 #f) ; 或函数
 72 | -1
 73 | > (or #f #f)
 74 | #f
 75 | > (xor #f 10) ; 异或
 76 | 10
 77 | > (xor 10 20)
 78 | #f
 79 | > (> 1 2)
 80 | #f
 81 | > (< -1 0)
 82 | #t
 83 | > (= 10 20)
 84 | #f
 85 | > (string-append "你好" "," "世界!")
 86 | ; 在Racket中,字符串由""括起来,string-append可以将多个字符串连接起来
 87 | "你好,世界!"
 88 | > (format "~a,~a!" "你好" "世界") ; format可以格式化字符串
 89 | "你好,世界!"
 90 | > (printf "~a,~a!" "你好" "世界")
 91 | ; printf用来输出字符串,注意format/printf的输出在DrRacket里的颜色的不同
 92 | 你好,世界!
 93 | > (number->string 42) ; 数字转字符串
 94 | "42"
 95 | > (string->number "42") ; 字符串转数字
 96 | 42
 97 | > (string->number "hello world")
 98 | false
 99 | > (string-length "hello world!") ; 求字符串长度
100 | 12
101 | > (string-length "你好,世界!") ; Racket认识unicode,所以给出正确的长度
102 | 6
103 | > (string? "你好") ; 测试参数是否为字符串
104 | #t
105 | > (number? "1") ; 测试参数是否为数字
106 | #f
107 | > (number? 1+2i)
108 | #t
109 | )
110 | 
111 | @margin-note{更多关于函数副作用的知识,请参考:@rh["http://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E5%89%AF%E4%BD%9C%E7%94%A8" "Wikipedia:函数副作用"]}
112 | 
113 | 在Racket中,绝大多数函数是没有副作用的,像 @r[printf] 这样的函数,除了有一个返回值以外,还向外设(这里是显示器)输出了字符,所以是有副作用的。注意 @r[printf] 的返回值并非一个字符串,我们通过下面的例子可以看到:
114 | 
115 | @#reader scribble/comment-reader(racketblock
116 | > (string-append (format "~a," "你好") "世界!")
117 | "你好,世界!"
118 | > (string-append (printf "~a," "你好") "世界!")
119 | ; 会给出错误提示,告诉你 string-append 期待 string?,却等来了 #
120 | )
121 | 
122 | 我们先把函数的副作用放在一边,在 @secref{advanced-racket} 那一章里面谈函数式编程时会讲到。
123 | 
124 | 在上面的例子中,我们看到了两种「奇怪」的函数:@r[string->number] 这样中间用 @bold{->} 连接的,以及 @r[string?] 这样结尾为 @bold{?} 的函数。在Racket里,函数(或者变量)的命名非常宽松,不像其它语言那么死板,你甚至可以这么定义一个函数:
125 | 
126 | @rb[
127 | > (define (-@&*123y!!!->!my_god? x) x)
128 | > (-@&*123y!!!->!my_god? 10)
129 | 10
130 | ]
131 | 
132 | 因此,在Racket里,人们往往使用一些约定俗成的符号来让函数的可读性更强,比如说判定系列的函数都统一用 @bold{?} 来结尾,
133 | 而转换系列的函数用 @bold{->} 来注明。
134 | 
135 | 既然提到了函数和变量,我们来看看它们是如何定义的:
136 | 
137 | @rb[
138 | > (define PI 3.1415926)
139 | > (define hello "hello world")
140 | > (format "~a:~a" hello PI)
141 | "hello world:3.1415926"
142 | ]
143 | 
144 | 使用 @r[define] 这个函数,我们可以定义一个变量,同样,我们也可以定义函数:
145 | 
146 | @#reader scribble/comment-reader(racketblock
147 | > (define
148 |     (circle-area r) ; 函数名 参数列表
149 |     (* pi (sqr r))  ; 函数体
150 |   )
151 | > (circle-area 10)
152 | 314.1592653589793
153 | )
154 | 
155 | 我们之前讲到Racket中没有关键字,那么,@r[define define 10] 会有什么后果?
156 | 
157 | @#reader scribble/comment-reader(racketblock
158 | > (define define 10)
159 | > define
160 | 10
161 | > (define a 10) ; 这里就会抛出异常,和执行 (10 a 10)的错误一样
162 | > (10 a 10)
163 | )
164 | 
165 | 所以,当你对Racket掌握到一定程度后,你可以任意改造这门语言,让它成为你的私人禁脔。
166 | 
167 | 对了,现在你已经把 @(drr) 的REPL解释器折腾坏了,不过没关系,运行一下 @(drr) 工具栏上的 @bold{Run},一切又恢复如初了。
168 | 


--------------------------------------------------------------------------------
/code/my-2048.rkt:
--------------------------------------------------------------------------------
  1 | #lang racket
  2 | 
  3 | (require "shared/utility.rkt"
  4 |          (rename-in 2htdp/image [rotate rotate-1])
  5 |          2htdp/universe)
  6 | 
  7 | (define PIECE_DIST '(2 2 2 2 2 2 2 2 2 4))
  8 | 
  9 | (define (make-board n)
 10 |   (make-list n (make-list n 0)))
 11 | 
 12 | (define (init-board n)
 13 |   (put-random-piece (put-random-piece (make-board n))))
 14 | 
 15 | (define (get-a-piece)
 16 |   (choice PIECE_DIST))
 17 | 
 18 | ; e.g. '((2 0) (2 4)) -> #t
 19 | (define (avail? lst)
 20 |   (if (list? lst)
 21 |       (ormap avail? lst)
 22 |       (zero? lst)))
 23 | 
 24 | ; e.g. '((2 2) (2 0) (0 0)) -> '(1 2)
 25 | (define (get-empty-refs lst zero-fun?)
 26 |   (for/list ([item lst]
 27 |              [i (range (length lst))]
 28 |              #:when (zero-fun? item))
 29 |     i))
 30 | 
 31 | ; e.g. '(0 2 0 0) -> '(0 2 0 2)
 32 | ; e.g. '((0 2 0 0) (2 4 8 16) (0 4 4 8) (2 0 0 0)) -> 
 33 | ;      '((0 2 0 0) (2 4 8 16) (0 4 4 8) (2 0 2 0))
 34 | (define (put-random-piece lst)
 35 |   (if (avail? lst)
 36 |       (if (list? lst)
 37 |           (let* ([i (choice (get-empty-refs lst avail?))]
 38 |                  [v (list-ref lst i)])
 39 |             (append (take lst i)
 40 |                     (cons (put-random-piece v) (drop lst (add1 i)))))
 41 |           (get-a-piece))
 42 |       lst))
 43 | 
 44 | ; e.g. '(2 2 2 4 4 4 8) -> '(4 2 8 4 8)
 45 | (define (merge row)
 46 |   (cond [(<= (length row) 1) row]
 47 |         [(= (first row) (second row))
 48 |          (cons (* 2 (first row)) (merge (drop row 2)))]
 49 |         [else (cons (first row) (merge (rest row)))]))
 50 | 
 51 | ; e.g. '(2 0 4 4) #f -> (0 0 2 8)
 52 | (define (move-row row v left?)
 53 |    (if left?
 54 |        (let* ([n (length row)]
 55 |               [l (merge (filter (λ (x) (not (zero? x))) row))]
 56 |               [padding (make-list (- n (length l)) v)])
 57 |          (append l padding))
 58 |        (reverse (move-row (reverse row) v (not left?)))))
 59 | 
 60 | (define (move lst v left?)
 61 |   (map (λ (x) (move-row x v left?)) lst))
 62 | 
 63 | (define (move-left lst)
 64 |   (put-random-piece (move lst 0 #t)))
 65 | 
 66 | (define (move-right lst)
 67 |   (put-random-piece (move lst 0 #f)))
 68 | 
 69 | (define (move-up lst)
 70 |   ((compose1 rotate move-left rotate) lst))
 71 | 
 72 | (define (move-down lst)
 73 |   ((compose1 rotate move-right rotate) lst))
 74 | 
 75 | (define ALL-OPS (list move-right move-down move-left move-up))
 76 | 
 77 | ; '((2 8 4 2) (8 4 8 16) (4 32 2 4) (2 16 4 2)) -> #t
 78 | (define (finished? lst)
 79 |   (andmap (λ (op) (equal? lst (op lst))) ALL-OPS))
 80 | 
 81 | (define (test-play lst step)
 82 |   (if (and (not (avail? lst)) (finished? lst))
 83 |       (values lst step)
 84 |       (test-play ((choice ALL-OPS) lst) (add1 step))))
 85 | 
 86 | 
 87 | ;; game
 88 | (define ALPHA #xb8)
 89 | 
 90 | (define GRID-COLOR (hex->rgb "#bbada0"))
 91 | 
 92 | (define TILE-BG
 93 |   (make-hash (map (λ (item) (cons (first item) (hex->rgb (second item))))
 94 |        '((0    "#ccc0b3") (2    "#eee4da") (4    "#ede0c8")
 95 |          (8    "#f2b179") (16   "#f59563") (32   "#f67c5f")
 96 |          (64   "#f65e3b") (128  "#edcf72") (256  "#edcc61")
 97 |          (512  "#edc850") (1024 "#edc53f") (2048 "#edc22e")))))
 98 | 
 99 | (define TILE-FG 'white)
100 | 
101 | (define TILE-SIZE 80)
102 | (define TILE-TEXT-SIZE 50)
103 | (define MAX-TEXT-SIZE 65)
104 | (define TILE-SPACING 5)
105 | 
106 | (define (make-tile n)
107 |   (define (text-content n)
108 |     (if (zero? n) ""
109 |         (number->string n)))
110 | 
111 |   (overlay (let* ([t (text (text-content n) TILE-TEXT-SIZE TILE-FG)]
112 |                   [v (max (image-width t) (image-height t))]
113 |                   [s (if (> v MAX-TEXT-SIZE) (/ MAX-TEXT-SIZE v) 1)])
114 |              (scale s t))
115 |            (square TILE-SIZE 'solid (hash-ref TILE-BG n))
116 |            (square (+ TILE-SIZE (* 2 TILE-SPACING)) 'solid GRID-COLOR)))
117 | 
118 | (define (show-board b)
119 |   (let ([images (for/list ([row b])
120 |                   (hc-append (map make-tile row) TILE-SPACING))])
121 |     (vc-append images TILE-SPACING)))
122 | 
123 | (define (show-board-over b)
124 |   (let* ([board (show-board b)]
125 |          [layer (square (image-width board) 'solid (color 0 0 0 90))])
126 |     (overlay (text "Game over!" 40 TILE-FG)
127 |              layer board)))
128 | 
129 | (define (key->ops a-key)
130 |   (cond
131 |     [(key=? a-key "left")  move-left]
132 |     [(key=? a-key "right") move-right]
133 |     [(key=? a-key "up")    move-up]
134 |     [(key=? a-key "down")  move-down]
135 |     [else (λ (x) x)]))
136 | 
137 | (define (change b key)
138 |   ((key->ops key) b))
139 | 
140 | (define (start n)
141 |   (big-bang (init-board n)
142 |             (to-draw show-board)
143 |             (on-key change)
144 |             (stop-when finished? show-board-over)
145 |             (name "2048 - racket")))
146 | (start 4)
147 | 


--------------------------------------------------------------------------------
/docs/01-begin/05-dance-with-racket.scrbl:
--------------------------------------------------------------------------------
  1 | #lang scribble/doc
  2 | 
  3 | @(require (for-label racket)
  4 |           scribble/manual
  5 |           "../../util/common.rkt")
  6 | 
  7 | @title[#:tag "begin-play"]{与Racket共舞}
  8 | 
  9 | Racket内置了很多库,在了解更多的语法细节前,让我们轻松一下,体验体验Racket和 @(drr) 带来的无穷乐趣。
 10 | 
 11 | 要想引入一个库中的可用函数,可以使用 @r[require],比如接下来我们要体验的库:
 12 | 
 13 | @margin-note{2htdp意为:How To Develop Program, 2nd Edition,是Racket语言为教学而设计的一套库,其同名电子书可以在 @rh["http://www.ccs.neu.edu/home/matthias/HtDP2e/" "这里"] 阅读。}
 14 | 
 15 | @rb[
 16 | (require 2htdp/image)
 17 | ]
 18 | 
 19 | 引入 @r[image] 库后,我们接下来就要做一些有意思的事情了。
 20 | 
 21 | 
 22 | @rb[
 23 | > (define flag (rectangle 100 61.8 "solid" "red"))
 24 | > flag
 25 | #,(rectangle 100 61.8 "solid" "red")
 26 | > (define big-star (star 15 "solid" "yellow"))
 27 | > big-star
 28 | #,(star 15 "solid" "yellow")
 29 | > (overlay big-star flag)
 30 | #,(overlay (star 15 "solid" "yellow") (rectangle 100 61.8 "solid" "red"))
 31 | ]
 32 | 
 33 | @r[rectangle] 和 @r[star] 用来生成图形,@r[overlay] 将一个图形盖到另一个上面。我们再看看这些例子:
 34 | 
 35 | @rb[
 36 | > (triangle 40 "solid" "tan")
 37 | #,(triangle 40 "solid" "tan")
 38 | > (rhombus 40 60 "outline" "magenta")
 39 | #,(rhombus 40 60 "outline" "magenta")
 40 | > (circle 20 "solid" "green")
 41 | #,(circle 20 "solid" "green")
 42 | > (regular-polygon 50 3 "outline" "red")
 43 | #,(regular-polygon 50 3 "outline" "red")
 44 | > (regular-polygon 40 4 "solid" "blue")
 45 | #,(regular-polygon 40 4 "solid" "blue")
 46 | > (regular-polygon 20 8 "solid" "red")
 47 | #,(regular-polygon 20 8 "solid" "red")
 48 | > (ellipse 50 30 "solid" "purple")
 49 | #,(ellipse 50 30 "solid" "purple")
 50 | > (overlay (ellipse 10 10 "solid" "red")
 51 |            (ellipse 20 20 "solid" "black")
 52 |            (ellipse 30 30 "solid" "red")
 53 |            (ellipse 40 40 "solid" "black")
 54 |            (ellipse 50 50 "solid" "red")
 55 |            (ellipse 60 60 "solid" "black"))
 56 | #,(overlay (ellipse 10 10 "solid" "red")
 57 |            (ellipse 20 20 "solid" "black")
 58 |            (ellipse 30 30 "solid" "red")
 59 |            (ellipse 40 40 "solid" "black")
 60 |            (ellipse 50 50 "solid" "red")
 61 |            (ellipse 60 60 "solid" "black"))
 62 | > (overlay/xy (rectangle 20 20 "solid" "red")
 63 |               10 10
 64 |               (rectangle 20 20 "solid" "black"))
 65 | #,(overlay/xy (rectangle 20 20 "solid" "red")
 66 |               10 10
 67 |               (rectangle 20 20 "solid" "black"))
 68 | ]
 69 | 
 70 | 这里大部分函数都很好理解,就不详细解释,最后的 @r[overlay/xy] 也是一个函数,Racket约定使用 @bold{/} 符号的函数代表其属于同一族,即 @r[overlay/xy] 是 @r[overlay] 的变体。
 71 | 
 72 | 我们以一个动画来结束本小结的内容吧:
 73 | 
 74 | @rb[
 75 | > (radial-star 8 8 64 "solid" "darkslategray")
 76 | #,(radial-star 8 8 64 "solid" "darkslategray")
 77 | > (define (my-star x)
 78 |    (radial-star x 8 64 "solid" "darkslategray"))
 79 | > (my-star 20)
 80 | #,(radial-star 20 8 64 "solid" "darkslategray")
 81 | > (place-image (my-star 30) 75 75 (empty-scene 150 150))
 82 | #,(place-image (radial-star 30 8 64 "solid" "darkslategray")
 83 |                75 75 (empty-scene 150 150))
 84 | > (require 2htdp/universe)
 85 | > (animate (#,(hl lambda) (x)
 86 |            (place-image (my-star (+ x 2)) 75 75 (empty-scene 150 150))))
 87 | ]
 88 | 
 89 | 这里我们引入了 @r[lambda] 的概念,这是因为 @r[animate] 需要一个接受一个参数的函数作为其参数,所以我们需要给它一个函数。@r[lambda] 是用来声明一个匿名函数的,这里:
 90 | 
 91 | @rb[
 92 | > (animate (lambda (x)
 93 |            (place-image (my-star (+ x 2)) 75 75 (empty-scene 150 150))))
 94 | ]
 95 | 
 96 | 等价于:
 97 | 
 98 | @rb[
 99 | > (define (my-image x)
100 |           (place-image (my-star (+ x 2)) 75 75 (empty-scene 150 150)))
101 | > (animate my-image)
102 | 195
103 | ]
104 | 
105 | @r[animate] 会启动一个时钟,每秒产生 @r[28] 个tick,从 @r[0] 开始,每次tick加 @r[1],然后把生成的值传给传入的函数。由于 @r[radial-star] 的角的个数至少是2,所以这里在定义 @r[my-image] 时,为传入的 @r[x] 加了 @r[2]。@r[animate] 会无限运行下去,直到你把打开的窗口关掉。此时,返回的结果就是走过的tick数。
106 | 
107 | 我们可以重新定义一下 @r[my-star],使这个动画运行一段时间后重头循环运行:
108 | 
109 | @rb[
110 | > (define (my-star x)
111 |           (radial-star (+ (remainder x 100) 2) 8 64 "solid" "darkslategray"))
112 | ]
113 | 
114 | 你可以尝试重新运行动画,看看效果,然后自行理解其含义。:)
115 | 
116 | Racket的还提供了另一种动画方案 @r[big-bang],可以这么使用:
117 | 
118 | @rb[
119 | > (define (my-star x) (radial-star x 8 64 "solid" "blue"))
120 | > (define (ten? x) (equal? x 10))
121 | > (big-bang 100
122 |             [to-draw my-star]
123 |             [on-tick sub1]
124 |             [stop-when ten?]
125 |             [on-key (lambda (s ke) 100)])
126 | ]
127 | 
128 | 它允许你设置一个初始条件(@r[100]),执行函数(@r[my-star]),tick发生时对初值的改变(@r[sub1]),以及何时停止动画(@r[ten?])。此外,当动画未停止之前,有键盘事件发生时(你敲了任意键),初始条件又恢复成 (@r[100]),见 @r[on-key] 里的 @r[lambda] 函数。
129 | 
130 | 如果读完本节,对图形处理你还意犹未尽,可以读以下文档:
131 | @itemlist[
132 | 
133 | @item{@rh["http://docs.racket-lang.org/quick/" "Quick: An Introduction to Racket with Pictures"]}
134 | 
135 | @item{@rh["http://www.ccs.neu.edu/home/matthias/HtDP2e/part_prologue.html" "Prologue of \"How to Design Programs 2nd\""]}
136 | 
137 | @item{@rdoc-teachpack{image}(内有很多例子)}
138 | 
139 | ]
140 | 


--------------------------------------------------------------------------------
/docs/02-basics/01-grammar.scrbl:
--------------------------------------------------------------------------------
  1 | #lang scribble/doc
  2 | 
  3 | @(require (for-label racket)
  4 |           scribble/manual
  5 |           "../../util/common.rkt")
  6 | 
  7 | @title[#:tag "basics-grammar"]{Racket语法基础}
  8 | 
  9 | 在Racket的世界里,一切皆为表达式。表达式可以返回一个值,或者一个列表(list)。而函数,则是构成表达式的基本要素。在这一章里,我们深入到Racket的语言细节,看看语言基本的库都提供了哪些必备的功能。
 10 | 
 11 | @section[#:tag "basics-grammer-set"]{变量绑定}
 12 | 
 13 | 在Racket中,默认情况下,变量的值是不可变的,它只能绑定和重新绑定,如下面的例子:
 14 | 
 15 | @rb[
 16 | > (define x 1)
 17 | > (define x (+ x 1))
 18 | > x
 19 | 2
 20 | ]
 21 | 
 22 | @r[define] 是可以做全局绑定的,每次只能绑定一个变量。如果想一次绑定多个变量,可以使用 @r[let]:
 23 | 
 24 | @#reader scribble/comment-reader(racketblock
 25 | > (let ([a 3]
 26 |         [b (list-ref '(1 2 3 4) 3)])
 27 |     (sqr (+ a b)))
 28 | 49
 29 | > #,(hl (or a b))  ;; 这里会抛出undefined异常
 30 | )
 31 | 
 32 | @r[let] 和 @r[define] 最大的不同是 @r[let] 是局部绑定,绑定的范围仅仅在其作用域之内,上面的例子中,@r[_a] 和 @r[_b] 出了 @r[let] 的作用域便不复存在了。
 33 | 
 34 | 如果在绑定的过程中,需要互相引用,可以使用 @r[let*]。我们可以看下面的例子:
 35 | 
 36 | @re[
 37 | (let* ([x 10]
 38 |          [y (* x x)])
 39 |     (list x y))
 40 | ]
 41 | 
 42 | 有时候,你希望一次绑定多个值,可以使用 @r[let-values]:
 43 | 
 44 | @re[
 45 | (let-values ([(x y) (quotient/remainder 10 3)])
 46 |     (list y x))
 47 | ]
 48 | 
 49 | 它也有对应的 @r[let*-values] 形式。如下例:
 50 | 
 51 | @re[
 52 | (let*-values ([(pi r rr) (values 3.14159 10 (lambda (x) (* x x)))]
 53 |               [(perimeter area) (values (* 2 pi r) (* pi (rr r)))])
 54 |     (list area perimeter))
 55 | ]
 56 | 
 57 | 在这个例子里,我们看到 @r[let] 也能把一个变量和一个 @r[lambda] 函数进行绑定。我们看到,@r[_rr] 在使用的过程中,需要为其传入一个已经绑定好的变量,比如本例中的 @r[_r]。那么问题来了,如果函数的参数要延迟到 @r[let] 语句的 @r[_body] 里才能获得,那该怎么办?Racket提供了 @r[letrec]:
 58 | 
 59 | @re[
 60 | (letrec ([is-even? (lambda (n)
 61 |                        (or (zero? n)
 62 |                            (is-odd? (sub1 n))))]
 63 |          [is-odd? (lambda (n)
 64 |                       (and (not (zero? n))
 65 |                            (is-even? (sub1 n))))])
 66 |     (is-odd? 11))
 67 | ]
 68 | 
 69 | 在介绍它的功能之前,我们先看看同样的例子如果用 @r[let] 会有什么后果:
 70 | 
 71 | @re[
 72 | (let ([is-even? (lambda (n)
 73 |                        (or (zero? n)
 74 |                            (is-odd? (sub1 n))))]
 75 |            [is-odd? (lambda (n)
 76 |                       (and (not (zero? n))
 77 |                            (is-even? (sub1 n))))])
 78 |     (is-odd? 11))
 79 | ]
 80 | 
 81 | 这里会提示 @r[_is-even?] 没有定义。@r[letrec] 引入了一个语法层面的概念 @r[locations],这个概念我们 @secref{advanced-racket} 那一章再讲。现在,我们只要这么理解:@r[letrec] 会先初始化每个要绑定的变量,为其放上一个占位符,等到引用的时候才真正开始计算。
 82 | 
 83 | 当然,这种方法并不适用于任何场合:被引用的表达式不能是立即求值的表达式,而是延迟计算的,如上例中的 @r[lambda]。几种错误的 @r[letrec] 用法:
 84 | 
 85 | @re[
 86 | (letrec ([x (+ y 10)]
 87 |          [y (+ z 10)]
 88 |          [z 10])
 89 |     x)
 90 | 
 91 | (letrec ([x (y 10)]
 92 |          [y (z 10)]
 93 |          [z (lambda (a) (+ a 10))])
 94 |     x)
 95 | ]
 96 | 
 97 | 正确的用法是:
 98 | 
 99 | @re[
100 | (letrec ([x (lambda () (+ (y) 10))]
101 |          [y (lambda () (+ (z) 10))]
102 |          [z (lambda () 10)])
103 |     (x))
104 | ]
105 | 
106 | @margin-note{如果你不畏艰险,执意要攻克这些语句,那么可以访问 @rh["http://docs.racket-lang.org/reference/let.html" "这里"] 阅读Racket的官方文档。}
107 | 
108 | 要完全理解 @r[letrec] 并非易事。就目前我们的能力而言,基本的 @r[let] 和 @r[let*] 在绝大多数场景下都足够用了。Racket还提供了其它一些绑定相关的语句:@r[letrec-values],@r[let-syntax],@r[letrec-syntax],@r[let-syntaxes],@r[letrec-syntaxes],@r[letrec-syntaxes+values]。如果你看得头晕目眩,不知所以,那么恭喜你,咱俩感觉一样一样的!我们暂且跳过它们,稍稍休息一下,换换脑子,然后进入下一个环节。
109 | 
110 | @section[#:tag "basics-grammer-cond"]{条件语句}
111 | 
112 | @margin-note{注意:这里说条件语句其实是不严谨的,应该是条件表达式。一切皆为表达式,不是吗?}
113 | 
114 | 在Racket里,条件语句也是函数。我们看最基本的条件语句:
115 | 
116 | @defproc[(if [test-expr (判断条件)]
117 |              [true-expr (当条件为真时执行的表达式)]
118 |              [false-expr (当条件为假时执行的表达式)])
119 |          value]{
120 | 对于 @r[if] 表达式,最终执行的表达式的结果为整个表达式的结果。下面是几个例子:
121 | 
122 | @rb[
123 | > (if (positive? -5) (error "doesn't get here") 2)
124 | 2
125 | > (if (member 2 (list 1 2 3 4)) 1 (error "doesn't get here"))
126 | 1
127 | > (if 'this-is-a-symbol "yes" "no")
128 | "yes"
129 | ]
130 | }
131 | 
132 | 再次强调:判断条件只要不是 @r[#f],任何其它值都等价于 @r[@t]。
133 | 
134 | 对于其它常见的语言,条件语句可以有多个分支,比如Python里有 @r[elif],使用如下:
135 | 
136 | @code-hl[#:lang "python"]{
137 | if score > 90:
138 |     return "A"
139 | elif score > 70:
140 |     return "B"
141 | elif score > 60:
142 |     return "Pass"
143 | else:
144 |     return "Not Pass"
145 | }
146 | 
147 | Racket的 @r[if] 没有多个分支,但是可以通过嵌套来完成相同的功能:
148 | 
149 | @rb[
150 | > (define score 55)
151 | > (if (> score 90) "A"
152 |                    (if (> score 70) "B"
153 |                                     (if (> score 60) "Pass"
154 |                                                      "Not Pass")))
155 | "Not Pass"
156 | ]
157 | 
158 | @margin-note{Racket里的 @r[cond] 跟其它语言中的 @r[switch] 类似,可以类比一下。}
159 | 
160 | 当然,同样的功能可以 @r[cond] 来完成。@r[cond] 的语法如下:
161 | 
162 | @specform/subs[#:literals (else =>)
163 |                (cond cond-clause ...)
164 |                ([cond-clause [test-expr then-body ...+]
165 |                              [else then-body ...+]
166 |                              [test-expr => proc-expr]
167 |                              [test-expr]])]
168 | 
169 | 在 @r[cond] 表达式中,每个 @r[_test-expr] 都按定义的顺序去求值并判断。如果判断结果为 @r[#f],对应的 @r[_body]s 会被忽略,下一个 @r[_test-expr] 会被求值并判断;在这个过程中,只要任何一个计算到的 @r[_test-expr] 的结果为 @r[#t],其 @r[_body]s 就会得到执行,执行的结果作为 @r[cond] 整个表达式的返回值。
170 | 
171 | 在 @r[cond] 表达式中,最后一个 @r[_test-expr] 可以被定义为 @r[else]。这样,如果前面的判断条件都测试失败,该 @r[_test-expr] 对应的 @r[_body]s 会作为缺省的行为得到执行。
172 | 
173 | 
174 | 上面的嵌套 @r[if] 的写法可以用 @r[cond] 表述得更加优雅一些:
175 | 
176 | @rb[
177 | > (cond [(> score 90) "A"]
178 |         [(> score 70) "B"]
179 |         [(> score 60) "Pass"]
180 |         [else "Not Pass"])
181 | "Not Pass"
182 | ]
183 | 
184 | 有一种特殊的 @r[_test-expr],它可以将自己的结果传递给 @r[_proc-expr] 作为参数进行处理,定义如下:
185 | 
186 | @specform[[test-expr => proc-expr]]
187 | 
188 | @margin-note{注意 @r[=>] 只能用于 @r[cond] 表达式中。}
189 | 
190 | 我们看一个例子:
191 | 
192 | @rb[
193 | > (cond
194 |    [(member 2 '(1 2 3 4)) => (lambda (l) (map sqr l))])
195 | '(4 9 16)
196 | > (member 2 '(1 2 3 4))
197 | '(2 3 4)
198 | ]
199 | 
200 | 通过这个例子,我们可以感受到条件表达式往往不返回 @r[#t],而尽可能返回一个有意义的值的妙处了。
201 | 
202 | 除了 @r[if] 和 @r[cond] 这样的标准条件表达式外,由于支持表达式的部分计算,@r[and] 和 @r[or] 也常常被用做条件表达式的简写。我们看一些例子:
203 | 
204 | @rb[
205 | > (and)
206 | #t
207 | > (and 1)
208 | 1
209 | > (and (values 1 2))
210 | 1
211 | 2
212 | > (and #f (error "doesn't get here"))
213 | #f
214 | > (and #t 5)
215 | 5
216 | > (or)
217 | #f
218 | > (or 1)
219 | 1
220 | > (or (values 1 2))
221 | 1
222 | 2
223 | > (or 5 (error "doesn't get here"))
224 | 5
225 | > (or #f 5)
226 | 5
227 | ]
228 | 
229 | 对于上面的根据 @r[score] 返回成绩评定的条件表达式,可以用 @r[or] 这么写:
230 | 
231 | @rb[
232 | > (or (and (> score 90) "A")
233 |       (and (> score 70) "B")
234 |       (and (> score 60) "Pass")
235 |       "Not Pass")
236 | "Not Pass"
237 | ]
238 | 
239 | 当然,这么写并不简单,读起来不如 @r[cond] 的形式舒服。考虑下面的嵌套 @r[if],用 @r[and] 表达更漂亮一些:
240 | 
241 | @margin-note{这个例子的逻辑是,退出编辑器时,如果文件修改了,则弹窗询问用户是否保存,如果用户选择是,则存盘退出,否则都直接退出。}
242 | 
243 | @rb[
244 | > (if (file-modified? f) (if (confirm-save?) (save-file)
245 |                                             #f)
246 |                         #f)
247 | > (and (file-modified? f) (confirm-save?) (save-file))
248 | ]
249 | 
250 | @section[#:tag "basics-grammer-loop"]{循环与递归}
251 | 
252 | 一般而言,函数式编程语言没有循环,只有递归,无论是什么形式的循环,其实都可以通过递归来完成。比如说这样一个循环:
253 | 
254 | @rb[
255 | > (for ([i '(1 2 3)])
256 |     (display i))
257 | 123
258 | ]
259 | 
260 | 可以这样用递归来实现:
261 | 
262 | @rb[
263 | > (define (for/recursive l f)
264 |     (if (> (length l) 0) (let ([h (car l)]
265 |                                [t (cdr l)])
266 |                             (f h)
267 |                             (for/recursive t f))
268 |         (void)))
269 | > (for/recursive '(1 2 3) display)
270 | 123
271 | ]
272 | 
273 | 然而,@r[for] 这样的循环结构毕竟要简单清晰一些,因此,Racket提供了多种多样的 @r[for] 循环语句。我们先看一个例子:
274 | 
275 | @rb[
276 | > (for ([i '(1 2 3 4)])
277 |     (displayln (sqr i)))
278 | 1
279 | 4
280 | 9
281 | 16
282 | ]
283 | 
284 | @margin-note{一个表达式,如果没有返回值,又没有副作用,那么它存在的必要何在?}
285 | 
286 | 注意 @r[for] 表达式返回值为 @r[void],并且一般而言,它是有副作用的。@r[for] 有很多无副作用的变体,如下所示:
287 | 
288 | @deftogether[[
289 | @defform[(for/list (clause ...) body ...+)]
290 | @defform[(for/hash (clause ...) body ...+)]
291 | @defform[(for/hasheq (clause ...) body ...+)]
292 | @defform[(for/hasheqv (clause ...) body ...+)]
293 | @defform[(for/vector (clause ...) body ...+)]
294 | @defform[(for/flvector (clause ...) body ...+)]
295 | @defform[(for/extflvector (clause ...) body ...+)]
296 | @defform[(for/and (clause ...) body ...+)]
297 | @defform[(for/or   (clause ...) body ...+)]
298 | @defform[(for/first (clause ...) body ...+)]
299 | @defform[(for/last (clause ...) body ...+)]
300 | @defform[(for/sum (clause ...) body ...+)]
301 | @defform[(for/product (clause ...) body ...+)]
302 | @defform[(for*/list (clause ...) body ...+)]
303 | @defform[(for*/hash (clause ...) body ...+)]
304 | @defform[(for*/hasheq (clause ...) body ...+)]
305 | @defform[(for*/hasheqv (clause ...) body ...+)]
306 | @defform[(for*/vector (clause ...) body ...+)]
307 | @defform[(for*/flvector (clause ...) body ...+)]
308 | @defform[(for*/extflvector (clause ...) body ...+)]
309 | @defform[(for*/and (clause ...) body ...+)]
310 | @defform[(for*/or   (clause ...) body ...+)]
311 | @defform[(for*/first (clause ...) body ...+)]
312 | @defform[(for*/last (clause ...) body ...+)]
313 | @defform[(for*/sum (clause ...) body ...+)]
314 | @defform[(for*/product (clause ...) body ...+)]
315 | ]]{
316 | 这里的 @r[_clause] 是 @r[[id sequence-expr]],可以有多个,当存在多个 @r[_clause] 时,不带 @bold{*} 的版本会同时处理每个 @r[_sequence-expr],并且,只要任何一个 @r[_sequence-expr] 结束,循环便结束;而带 @bold{*} 的版本会嵌套处理每个 @r[_sequence-expr],直至全部可能都被穷尽。@r[_for] 表达式的返回值根据 @bold{/} 后面的symbol确定,比如说 @r[for/list] 的返回值是一个列表。
317 | }
318 | 
319 | 我们通过一些例子来具体看看这些循环表达式如何使用:
320 | 
321 | @rb[
322 | > (for/list ([i '(1 2 3 4)]
323 |              [name '("goodbye" "farewell" "so long")])
324 |     (format "~a: ~a" i name))
325 | '("1: goodbye" "2: farewell" "3: so long")
326 | > (#,(hl for*/list) ([i '(1 2 3 4)]
327 |              [name '("goodbye" "farewell" "so long")])
328 |     (format "~a: ~a" i name))
329 | '("1: goodbye"
330 |   "1: farewell"
331 |   "1: so long"
332 |   "2: goodbye"
333 |   "2: farewell"
334 |   "2: so long"
335 |   "3: goodbye"
336 |   "3: farewell"
337 |   "3: so long"
338 |   "4: goodbye"
339 |   "4: farewell"
340 |   "4: so long")
341 | ]
342 | 
343 | 注意看 @r[for*/list] 和 @r[for/list] 的区别。再看几个例子:
344 | 
345 | @rb[
346 | > (for/product ([i '(1 2 3)]
347 |                 [j '(4 5 6)])
348 |     (* i j))
349 | 720
350 | > (for/sum ([i '(1 2 3)]
351 |             [j '(4 5 6)])
352 |     (* i j))
353 | 32
354 | > (for/last ([i '(1 2 3)]
355 |              [j '(4 5 6)])
356 |     (* i j))
357 | 18
358 | > (for/hash ([i '(1 2 3)]
359 |              [j '(4 5 6)])
360 |     (values i j))
361 | '#hash((1 . 4) (2 . 5) (3 . 6))
362 | ]
363 | 
364 | @r[for] 循环还有很多内置的函数来获取循环中的值,比如 @r[in-range],@r[in-naturals] 等等,如下例所示:
365 | 
366 | @rb[
367 | > (for/sum ([i 10]) (sqr i))
368 | 285
369 | > (for/list ([i (in-range 10)])
370 |     (sqr i))
371 | '(0 1 4 9 16 25 36 49 64 81)
372 | > (for ([i (in-naturals)])
373 |     (if (= i 10)
374 |         (error "too much!")
375 |         (display i)))
376 | 0123456789
377 | too much!
378 | ]
379 | 
380 | 我们可以看到,@r[in-naturals] 会生成一个无穷的序列,除非在 @r[_body] 中抛出异常,否则循环无法停止。这个特性可以用于类似其它语言的 @r[_while(1)] 这样的无限循环。
381 | 
382 | 很多时候,循环不是必须的。Racket是一门函数式编程语言,有很多优秀的用于处理 @r[_sequence] 的函数,如 @r[map],@r[filter],@r[foldl] 等等,当你打算使用 @r[for] 循环时,先考虑一下这些函数是否可以使用。
383 | 
384 | 更多有关 @r[for] 循环的细节,请参考 @rh["http://docs.racket-lang.org/guide/for.html" "Racket官方文档:iterations and comprehensions"]。


--------------------------------------------------------------------------------
/docs/03-practical-programs/03-program-2048.scrbl:
--------------------------------------------------------------------------------
  1 | #lang scribble/doc
  2 | 
  3 | @(require (for-label racket)
  4 |           scribble/manual
  5 |           "../../util/common.rkt")
  6 | 
  7 | @title[#:tag "practical-2048"]{做个2048小游戏}
  8 | 
  9 | @rh["http://gabrielecirulli.github.io/2048/" "2048游戏"] 是个曾经风靡一时的javascript小游戏,玩家在一个4x4的棋盘上,通过上下左右四个键移动棋盘上的棋子,规则是这样:
 10 | 
 11 | @itemlist[
 12 | @item{开始时棋盘上随机有两个棋子,2或4都有可能,其它为空}
 13 | @item{玩家可以用方向键移动棋子。移动时所有棋子一起整体移动到用户按下的方向,直到不能移动为止}
 14 | @item{在移动方向上,相邻的两个数字如果相同,则合并为一个,合并后的结果为两个数字之和(即乘以2)}
 15 | @item{每次合并的结果作为得分,累加起来}
 16 | @item{每移动一次,棋盘上空闲的位置会随机出现2或者4,出现2的几率(90%)要远大于4(10%)}
 17 | @item{当棋子布满棋盘,四个方向移动时又无法进行合并,则游戏结束}
 18 | ]
 19 | 
 20 | 感谢racket的 @r[2htdp/universe] package,我们可以很轻松地制作这样一个游戏。
 21 | 
 22 | @section[#:tag "practical-2048-algorithm"]{数据结构和算法}
 23 | 
 24 | 对于这样一个4x4的棋盘,最直观的想法是用一个嵌套的 @r[list] 来表达:
 25 | 
 26 | @re[
 27 | (define (make-board n)
 28 |   (make-list n (make-list n 0)))
 29 | (make-board 4)
 30 | ]
 31 | 
 32 | @margin-note{用过python的人应该知道,可以使用 @bold{random.choice} 从一个列表中随机选择。racket貌似没有这样的函数,不过没关系,我们自己写}
 33 | 我们需要在棋盘上的空闲位置随机放两个棋子,所以我们先要能够随机从 @r['(2 2 2 2 2 2 2 2 2 4)] 中挑一个出来做棋子:
 34 | 
 35 | @re[
 36 | (define PIECE_DIST '(2 2 2 2 2 2 2 2 2 4))
 37 | (define (choice l)
 38 |   (if (list? l) (list-ref l (random (length l)))
 39 |       (vector-ref l (random (vector-length l)))))
 40 | 
 41 | (define (get-a-piece)
 42 |   (choice PIECE_DIST))
 43 | 
 44 | (get-a-piece)
 45 | ]
 46 | 
 47 | 接下来我们要随机找出一个空闲的位置:
 48 | 
 49 | @re[
 50 | ; e.g. '((2 0) (2 4)) -> #t
 51 | (define (avail? lst)
 52 |   (if (list? lst)
 53 |       (ormap avail? lst)
 54 |       (zero? lst)))
 55 | 
 56 | ; e.g. '((2 2) (2 0) (0 0)) -> '(1 2)
 57 | (define (get-empty-refs lst zero-fun?)
 58 |   (for/list ([item lst]
 59 |              [i (range (length lst))]
 60 |              #:when (zero-fun? item))
 61 |     i))
 62 | 
 63 | ; e.g. '(0 2 0 0) -> '(0 2 0 2)
 64 | ; e.g. '((0 2 0 0) (2 4 8 16) (0 4 4 8) (2 0 0 0)) ->
 65 | ;      '((0 2 0 0) (2 4 8 16) (0 4 4 8) (2 0 2 0))
 66 | (define (put-random-piece lst)
 67 |   (if (avail? lst)
 68 |       (if (list? lst)
 69 |           (let* ([i (choice (get-empty-refs lst avail?))]
 70 |                  [v (list-ref lst i)])
 71 |             (append (take lst i)
 72 |                     (cons (put-random-piece v) (drop lst (add1 i)))))
 73 |           (get-a-piece))
 74 |       lst))
 75 | 
 76 | (put-random-piece '((0 2 0 0) (2 4 8 16) (0 4 4 8) (2 0 0 0)))
 77 | ]
 78 | 
 79 | @r[_avail?] 递归查看一个棋盘或者棋盘上的一行是否有 @r[0],来决定是否可以往上放棋子。@r[_get-empty-refs] 获取当前棋盘(或者一行)上面的的可放棋子的行(或者行中元素)的索引列表,以便于我们随机摆放棋子。最后,我们可以递归选择一个随机的行,随机的列,放入一个随机的棋子。
 80 | 
 81 | @re[
 82 | (get-empty-refs '(0 0 0 2) avail?)
 83 | (get-empty-refs '((2 2) (2 0) (0 0)) avail?)
 84 | (put-random-piece '((0 2 0 0) (2 4 8 16) (0 4 4 8) (2 0 0 0)))
 85 | ]
 86 | 
 87 | 这样,我们就可以初始化棋盘了:
 88 | 
 89 | @re[
 90 | (define (init-board n)
 91 |   (put-random-piece (put-random-piece (make-board n))))
 92 | 
 93 | (init-board 4)
 94 | (init-board 4)
 95 | (init-board 4)
 96 | (init-board 4)
 97 | ]
 98 | 
 99 | 接下来就是按照规则合并棋子:
100 | 
101 | @re[
102 | (define (merge row)
103 |   (cond [(<= (length row) 1) row]
104 |         [(= (first row) (second row))
105 |          (cons (* 2 (first row)) (merge (drop row 2)))]
106 |         [else (cons (first row) (merge (rest row)))]))
107 | (merge '(2 2 2 4 4 4 8))
108 | ]
109 | 
110 | @r[_merge] 会从第一个元素起递归处理列表中的所有元素,如果相等就两两合并,然后返回合并后的列表。但是,如果两个数值相同的元素,比如:@r['(2 0 2 0)],中间隔着 @r[0] 怎么办?我们可以用 @r[filter] 返回非 @r[0] 的元素,然后再把 @r[0] 补齐。这就是 @r[_move-row] 要做的事情:
111 | 
112 | @re[
113 | (define (move-row row v left?)
114 |    (if left?
115 |        (let* ([n (length row)]
116 |               [l (merge (filter (λ (x) (not (zero? x))) row))]
117 |               [padding (make-list (- n (length l)) v)])
118 |          (append l padding))
119 |        (reverse (move-row (reverse row) v (not left?)))))
120 | (define (move lst v left?)
121 |   (map (λ (x) (move-row x v left?)) lst))
122 | (move '((0 2 0 0) (2 4 8 16) (0 4 4 8) (2 0 0 0)) 0 #t)
123 | ]
124 | 
125 | 进一步,我们实现四个方向上的移动:
126 | 
127 | @re[
128 | (define (move-left lst)
129 |   (put-random-piece (move lst 0 #t)))
130 | 
131 | (define (move-right lst)
132 |   (put-random-piece (move lst 0 #f)))
133 | 
134 | (define (transpose lsts)
135 |   (apply map list lsts))
136 | 
137 | (define (move-up lst)
138 |   ((compose1 transpose move-left transpose) lst))
139 | 
140 | (define (move-down lst)
141 |   ((compose1 transpose move-right transpose) lst))
142 | ]
143 | 
144 | @r[apply] 是个神奇的函数,如果你学过其它函数式编程语言,或者经常写javascript,那么你一定知道 @r[apply]。它能够让传递给函数的列表展开,成为函数执行时的若干个参数。下面的两个表达式是等价的:
145 | 
146 | @re[
147 | (apply map list '((1 2) (3 4)))
148 | (map list '(1 2) '(3 4))
149 | ]
150 | 
151 | @margin-note{请自行类比 @r[add1] 和 @r[add]}
152 | 
153 | @margin-note{关于函数链的更多信息,请参考 @rh["http://en.wikipedia.org/wiki/Method_chaining" "wikipedia: Method_chaining"]}
154 | 
155 | @r[compose1] 是 @r[compose] 的只接受一个参数的特殊形式。@r[compose] 是函数式编程中另一个很重要的函数,它能够把传入的若干个接收同样参数并返回和参数相同形式的返回值的函数组合成一个 @bold{函数链},就像一个新函数一样。在函数链上,代码的执行是由内到外,如下所示:
156 | 
157 | @re[
158 | ((compose1 - sqrt add1) 8)
159 | ((compose1 add1 sqrt -) 8)
160 | ]
161 | 
162 | 更多函数式编程的内容,请参考 @secref{advanced-racket-fp}。我们回到2048游戏的算法设计中。当任意一个方向上移动的结果和移动前相同,意味着游戏结束:
163 | 
164 | @re[
165 | (define ALL-OPS (list move-right move-down move-left move-up))
166 | 
167 | (define (finished? lst)
168 |   (andmap (λ (op) (equal? lst (op lst))) ALL-OPS))
169 | (finished? '((2 8 4 2) (8 4 8 16) (4 32 2 4) (2 16 4 2)))
170 | ]
171 | 
172 | 我们测试一下,随机走,能走多少步,游戏结束:
173 | 
174 | @re[
175 | (define (test-play lst step)
176 |   (if (and (not (avail? lst)) (finished? lst))
177 |       (values lst step)
178 |       (test-play ((choice ALL-OPS) lst) (add1 step))))
179 | (test-play (init-board 4) 0)
180 | (test-play (init-board 4) 0)
181 | (test-play (init-board 4) 0)
182 | ]
183 | 
184 | 4x4的棋盘不够过瘾,我们来个大的:
185 | 
186 | @re[
187 | (test-play (init-board 6) 0)
188 | ]
189 | 
190 | OK,现在这个游戏的基本算法就有了,geek们已经开始可以通过 @r[_move-xx] 等 API 在 @(drr) 中进行游戏了。
191 | 
192 | @section[#:tag "practical-2048-game"]{制作游戏}
193 | 
194 | 我们先来点体力活,定义游戏的配色。由于互联网世界的颜色表示均使用十六进制的hex码,而 @r[2htdp/image] 里使用的 @r[color] 是用RGBA定义,因此我们需要做个颜色的转换:
195 | 
196 | @re[
197 | (define (hex->rgb hex [alpha 255])
198 |   (define r (regexp-match #px"^#(\\w{2})(\\w{2})(\\w{2})$" hex))
199 |   (define (append-hex s) (string-append "#x" s))
200 |   (define (color-alpha c) (apply color (append c (list alpha))))
201 |   (if r
202 |       (color-alpha (map (compose1 string->number append-hex) (cdr r)))
203 |       #f))
204 | (hex->rgb "#aabbcc" #xba)
205 | ]
206 | 
207 | 有了这个函数,我们就可以很方便地定义配色和棋子大小:
208 | 
209 | @re[
210 | (define ALPHA #xb8)
211 | 
212 | (define GRID-COLOR (hex->rgb "#bbada0"))
213 | 
214 | (define TILE-BG
215 |   (make-hash (map (λ (item) (cons (first item) (hex->rgb (second item))))
216 |        '((0    "#ccc0b3") (2    "#eee4da") (4    "#ede0c8")
217 |          (8    "#f2b179") (16   "#f59563") (32   "#f67c5f")
218 |          (64   "#f65e3b") (128  "#edcf72") (256  "#edcc61")
219 |          (512  "#edc850") (1024 "#edc53f") (2048 "#edc22e")))))
220 | 
221 | (define TILE-FG 'white)
222 | 
223 | (define TILE-SIZE 80)
224 | (define TILE-TEXT-SIZE 50)
225 | (define MAX-TEXT-SIZE 65)
226 | (define TILE-SPACING 5)
227 | ]
228 | 
229 | 接下来就是显示一个棋子。不同数值的棋子的颜色不同,而值为 @r[0] 的棋子不显示数字。我们还得处理一些显示的问题,比如说 @r[2048] 这样的数值,如果以预定义的大小显示,则会超出棋子的大小,所以我们需要 @r[scale]:
230 | 
231 | @re[
232 | (define (make-tile n)
233 |   (define (text-content n)
234 |     (if (zero? n) ""
235 |         (number->string n)))
236 | 
237 |   (overlay (let* ([t (text (text-content n) TILE-TEXT-SIZE TILE-FG)]
238 |                   [v (max (image-width t) (image-height t))]
239 |                   [s (if (> v MAX-TEXT-SIZE) (/ MAX-TEXT-SIZE v) 1)])
240 |              (scale s t))
241 |            (square TILE-SIZE 'solid (hash-ref TILE-BG n))
242 |            (square (+ TILE-SIZE (* 2 TILE-SPACING)) 'solid GRID-COLOR)))
243 | (make-tile 2048)
244 | (make-tile 2)
245 | (make-tile 0)
246 | ]
247 | 
248 | 如果你读了 @other-doc['(lib "scribblings/quick/quick.scrbl")] 的话,你会对 @r[racket/pict] 中的 @r[hc-append] 和 @r[vc-append] 两个函数有印象。可惜这两个函数不接受我们使用 @r[2htdp/image] 中的各种方式制作出来的 image,所以 @r[_make-tile] 生成的图片无法使用这两个函数,那我们就只好自己写了:
249 | 
250 | @re[
251 | (define (image-append images get-pos overlap)
252 |   (if (<= (length images) 1)
253 |       (car images)
254 |       (let* ([a (first images)]
255 |              [b (second images)]
256 |              [img (apply overlay/xy
257 |                          (append (list a) (get-pos a overlap) (list b)))])
258 |         (image-append (cons img (drop images 2)) get-pos overlap))))
259 | 
260 | (define (hc-append images [overlap 0])
261 |   (image-append images
262 |                 (λ (img o) (list (- (image-width img) o) 0))
263 |                 overlap))
264 | 
265 | (define (vc-append images [overlap 0])
266 |   (image-append images
267 |                 (λ (img o) (list 0 (- (image-height img) o)))
268 |                 overlap))
269 | (hc-append (map make-tile '(0 2 4 8)) 5)
270 | (vc-append (map make-tile '(1024 256 4 8)) 5)
271 | ]
272 | 
273 | 合并的方式很简单 —— 先将列表中头两个图片合并成一个,和剩下的图片组成新的列表,然后递归下去,直至合并成一张。@r[_hc-append] 和 @r[_vc-append] 其实就是调用 @r[overlay/xy] 的参数不同,所以这里抽象出来一个 @r[_image-append]。
274 | 
275 | 有了这两个函数,那么展示一个棋盘就轻而易举了:
276 | 
277 | @re[
278 | (define (show-board b)
279 |   (let ([images (for/list ([row b])
280 |                   (hc-append (map make-tile row) TILE-SPACING))])
281 |     (vc-append images TILE-SPACING)))
282 | (show-board (init-board 4))
283 | (show-board (init-board 6))
284 | ]
285 | 
286 | @section[#:tag "practical-2048-animation"]{让游戏运行起来}
287 | 
288 | 在 @r[2htdp/universe] 中,提供了 @r[big-bang] 函数。我们在之前的章节中见过这个函数。@r[big-bang] 接受键盘和鼠标事件,还会定期发出 @r[on-tick] 事件。@r[big-bang] 中所有函数都会接受一个调用者指定的参数,这个参数在 @r[big-bang] 的第一个参数中提供初值,并且根据各种事件产生后处理函数的返回值变化。你可以认为这是一个有关游戏状态的参数。在2048游戏中,棋盘就是一个游戏状态,其它的如用户得分,总共走下的步数等等,都属于游戏状态。为简单起见,我们就认为这个游戏的状态就是我们之前反复操作的棋盘。
289 | 
290 | @margin-note{如果用户敲了其它键,我们返回一个对棋盘状态什么也不做的函数}
291 | 首先我们先把键盘操作映射成为我们之前制作好的函数:
292 | 
293 | @re[
294 | (require 2htdp/universe)
295 | (define (key->ops a-key)
296 |   (cond
297 |     [(key=? a-key "left")  move-left]
298 |     [(key=? a-key "right") move-right]
299 |     [(key=? a-key "up")    move-up]
300 |     [(key=? a-key "down")  move-down]
301 |     [else (λ (x) x)]))
302 | ]
303 | 
304 | 接下来我们定义整个游戏:
305 | 
306 | @re[
307 | (define (show-board-over b)
308 |   (let* ([board (show-board b)]
309 |          [layer (square (image-width board) 'solid (color 0 0 0 90))])
310 |     (overlay (text "Game over!" 40 TILE-FG)
311 |              layer board)))
312 | 
313 | (show-board-over (init-board 5))
314 | 
315 | (define (change b key)
316 |   ((key->ops key) b))
317 | 
318 | (define (start n)
319 |   (big-bang (init-board n)
320 |             (to-draw show-board)
321 |             (on-key change)
322 |             (stop-when finished? show-board-over)
323 |             (name "2048 - racket")))
324 | ]
325 | 
326 | @r[_(init-board n)] 初始化一个棋盘,这个棋盘状态会传入 @r[_show_board],@r[_change],@r[_finished?] 和 @r[_show-board-over] 中。同样,这些函数都会返回一个新的棋盘状态,供 @r[big-bang] 下次事件发生的时候使用。
327 | 
328 | 当键盘事件发生时,@r[_change] 会被调用,此时,棋盘会按照用户的按键进行变化;变化完成后,结果会通过 @r[_show-board] 展示出来;同时,每次状态改变发生后,@r[big-bang] 都会检查是否 @r[_finished?],如果为 @r[#t],则调用 @r[_show-board-over]。
329 | 
330 | 现在,你可以调用 @r[(start 4)] 开始游戏了。是不是非常简单?
331 | 
332 | 如果你想查阅本节所述的代码,可以打开 @rh["https://github.com/tyrchen/racket-book/blob/master/code/my-2048.rkt" "my-2048.rkt"]。有部分公用的代码我将其放在了 @rh["https://github.com/tyrchen/racket-book/blob/master/code/shared/utility.rkt" "utility.rkt"] 中,日后的例子中可能还会看到这些函数的倩影。
333 | 
334 | 如果你已经将 @rh["https://github.com/tyrchen/racket-book" "tyrchen/racket-book"] 这个 repo 克隆到了本地,那么你可以在 @bold{code} 目录下运行:
335 | 
336 | @code-hl[#:lang "bash"]{
337 | $ racket my-2048.rkt
338 | }
339 | 
340 | 稍待片刻,游戏就会开始运行。
341 | 
342 | @section[#:tag "practical-2048-more"]{课后作业}
343 | 
344 | 这个游戏和原版游戏相比,山寨度也就达到了70%。目前还有以下遗憾:
345 | 
346 | @itemlist[
347 | @item{没有记录和显示用户的得分,以及走过的步数}
348 | @item{没有动画效果,游戏的体验比较生硬}
349 | @item{没有计时,无法提供更多的玩法}
350 | @item{没有音效,略嫌枯燥}
351 | ]
352 | 
353 | 这些遗憾就交由感兴趣的读者完成。
354 | 


--------------------------------------------------------------------------------
/docs/02-basics/02-data.scrbl:
--------------------------------------------------------------------------------
  1 | #lang scribble/doc
  2 | 
  3 | @(require (for-label racket)
  4 |           scribble/manual
  5 |           "../../util/common.rkt")
  6 | 
  7 | @title[#:tag "basics-data"]{基本数据结构}
  8 | 
  9 | 了解了最基本的控制结构,我们来看看Racket提供了哪些数据结构。@rh["http://en.wikipedia.org/wiki/Algorithms_%2B_Data_Structures_%3D_Programs" "Wirth 说过"]:
 10 | 
 11 | @verbatim[#:indent 4]{
 12 | Program = Algorithm + Data Structure
 13 | }
 14 | 
 15 | 掌握了一门语言支持的数据结构,你就已经掌握了这门语言的一半的精髓。
 16 | 
 17 | @section[#:tag "basics-data-number"]{数值}
 18 | 
 19 | 数值是Racket的最基本的类型。Racket支持的数值类型非常丰富:整数,浮点数(实数),虚数,有理数(分数)等等。我们看一些例子:
 20 | 
 21 | @re[
 22 | 1234
 23 | (+ 1000000000000000000000000000000000000000000000000000000000000 4321)
 24 | 1.41414141414141414141414141414141414141414141414141414141414
 25 | 1414141414141414141414141414.14141414141414141414141414141414
 26 | (/ 2 3)
 27 | (/ 2 3.0)
 28 | 1+2i
 29 | 
 30 | (number? 1.4)
 31 | (number? (/ 9 10))
 32 | (number? 1+2i)
 33 | (number? 1.4e27)
 34 | 
 35 | (exact->inexact 1/3)
 36 | (floor 1.9)
 37 | (ceiling 1.01)
 38 | (round 1.5)
 39 | (eval:alts @#,racketvalfont{#b111} #b111)
 40 | (eval:alts @#,racketvalfont{#o777} #o777)
 41 | (eval:alts @#,racketvalfont{#xdeadbeef} #xdeadbeef)
 42 | ]
 43 | 
 44 | 最后的三个例子里展示了Racket可以通过 @bold{#b},@bold{#o},@bold{#x} 来定义二进制,八进制和十六进制的数字。
 45 | 
 46 | @section[#:tag "basics-data-string"]{string}
 47 | 
 48 | 字符串是基本上所有语言的标配,Racket也不例外。我们看看这些例子:
 49 | 
 50 | @re[
 51 | (string? "Hello world")
 52 | (string #\R #\a #\c #\k #\e #\t)
 53 | (make-string 10 #\c)
 54 | (string-length "Tyr Chen")
 55 | (string-ref "Apple" 3)
 56 | (substring "Less is more" 5 7)
 57 | (string-append "Hello" " " "world!")
 58 | (string->list "Eternal")
 59 | (list->string '(#\R #\o #\m #\e))
 60 | ]
 61 | 
 62 | Racket还提供了一个关于 @r[string] 的库 @r[racket/string],可以 @r[(require racket/string)] 后使用:
 63 | 
 64 | @re[
 65 | (string-join '["this" "is" "my" "best" "part"])
 66 | (string-join '("随身衣物" "充电器" "洗漱用品") ","
 67 |                #:before-first "打包清单:"
 68 |                #:before-last "和"
 69 |                #:after-last "等等。")
 70 | (string-split "  foo bar  baz \r\n\t")
 71 | (string-split "股票,开盘价,收盘价,最高价,最低价" ",")
 72 | (string-trim "  foo bar  baz \r\n\t")
 73 | (string-replace "股票,开盘价,收盘价,最高价,最低价" "," "\n")
 74 | (string-replace "股票,开盘价,收盘价,最高价,最低价" "," "\n" #:all? #f)
 75 | ]
 76 | 
 77 | 延伸阅读:更多和 @r[string] 相关的函数,可以参考 @rdoc-ref{strings}。
 78 | 
 79 | @section[#:tag "basics-data-list"]{列表}
 80 | 
 81 | @r[list] 是Lisp(LISt Processor)的精髓所在,其重要程度要比 @r[lambda] 更胜一筹。其实软件无非是一个处理输入输出的系统:一组输入经过这个系统的若干步骤,变成一组输出。「一组输入」是列表,「若干步骤」是列表,「一组输出」也是列表。所以程序打交道的对象大多是列表。
 82 | 
 83 | Lisp里最基本的操作是 @r[car](读/ˈkɑr/) 和 @r[cdr](读/ˈkʌdər/),他们是操作 @r[cons] 的原子操作。@r[cons] 也被称为pair,包含两个值,@r[car] 获取第一个值,@r[cdr] 获取第二个值。Racket继承了Lisp的这一特性:
 84 | 
 85 | @re[
 86 | (cons 'x 'y)
 87 | '(10 . 20)
 88 | (define pair (cons 10 20))
 89 | (car pair)
 90 | (cdr pair)
 91 | ]
 92 | 
 93 | @margin-note{@r[cadar l] 是 @r[(car (cdr (car (l))))] 的简写}
 94 | 
 95 | 如果把第二个元素以后的内容看作一个列表,列表也可以被看作是 @r[cons]。我们做几个实验:
 96 | 
 97 | @re[
 98 | (cons 1 (cons 2 3))
 99 | (cons 1 (cons 2 (cons 3 '())))
100 | (cons 1 (cons 2 empty))
101 | (list 1 2 3)
102 | (define l1 '(1 2 3 4 5 6 7 8))
103 | (car l1)
104 | (cdr l1)
105 | (car (cdr l1))
106 | (cadr l1)
107 | (cdr (cdr l1))
108 | (cddr l1)
109 | (caddr l1)
110 | (cddddr l1)
111 | ]
112 | 
113 | 在Racket里,@r[_pair] 不是 @r[_list],但 @r[_list] 是 @r[_pair]:
114 | 
115 | @re[
116 | (pair? '(1 . 2))
117 | (list? '(1 . 2))
118 | (pair? '(1 2))
119 | (list? '(1 2))
120 | ]
121 | 
122 | 那么,@r[(cons 1 (cons 2 3))] 代表什么呢?为什么结果这么奇特,不是一个正常的列表?我们暂且放下这么疑问,留待以后再回答这个问题。
123 | 
124 | 我们看看主要的列表操作的函数:
125 | 
126 | @re[
127 | (define l (list 234 123 68 74 100 1 3 5 8 4 2))
128 | (first l)
129 | (rest l)
130 | (take l 4)
131 | (drop l 4)
132 | (split-at l 4)
133 | (takef l even?)
134 | (dropf l even?)
135 | (length l)
136 | (list-ref l 0)
137 | (list-tail l 4)
138 | (append l '(0 1 2))
139 | (reverse l)
140 | 
141 | (flatten (list '(1) '(2) '(3)))
142 | (remove-duplicates '(1 2 3 2 4 5 1))
143 | 
144 | (filter (lambda (x) (> x 100)) l)
145 | (partition (lambda (x) (< x (first l))) l)
146 | ]
147 | 
148 | @r[partition] 是个有趣的函数,它把列表按照你给定的条件分成两个列表:满足条件的;不满足条件的。嗯,让我们来利用这一函数,写个自己的 @r[_quicksort] 函数:
149 | 
150 | @re[
151 | (define l (list 234 123 68 74 100 1 3 5 8 4 2))
152 | (define (qsort1 l)
153 |   (if (or (empty? l) (empty? (cdr l))) l
154 |       (let*-values ([(key) (car l)]
155 |                      [(small big)
156 |                       (partition (lambda (x) (< x key))
157 |                                  (cdr l))])
158 |         (append (qsort1 small)
159 |                 (list key)
160 |                 (qsort1 big)))))
161 | 
162 | (qsort1 l)
163 | ]
164 | 
165 | 对于给定的列表,我们拿出第一个元素µ,把剩下里的元素分成两份:小于µ的元素列表和大于µ的元素列表。然后对整个过程进行递归,直到所有元素排完。我们虽然实现了 @r[_quicksort] 的基本功能,但目前只能实现数字的排序,如果是其它数据结构呢?这个实现太不灵活。让我们稍稍修改一下,把比较的逻辑抽取出来:
166 | 
167 | @rb[
168 | (define (qsort2 l #,(hl cmp))
169 |   (if (or (empty? l) (empty? (cdr l))) l
170 |       (let*-values ([(key) (car l)]
171 |                      [(small big)
172 |                       (partition (lambda (x)(#,(hl cmp) x key))
173 |                                  (cdr l))])
174 |         (append (qsort2 small #,(hl cmp))
175 |                 (list key)
176 |                 (qsort2 big #,(hl cmp))))))
177 | 
178 | > (qsort2 l <)
179 | '(1 2 3 4 5 8 68 74 100 123 234)
180 | > (qsort2 l >)
181 | '(234 1 2 3 4 5 8 68 74 100 123)
182 | > (qsort2 (list "hello" "world" "baby") string (qsort3 (list '(3 "关上冰箱") '(1 "打开冰箱") '(2 "把大象塞到冰箱里")) < car)
207 | '((1 "打开冰箱") (2 "把大象塞到冰箱里") (3 "关上冰箱"))
208 | > (qsort3 l < (lambda (x) x))
209 | '(1 2 3 4 5 8 68 74 100 123 234)
210 | ]
211 | 
212 | 现在,功能是丰满了,但函数使用起来越来越麻烦,最初的 @r[(qsort1 l)] 变成了如今的 @r[(qsort3 l < (lambda (x) x))],好不啰嗦。还好,Racket提供了函数的可选参数,我们再来修订一下:
213 | 
214 | @rb[
215 | (define (qsort l [cmp <] [key (lambda (x) x)])
216 |   (if (or (empty? l) (empty? (cdr l))) l
217 |       (let*-values ([(item) (car l)]
218 |                     [(small big)
219 |                      (partition (lambda (x) (cmp (key x) (key item)))
220 |                                 (cdr l))])
221 |         (append (qsort small cmp key)
222 |                 (list item)
223 |                 (qsort big cmp key)))))
224 | 
225 | > (qsort l)
226 | '(1 2 3 4 5 8 68 74 100 123 234)
227 | > (qsort l >)
228 | '(234 123 100 74 68 8 5 4 3 2 1)
229 | ]
230 | 
231 | 费了这么多口舌,其实Racket自己就提供了一个全功能的 @r[sort]:
232 | 
233 | @re[
234 | (define l (list 234 123 68 74 100 1 3 5 8 4 2))
235 | (sort l <)
236 | (sort (list "hello" "world" "babay") stringlist v1)
252 | (list->vector '(3 2 1))
253 | (vector-ref v1 0)
254 | (for ([i #(1 2 3)]) (display i))
255 | (vector-take v1 1)
256 | (vector-drop v1 1)
257 | (vector-split-at v1 1)
258 | (vector-length v1)
259 | (vector-append v1 #(0 1 2))
260 | ]
261 | 
262 | 可以看到,@r[vector] 的使用几乎和 @r[list] 一致,甚至函数的命名都很相似。
263 | 
264 | 延伸阅读:更多和 @r[vector] 相关的函数,可以参考 @rdoc-ref{vectors}。
265 | 
266 | @section[#:tag "basics-data-hash"]{哈希表}
267 | 
268 | 哈希表(@r[hash])是目前几乎每种语言都会内置的数据结构。在Python里,它叫 @bold{dict};在golang中,它叫 @bold{map}。哈希表是由一系列键值对(Key-value pair)组成的数据结构,具备和数组相同的 @bold{O(1)}存取能力(最佳情况)。标准的哈希表的实现方式如下:
269 | 
270 | @image["assets/images/hash_table.png"]
271 | 
272 | @margin-note{更多有关哈希表的算法和存储方式,请参考:@rh["http://en.wikipedia.org/wiki/Hash_table" "Hash table"]}
273 | 
274 | 当冲突产生时,一般会使用链表来保存冲突链。好的哈希算法会让系统中最长的冲突链尽可能地短。
275 | 
276 | 我们看看Racket中的哈希表如何定义和访问:
277 | 
278 | @re[
279 | (define ht (hash "key1" "value1" 'key2 1234 3 (list 1 2) (list 'key4) 'value4))
280 | (hash-ref ht "key1")
281 | (hash-ref ht 'key2)
282 | (hash-ref ht 3)
283 | (hash-ref ht (list 'key4))
284 | (hash-ref ht 'key5)
285 | ]
286 | 
287 | 哈希表的 @r[_key] 和 @r[_value] 可以是任意类型,访问不存在的 @r[_key] 会抛出异常。通过上面的例子,我们可以看到,哈希表的简写符号是 @bold{#hash},@r[_key] 和 @r[_value] 间使用 @bold{.} 来连接。
288 | 
289 | 由于哈希表的 @r[_key] 可以是任意类型,这就带来一个问题:@r[_key] 究竟怎么比较?是使用 @r[equal?](相同的值) 还是 @r[eq?](相同的对象),还是 @r[eqv?](字符和数字相同或对象相同)?我们先看看这三种比较方式的区别:
290 | 
291 | @re[
292 | (eq? (+ 1 1) (+ 1 1))
293 | (eqv? (+ 1 1) (+ 1 1))
294 | (equal? (+ 1 1) (+ 1 1))
295 | (eq? (sqrt 1001) (sqrt 1001))
296 | (eqv? (sqrt 1001) (sqrt 1001))
297 | (equal? (sqrt 1001) (sqrt 1001))
298 | (eq? (list 1 2) (list 1 2))
299 | (eqv? (list 1 2) (list 1 2))
300 | (equal? (list 1 2) (list 1 2))
301 | (eq? (make-string 3 #\z) (make-string 3 #\z))
302 | (eqv? (make-string 3 #\z) (make-string 3 #\z))
303 | (equal? (make-string 3 #\z) (make-string 3 #\z))
304 | ]
305 | 
306 | 对应的,哈希表也可以使用 @r[hash],@r[hasheq],@r[hasheqv] 来创建,相应的简写符号也不同。不过,通过这三种方式创建的哈希表是不可修改的,可很多使用哈希表的场景都需要修改哈希表,怎么办?Racket提供了 @r[make-hash], @r[make-hasheqv], 以及 @r[make-hasheq]。我们看例子:
307 | 
308 | @re[
309 | (define ht (make-hash))
310 | (hash-set! ht "key1" 'v1)
311 | (hash-set! ht (list 1 2) #hash(("k" . "v")))
312 | (hash-ref (hash-ref ht (list 1 2)) "k")
313 | ]
314 | 
315 | 延伸阅读:更多和 @r[hash] 相关的函数,可以参考 @rdoc-ref{hashtables}。
316 | 
317 | @section[#:tag "basics-data-symbol"]{symbol}
318 | 
319 | 在 @secref{basics-data-list} 的例子中,你也许会注意到列表的简写形式:相对于 @r[(list 1 2 3)],@r['(1 2 3)] 能更简单地表现一个列表。那么,如果要用它定义嵌套的列表呢?该怎么定义?是 @r['('(1 2) '(3 4))] 么?让Racket直接告诉我们:
320 | 
321 | @re[
322 | (list (list 1 2) (list 3 4))
323 | ]
324 | 
325 | 咦!仅需要一个简单的 @(bq),我们就可以定义嵌套的列表。那么,@(bq)究竟是个什么东西?为什么它能够支持列表的嵌套?而不是我们想象的那样去定义?让我们多尝试一些代码,探寻 @(bq) 的秘密:
326 | 
327 | @re[
328 | (define l1 ''(1 2 3 4))
329 | (car l1)
330 | (cdr l1)
331 | (cadr l1)
332 | (caadr l1)
333 | (cdadr l1)
334 | (define l2 '('(1 2) '(3 4)))
335 | (car l2)
336 | (cdr l2)
337 | ''1
338 | (car ''1)
339 | (cdr ''1)
340 | (cadr ''1)
341 | (cddr ''1)
342 | ]
343 | 
344 | 是不是有中很凌乱的感觉?按照racket的定义,@(bq) 是 @r[quote] 的简写,所以 @r['(1 2 3)] 等价于 @bold{(quote (1 2 3))},而其会被racket进一步翻译成 @r[(list '1 '2 '3)]。在racket里,对数字和字符串 @r[quote] 等于其本身:
345 | 
346 | @re[
347 | '1
348 | '"hello world"
349 | '#t
350 | '#f
351 | 'a
352 | ]
353 | 
354 | 所以 @r[(list '1 '2 '3)] 等于 @r[(list 1 2 3)]。按这个推演,我们看上面的例子:
355 | 
356 | @rb[
357 | > ''(1 2 3 4) -> '(quote (1 2 3 4))
358 |               -> (list 'quote '(1 2 3 4))
359 |               -> (list 'quote (list 1 2 3 4))
360 | ]
361 | 
362 | 试试看:
363 | 
364 | @re[
365 | (list 'quote (list 1 2 3 4))
366 | ]
367 | 
368 | Bingo!我们再看上面那个复杂一些的例子:
369 | 
370 | @rb[
371 | > '('(1 2) '(3 4)) -> '((quote (1 2)) (quote (3 4)))
372 |                    -> (list '(quote (1 2)) '(quote (3 4)))
373 |                    -> (list (list 'quote '(1 2)) (list 'quote '(3 4)))
374 |                    -> (list (list 'quote (list 1 2)) (list 'quote (list 3 4)))
375 | ]
376 | 
377 | 试试看:
378 | 
379 | @re[
380 | (list (list 'quote (list 1 2)) (list 'quote (list 3 4)))
381 | ]
382 | 
383 | 希望你看到这里还没晕。现在你应该理解为什么嵌套的 @r[list] 用一个 @r[quote] 就能搞定,以及之前例子中 @r[car] / @r[cdr] 会出来各种凌乱的结果的原因了吧。
384 | 
385 | 好了,热身活动结束,我们谈谈这一节要讲的内容 —— @r[symbol]。
386 | 
387 | 在Racket中,使用 @bold{'} quote 起来的符号,就是 symbol。symbol 有点类似于erlang中的atom,是一个不可变的原子值,其值和表现形式一样。symbol 和 variable 不同的地方在于,variable本身是只个符号,其值和符号本身没有任何关系。
388 | 
389 | @re[
390 | (eq? 'a (quote a))
391 | ]
392 | 
393 | 从之前的例子中,我们可以看到,一个 @r[quote] 会反复作用于内部的表达式,因此一旦一个表达式被 @r[quote] 起来,它就不会进行运算:
394 | 
395 | @re[
396 | '(1 (+ 1 1) 3)
397 | (quote (1 (+ 1 1) 3))
398 | (define x 2)
399 | '(1 x 3)
400 | ]
401 | 
402 | 然而,有些场合下,我们需要内部的表达式计算后再被 @r[quote],成为一个 symbol,怎么办?Racket提供了 @r[quasiquote],标记是 @bold{`}(tab键上边的那个符号)。大部分情况下,@r[quasiquote] 和 @r[quote] 表现形式是一样的,只有在表达式里出现 @r[unquote](标记是 @bold{,})时,@r[quasiquote] 会计算 @r[unquote] 的表达式的值:
403 | 
404 | @re[
405 | '(1 2 3)
406 | `(1 2 3)
407 | '(1 (unquote (+ 1 1)) 3)
408 | `(1 (unquote (+ 1 1)) 3)
409 | `(1 ,(+ 1 1) 3)
410 | '(1 x 3)
411 | `(1 ,x 3)
412 | ]
413 | 
414 | 对于Racket来说,代码和数据是没有分别的,这也是Lisp的一大特色。symbol 是代码和数据之间转化的桥梁,在和 @r[syntax] 相关的场合会被大量用到,其中 @r[quasiquote] 和 @r[unquote] 也有很多用武之地。在这一节里,我们可能还无法理解它的作用,但不要紧,之后的章节我们会继续看到它的身影。
415 | 
416 | 延伸阅读:更多和 @r[symbol] 相关的函数,可以参考 @rdoc-ref{symbols}。
417 | 


--------------------------------------------------------------------------------