本教程使用一个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)
![[image]](https://img-blog.csdnimg.cn/img_convert/2bababe3cc3ff822bebd2a3437fb7ec5.png)
结果不是圆,而是打印出了代码本身。换句话说,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% ...)
![[image]](https://img-blog.csdnimg.cn/img_convert/6313ea8b8d9b9fd143802263a2aac82e.png)
每个画布都会拉伸到与帧相同的大小,这是帧管理子对象的默认方式。
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,有些专业名称应该翻译不准,希望不会误人子弟,有条件的话建议看下原文。
如有纰漏,万望指出。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/54813.html