作者:Matthew Flatt
原文:https://docs.racket-lang.org/quick/index.html
本教程使用一个Racket的一个画图库对Racket做一个简短的介绍。画图库包含了一些有趣的例子,尽管你并不打算使用Racket进行艺术创作。毕竟,一图胜千言。
我们假定你会用DrRacket运行这些示例。使用DrRacket是直观感受Racket语法的最快方式,虽然实际上你可能用的是Emacs,vi,或其他编辑器。
1 准备
下载Racket,安装,然后打开D人Racket。
2 设置
DrRacket文档在此。
首先我们需要导入一个用来制作幻灯片的库,其中有一些画图函数。将下面的代码复制到DrRacket的代码编辑区。
#lang slideshow
然后点击运行按钮,你将看到光标移动到下方的交互区。
如果你之前使用过DrRacket,你可能需要先通过语言|选择语言菜单设置语言,然后点击运行。
3 开始
当你在交互区的>
后输入表达式然后回车,DrRacket就会进行求值并打印结果。表达式可以仅仅是一个值,比如数字5
或字符串"art gallery"
:
> 5
5
> "art gallery"
"art gallery"
表达式也可以是函数调用。调用函数需要用括号包裹,参数放在函数名后用空格分隔,如下:
> (circle 10)
⚪
circle
函数的结果是一张图片,就像打印数字或字符串一样。circle
的参数是圆的大小(像素)。就像你能猜到的那样,也有rectangle
函数接受两个参数:
> (rectangle 10 20)
▯
试着给circle
两个参数,看看会发生什么:
> (circle 10 20)
circle: arity mismatch;
the expected number of arguments does not match the given
number
expected: 1 plus optional arguments with keywords
#:border-color and #:border-width
given: 2
arguments...:
10
20
DrRacket用红色高亮出错的表达式(本文未展示)。
除了基本的构造函数circle
和rectangle
,也有hc-append
函数用来连接图片。示例如下:
> (hc-append (circle 10) (rectangle 10 20))
○▯
中划线是函数名的一部分。函数名中h的含义是水平拼接,c表示垂直居中。
想要获取帮助或者查看更多函数,将光标移至hc-append
然后按F1
,DrRacket将为你打开新天地。
如果你是阅读原文,可以直接点击链接进行跳转。
4 定义
如果要重复使用一个圆形或方形图片,就需要给他们命名。回到代码编辑区输入以下代码。
#lang slideshow
(define c (circle 10))
(define r (rectangle 10 20))
点击运行,现在你可以直接使用c
或r
了:
> r
▯
> (hc-append c r)
○▯
> (hc-append 20 c r c)
○ ▯ ○
如你所见,hc-append
函数可以接收任意数量的图片参数,并且在图片参数前接受一个可选的数字参数,表示图片之间的间隔大小。
我们可以在交互区求值编辑器写的表达式。通常编辑区用来书写需要保存的代码,交互区用来测试和debug。
让我们来添加一个函数定义。函数也是用define
定义,就像定义变量一样,不同的是函数名和参数需要用括号包裹,函数名和参数,以及参数与参数之间用空格分隔:
(define (square n)
; A semi-colon starts a line comment.
; The expression below is the function body.
(filled-rectangle n n))
函数定义决定了如何调用函数:
> (square 10)
■
同样,定义也可以在交互区求值,表达式也可以写在编辑区。程序运行时,编辑区表达式的值会显示在交互区。从现在开始,我们的示例会把定义和表达式写在一起,你可以在你喜欢的地方写。但是建议将定义写在编辑区。
5 临时绑定
define
关键字可以用来创建临时绑定。比如它可以用在函数体内:
(define (four p)
(define two-p (hc-append p p))
(vc-append two-p two-p))
> (four (circle 10))
◯◯
◯◯
通常,Racker程序员使用let
或let*
表示临时绑定。好处是let
可以用在任何需要表达式的地方,并且可以一次绑定多个变量:
(define (checker p1 p2)
(let ([p12 (hc-append p1 p2)]
[p21 (hc-append p2 p1)])
(vc-append p12 p21)))
> (checker (colorize (square 10) "red")
(colorize (square 10) "black"))
🟥⬛
⬛🟥
let
同时绑定了多个变量,因此它们之间不能相互引用。let*
则允许后面的绑定使用之前的绑定:
(define (checkerboard p)
(let* ([rp (colorize p "red")]
[bp (colorize p "black")]
[c (checker rp bp)]
[c4 (four c)])
(four c4)))
> (checkerboard (square 10))
🟥⬛🟥⬛🟥⬛🟥⬛
⬛🟥⬛🟥⬛🟥⬛🟥
🟥⬛🟥⬛🟥⬛🟥⬛
⬛🟥⬛🟥⬛🟥⬛🟥
🟥⬛🟥⬛🟥⬛🟥⬛
⬛🟥⬛🟥⬛🟥⬛🟥
🟥⬛🟥⬛🟥⬛🟥⬛
⬛🟥⬛🟥⬛🟥⬛🟥
函数也是值
尝试直接输入circle
求值而不是调用它:
> circle
#<procedure:circle>
即标识符circle
绑定到了一个函数(也称过程),就像c
绑定到一个圆。与圆形图片不同的是,函数没法打印,因此DrRacket只是打印了#<procedure:circle>
。
这个例子表明函数也是值,就像数字或图片一样。因此你可以定义函数接收其他函数作为参数:
(define (series mk)
(hc-append 4 (mk 5) (mk 10) (mk 20)))
> (series circle)
○◯⚪
> (series square)
◾◼⬛
当函数作为参数时,参数中的函数通常不会在别的地方调用。通过define
定义函数就会很麻烦,因为你需要给他取个名字并且找个地方放函数定义。替代方案是是用lambda表达式创建匿名函数:
> (series (lambda (size) (checkerboard (square size))))
;;此处有图!实在画不出来了,大家自行尝试。
lambda
后的括号中是函数参数,参数后的表达式是函数体。使用"lambda"而不是"function"或"procedure" 乃是Racket的历史和文化。
使用define
定义函数的语法只是使用define
和lambda
定义函数的简写。例如函数series
也可以定义如下:
(define series
(lambda (mk)
(hc-append 4 (mk 5) (mk 10) (mk 20))))
更多人偏向用简写形式的define
定义函数,而不是展开成lambda表达式。
7 文字作用域
Racket是一个文字作用域语言,这意味着一旦标识符在表达式中被使用,那么在表达式的作用域内,绑定都是可见的。此规则也适用lambda表达式。
在下面的rgb-series
函数中,每个lambda表达式中的mk
都指向参数中的mk
,因为它们在相同的作用域。
(define (rgb-series mk)
(vc-append
(series (lambda (sz) (colorize (mk sz) "red")))
(series (lambda (sz) (colorize (mk sz) "green")))
(series (lambda (sz) (colorize (mk sz) "blue")))))
> (rgb-series circle)
;;自行尝试
> (rgb-series square)
;;自行尝试
另一个例子,函数rgb-maker
接收一个函数并返回一个新的函数,新函数持有返回它的函数中的绑定,也就是闭包。
(define (rgb-maker mk)
(lambda (sz)
(vc-append (colorize (mk sz) "red")
(colorize (mk sz) "green")
(colorize (mk sz) "blue"))))
> (series (rgb-maker circle))
;;自行尝试
> (series (rgb-maker square))
;;自行尝试
注意两个函数答应结果的不同之处。
8 列表
Racket的许多风格都继承自Lisp,Lisp的名字最初代表“列表处理器”,列表仍然是Racket的重要组成部分。
list
函数任意数量的参数,并返回一个包含这些参数的列表。
> (list "red" "green" "blue")
'("red" "green" "blue")
> (list (circle 10) (square 10))
'(⚪ ⬛)
如你所见,列表打印结果是一个单引号和括号包裹的元素。有个令人疑惑的点是括号同时用于表达式,比如(circle 10)
,和答应结果,比如'("red" "green" "blue")
。关键不同的在于引号,这里有详细说明。为了强调这个区别,文档和DrRacket中,结果中的括号是蓝色,不同于表达式中的括号。
map
函数接收一个列表和一个函数,并将函数应用到列表的每个元素,返回一个新的列表。
(define (rainbow p)
(map (lambda (color)
(colorize p color))
(list "red" "orange" "yellow" "green" "blue" "purple")))
> (rainbow (square 5))
'(🟥🟧🟨🟩🟦🟪)
与map
类似,apply
函数也接受一个函数和一个列表,不同的是apply
的函数接受整个列表作为参数,而不是接受列表中元素作为参数。对于可变参数函数,apply
是非常有用的,比如vc-append
:
> (apply vc-append (rainbow (square 5)))
🟥
🟧
🟨
🟩
🟦
🟪
注意,vc-append (rainbow (square 5)))
是错误的,因为vc-append
的参数不是列表,而是可变数量的图片。apply
函数就是可变参数函数和列表之间的桥梁。
9 模块
由于你的编辑窗口的第一行代码是#lanf slideshow
,你在编辑窗口写的所有代码都在同一个模块中。而且,初始化会导入slideshow
模块的所有函数。
使用require
可以导入其他库。例如pict/flash
库中有一个filled-flash
函数:
(require pict/flash)
> (filled-flash 40 30)
;;一个带刺的椭圆
模块的命名和发布有多种方式:
-
一些模块打包在Racket的发行版中,另一些则安装在层级目录。例如,模块
pict/flash
意味着模块实现放在pict
集合flash.rkt
文件中。如果一个模块名没有斜杠,在引用main.rkt
文件。 -
有些模块以包的形式分发。包可以通过文件→安装包菜单或者
raco pkg
命令行工具安装。包可以注册到https://pkgs.racket-lang.org/,或者直接从Git仓库、网站、文件或目录安装。更多请参考官方文档。
有些模块相对于其他模块存在,而不属于特定的包。例如,将之前的代码保存为"quick.rkt",并加上下面这行代码:
(provide rainbow square)
然后新建一个"use.rkt"文件和"quick.rkt"放到同一个目录,并输入以下代码:
#lang racket (require "quick.rkt") (rainbow (square 5))
当你运行"use.rkt"时,就能看到输出彩虹了。注意,"use.rkt"初始化导入的是
racket
,它本身并没有画图的函数,但是有require
和函数调用语法。
Racket通常将程序写成库或者模块,然后通过相对路径或集合路径导入。以这样的方式开发是有帮助的,特别是使用Git仓库存储时。
10 宏
来尝试一个新的库:
(require slideshow/code)
> (code (circle 10))
(circle 10)
结果不是圆,而是打印出了代码本身。换句话说,code
不是一个函数,而是一种新的创建图片的语法;code
后面也不是一个表达式,而是被code语法操作。
这也解释了为什么上一节我们会说racket
提供了require
和函数调用语法。库不是只能导出值,比如函数,也可以定义新的语法。从这个意义上说,Racket完全不是一种语言;它更多的是关于如何构建一种语言的想法,这样你就可以扩展它或者创造全新的语言。
引入新语法的其中一种方式是通过define-syntax
定义语法规则:
(define-syntax pict+code
(syntax-rules ()
[(pict+code expr)
(hc-append 10
expr
(code expr))]))
> (pict+code (circle 10))
○ (circle 10)
这种定义就是一个宏。(pict+code expr)
是使用宏的一个模式。程序中模式会被相应的模板替换,也就是(hc-append 10 expr (code expr))
。其中,(circle 10)
匹配上expr,因此最后被替换成(hc-append 10 (circle 10) (code (circle 10)))
。
当然,这种句法扩展是有利有弊的:发明一种新的语言可以让你更容易表达,但却让别人更难理解。
事实上,这篇文档的原文也是用扩展的Racket编写的,链接奉上,可能要上些手段才能打开。
11 对象
对象系统是语言扩展的另一个例子,也很值得学习。即使有lambda,有时对象也比函数更方便,特别是图形界面编程。Racket的GUI接口和图形系统就是用对象和类来表达的。
类是通过racket/class
库实现的,GUI和画图类由racket/gui/base
库提供。按惯例,类名以%结尾:
(require racket/class
racket/gui/base)
(define f (new frame% [label "My Art"]
[width 300]
[height 300]
[alignment '(center center)]))
> (send f show #t)
;;一个窗口
new
关键字创建一个类的实例,初始化参数如label
,width
通过名称指定。send
用来调用一个对象上的方法,如show
,参数就跟在方法名后,此例中的#t
就是布尔值"true"。
通过sldeshow
生成的图片封装了一个函数,可以使用图形工具箱的绘图指令将图片渲染到一个绘图上下文,比如一个帧中的画布。sldeshow
的make-pict-drawer
函数可以暴露图片的绘制函数。我们可以在画布绘制回调中使用make-pict-drawer
将图片绘制到画布。
(define (add-drawing p)
(let ([drawer (make-pict-drawer p)])
(new canvas% [parent f]
[style '(border)]
[paint-callback (lambda (self dc)
(drawer dc 0 0))])))
> (add-drawing (pict+code (circle 10)))
#(struct:object:canvas% ...)
> (add-drawing (colorize (filled-flash 50 30) "yellow"))
#(struct:object:canvas% ...)
每个画布都会拉伸到与帧相同的大小,这是帧管理子对象的默认方式。
12 接下来
本文有意避免了传统介绍Lisp和Scheme的方式:前缀函数、符号、引号,准引用列表,eval,一级延续,以及所有语法都是lambda的语法糖。虽然这些都是Racket一部分,但不是日常编程的主要成分。
相反,Racket程序员通常用函数、记录、对象、异常、正则表达式、模块和多线程编程。也就是说,与“极简主义”语言——这是Scheme经常被描述的方式——不同的是,Racket提供了一种丰富的语言,有一套广泛的库和工具。
如果你是新手,或者有耐心去看课本,我们推荐阅读How to Design Programs。如果你已经读过,或者你想知道这本书会带给你什么,可以看Continue:Web Applications in Racket。
对于老司机,可以看系统编程More:Systems Programming with Racket。
若想全面深入学习Racket语言和工具,移步The Racket Guide。
写在最后:
这篇文章只是一篇带有趣味性的介绍文章,从中我们可以看到一些Racket的语法以及它能做什么。不得不说Lisp家族语言还是非常强大的。如果看完本文感到对Racket有兴趣的话,一定要去官网看看文档。其实这篇文档也是官网上的。
本文翻译在许多地方按中文习惯做了调整,我也是刚接触Racket,有些专业名称应该翻译不准,希望不会误人子弟,有条件的话建议看下原文。
如有纰漏,万望指出。