快速:通过画图了解Racket


作者: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用红色高亮出错的表达式(本文未展示)。

除了基本的构造函数circlerectangle,也有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))

点击运行,现在你可以直接使用cr了:

> 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程序员使用letlet*表示临时绑定。好处是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定义函数的语法只是使用definelambda定义函数的简写。例如函数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]

结果不是圆,而是打印出了代码本身。换句话说,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关键字创建一个类的实例,初始化参数如labelwidth通过名称指定。send用来调用一个对象上的方法,如show,参数就跟在方法名后,此例中的#t就是布尔值"true"。

通过sldeshow生成的图片封装了一个函数,可以使用图形工具箱的绘图指令将图片渲染到一个绘图上下文,比如一个帧中的画布。sldeshowmake-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]
每个画布都会拉伸到与帧相同的大小,这是帧管理子对象的默认方式。

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,有些专业名称应该翻译不准,希望不会误人子弟,有条件的话建议看下原文。

如有纰漏,万望指出。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值