Yet Another Scheme Tutorial 01

Yet Another Scheme Tutorial

2023/8/24
4年后回看补充练习题;
新增 DrRacket运行结果;
富文本更新为 Markdown格式。

Scheme入门教程

Takafumi Shido

DeathKinglincode

原地址,http://deathking.github.io/yast-cn/


简介

这是一本面向初学者的温和且循序渐进的Scheme教程。目标读者是仅有些许编程经验的 PC用户。

如果你不满意于其它的教程,那么请尝试本书。我们有很多方法去解释像 Scheme程序设计语言这样的抽象主题,这之中最好的方法取决于读者的能力以及素养。(没有对任何人来说都绝对完美的方法。)这也正是尽管已经有很多 Scheme语言的教程,我还另写一本的原因所在。

本教程的目的在于给读者在 Scheme程序设计上提供足够的知识和能力以便能够阅读最好的计算机科学教科书之一的——《计算机程序的构造和解释》(Structure and Interpreter of Computer Program,SICP)。SICP使用 Scheme作为授课语言。

中文版序

Scheme恰似中国传统棋盘游戏——围棋。

这是因为它们都可以根据相当简单的规则产生美妙的代码或者棋局,这些规则在它们的领域中都是最简单的。简单的规则,无限的美妙变幻,这些都无比吸引那些聪明的家伙。但同时,大自然却让它们难以掌握。

我编写这份教程以打开掌握 Scheme之门。我相信中文译版会帮助更多的程序员掌握 Scheme程序设计语言。

尽管Common Lisp更加适合构建实用应用程序,但我依然推荐你首先学习 Scheme,因为这门语言:

  • 设计紧凑
  • 语法简单

业界大牛提出过“Scheme使你成为更棒的程序员”的看法。即是你很少在商业项目上使用 Scheme,但学习 Scheme获得的良好感觉将会指导你使用其它的编程语言。

网络上的Scheme教程(真是多如牛毛)总是或多或少的有些困难,因而不太适合初学者。这样来说的话,本教程是面向新手程序员的,他们只需要对编程有一点了解即可。

Scheme代码仅由单词,括号和空格组成,这些最初可能会使你感到烦扰。然而,如果你使用了一个合适的编辑器,它会为你展示配对的括号和自动缩进。

新增部分

教程是安装 MIT-Scheme,看知乎经验帖 学习SICP(《计算机程序的构造和解释》)的一些准备工作,DrRacket对于 Scheme也有很好的支持,之前叫做 DrScheme。搜索下载,安装,一气呵成。

将 Scheme 用作计算器

欢迎使用 DrRacket, 版本 8.5 [cs].
语言: Beginning Student [自定义]; memory limit: 128 MB.
Teachpack: 2htdp/image.rkt.
> (+ 1 2)
3

解释器返回3作为答案。请注意以下三点:

  1. 一对括号代表了一次计算的步骤
  2. 左括号后紧跟着一个函数的名字,然后是参数。Scheme中大多数的操作符都是函数。
  3. 标记的分隔符是空格(Space)、制表符(Tab)或者换行符(Newline)。逗号和分号不是分隔符。

总结:(操作 操作数···)
在这个函数中,当所有的参数被求值后,计算开始处理。对参数的求值顺序是没有被规范的,也就是说,参数并不是总是会从左到右求值。

所以,+ 只是个符号,被定义为加法过程的符号。同样的,可以定义符号-为加法过程。

算术操作

+-*/分别代表加、减、乘、除。这些函数都接受任意多的参数。
Scheme(以及大多数Lisp方言)都可以处理分数。
函数exact->inexact 用于把分数转换为浮点数。
Scheme也可以处理复数。复数是形如 a+bi的数,此处 a称为实部,b称为虚部。

  • 函数quotient用于求商数(quotient)。
  • 函数remaindermodulo用于求余数(remainder)。
  • 函数sqrt用于求参数的平方根(square root)。
> (+ 1 2 3)
6
> (* 2 3 4)
24
> (/ 29 3)
9.6
> (/ 0 4)
0
> (/ 4 0)
/: division by zero
> (exact->inexact
   (/ 29 3))
#i9.666666666666666
> (*
   (+ 2 3)
   (- 5 3))
10

数学上的三角函数,诸如sincostanasinacosatan都可以在 Scheme中使用。
atan接受1个或2个参数。如果atan的参数为1/2 π,那么就要使用两个参数来计算。

指数通过exp函数运算,对数通过log函数运算。ab次幂可以通过(expt a b)来计算。

形如这些由括号、标记(token)以及分隔符组成的式子,被称为S-表达式。

  • 练习 1

使用Scheme解释器计算下列式子:

  1. (1+39) * (53-45)                ; 320 
  2. (1020 / 39) + (45 * 2) ; 1510 / 13
  3. 求和:39, 48, 72, 23, 91 ; 273
  4. 求平均值:39, 48, 72, 23, 91(结果取为浮点数) ; 54.6
  • 练习 2

使用Scheme解释器求解下列式子:

  1. 圆周率π。 ;
  2. exp(2/3)。 ; 1.9477340410546757
  3. 3的4次幂。 ; 81
  4. 100的对数。 ; 4.605170185988092
作者给出的答案:
(* 4 (atan 1.0))          ;⇒   3.141592653589793

运行环境是,Deepin 终端,MIT - Scheme

MIT/GNU Scheme running under GNU/Linux

This is GNU Emacs 24.5.1 (x86_64-pc-linux-gnu, GTK+ Version 3.22.11)

 of 2017-09-12 on hullmann, modified by Debian

生成表

作为Lisp语言大家族的一员,Scheme同样擅长于处理表。你应该理解表以及有关表的操作以掌握 Scheme。
表在在后面章节中的递归函数和高阶函数中扮演重要角色。

首先,解释表的元素:Cons单元(Cons cells)

Cons单元

Cons cells是一个存放了两个地址的内存空间。Cons单元可用函数cons生成。

欢迎使用 DrRacket, 版本 8.5 [cs].
语言: racket,带调试; memory limit: 128 MB.
> (cons 1 2)
'(1 . 2)
;嵌套
> (cons 3
        (cons 1 2))
'(3 1 . 2)

> (cons #\a
        (cons 3 "hello"))
'(#\a 3 . "hello")
> (cons (cons 0 1)
        (cons 2 3))
'((0 . 1) 2 . 3)

函数cons给两个地址分配了内存空间,并把存放指向1的地址放在一个空间,把存放指向2的地址放在另一个空间。

·(3 . (1 . 2))·可以更方便地表示为'(3 1 . 2)。
;;程序语法符号与 Markdown语法冲突

存放指向1的地址的内存空间被称作car部分,对应的,存放指向2的地址的内存空间被称作cdr部分。carcdr分别是寄存器地址部分(Contents of the Address part of the Register)和寄存器减量部分(Contents of the Decrement part of the Register)的简称。cons这个名字是术语构造(construction)的简称。

Scheme可以通过地址操作所有的数据。(#\c代表了一个字符c。例如,#\a就代表字符a

> '(3 . (1 . 2))
'(3 1 . 2)

是Cons单元通过用cdr部分连接到下一个Cons单元的开头实现的。表中包含的’()被称作空表。就算数据仅由一个Cons单元组成,只要它的cdr单元是’(),那它就是一个表。

事实上,表可以像下面这样递归地定义:

  1. ‘()是一个表
  2. 如果ls是一个表且obj是某种类型的数据,那么(cons obj ls)也是一个表,正因为表是一种被递归定义的数据结构,将它用在递归的函数中显然是合理的。

原子

不使用Cons单元的数据结构称为原子(atom)。数字,字符,字符串,向量和空表’()都是原子。

’()既是原子,又是表。

  • 练习 1

使用cons来构建在前端表现为如下形式的数据结构。

'("hi" . "everybody")  ; (cons "hi" "everybody")
'(0)            ; `(0)
'(1 10 . 100)      ; (cons 1 (cons 10 100)) 
'(1 10 100)       ; (cons 1 (cons 10 (cons 100 `())))
'(#\I "saw" 3 "girls") ; (cons #\I (cons "saw" (cons 3 (cons "girls" `()))))
'("Sum of" (1 2 3 4) "is" 10) ; (cons "Sum of" (cons `(1 2 3 4) (cons "is" (cons 10 `()))))

之前上面练习使用引用符号与 Markdown 语法冲突,导致渲染后表达式难以理解。现改为代码模块。

下面是 DrRacket编写:

#lang racket
(cons "hi" "everybody")
(cons 0 `())
;;'(0)
(cons `() 0)
;;'(() . 0)

(cons 1
      (cons 10 100))
;;'(1 10 . 100)
(cons 1
      (cons 10
            (cons 100 `())))
;;'(1 10 100)

(cons #\I
      (cons "saw"
            (cons 3
                  (cons "girls" `()))))
; 实现(1 2 3 4)
(cons 1
            (cons 2
                  (cons 3
                        (cons 4 `()))))
(cons "Sum of"
      (cons
       (cons 1
             (cons 2
                   (cons 3
                         ; 为什么会少个括号?多一层封装?
                         (cons 4 `()))))
       
      (cons "is"
            (cons 10 `()))))
;另一个思路
(cons 1234 `())
(cons `(1 2 3 4) `())
(cons "Sum of"
      (cons `(1 2 3 4)
            (cons "is"
                  (cons 10 `()))))
;输出
'(1234)
'((1 2 3 4))
'("Sum of" (1 2 3 4) "is" 10)

引用

所有的记号都会依据 Scheme的求值规则求值:

  所有记号都会从最内层的括号依次向外层括号求值,

  且最外层括号返回的值将作为S-表达式的值。

  一个被称为引用(quote)的形式可以用来阻止记号被求值。它是用来将符号或者表原封不动地传递给程序,而不是求值后变成其它的东西。例如,(+ 2 3)会被求值为5,然而(quote (+ 2 3))则向程序返回(+ 2 3)本身。因为quote的使用频率很高,他被简写为。实际上,’()是对空表的引用,也就是说,尽管解释器返回()代表空表,你也应该用’()来表示空表。

Scheme有两种不同类型的操作符:

  其一是函数。函数会对所有的参数求值并返回值。

  另一种操作符则是特殊形式。特殊形式不会对所有的参数求值。

  除了quotelambdadefineifset!,等都是特殊形式。

小结表和引用

;尝试
> (cons (cons 0 1)
        (cons 3 4))
'((0 . 1) 3 . 4)
> (cons (cons 0 1)
        (cons 3
              (cons 4 '())))
'((0 . 1) 3 4)
;为什么不是'((0 . 1) (3 . 4))
;或者是'((0 . 1) '(3 . 4))
> (cons (cons 0 1)
        (cons (cons 3 4) 
              '()))
'((0 . 1) (3 . 4))
;等价开始的尝试
> (cons (cons 0 1)
        '(3 . 4))
'((0 . 1) 3 . 4)

> (cons 3
        (cons 4 '()))
'(3 4)
;小结,生成表仅有两个部分组成,且最后部分只能是表
;表是 (cons a b)或'(a b)
;点是表的第三种形态?
> (cons (cons 0 1)
        2
        (cons 3 4))
. . cons: arity mismatch;
 the expected number of arguments does not match the given number
  expected: 2
  given: 3
  
> (list (list 0 1)
        2
        (list 3 4))
'((0 1) 2 (3 4))

car函数和cdr函数

  返回一个Cons单元的car部分和cdr部分的函数分别是carcdr函数。

  如果cdr部分串连着Cons单元,解释器会打印出整个cdr部分

  如果 Cons单元的cdr部分不是’(),那么其值稍后亦会被展示。

  • 练习2

求值下列S-表达式。

 (car '(0))             ; 0
 (cdr '(0))             ; '()
 (car '((1 2 3) (4 5 6)))        ; '(1 2 3)
 (cdr '((1 2 3) (4 5 6)))     ; '( (4 5 6) )  // 自己添加,作为对比!!两个括号
 (cdr '(1 2 3 . 4))       ; '(2 3 . 4)
 (cdr (cons 3 (cons 2 (cons 1 '())))) ; '(2 1)

注意:. 的作用。!!

#lang racket
(car `(0))

(cdr `(0))


(car `((1 2 3)
       (4 5 6))
     )

(cdr (cons 3
           (cons 2
                 (cons 1 `()))))

(cons 3
      (cons 2
            (cons 1 `())))

输出

欢迎使用 DrRacket, 版本 8.5 [cs].
语言: racket,带调试; memory limit: 128 MB.
0
'()
'(1 2 3)
'(2 1)
'(3 2 1)

小结:没搞清楚 .的含义。

#Mit-scheme
(cdr (cons 1 (cons 2 (cons 3 4))))

;Value 29: (2 3 . 4)

(cons 1 (cons 2 (cons 3 4)))

;Value 30: (1 2 3 . 4)
/* 示例对比 */(cons 1 (cons 2 3))

;Value 27: (1 2 . 3)

(cdr (cons 1 (cons 2 3)))

;Value 23: (2 . 3)

(cons 1 (cons 2 (cons 3 `())))

;Value 28: (1 2 3)

(cdr (cons 1 (cons 2 (cons 3 `()))))

;Value 24: (2 3)

还原表(练习)

#lang racket
;还原表
;'(0)
(cons 0 '())
(car (cons 0 '()))
(cdr (cons 0 '()))
;;'(0)
;;0
;;'()

;'((1 2 3) (4 5 6))
(cons '(1 2 3)
      (cons '(4 5 6) '()))
(car (cons '(1 2 3)
      (cons '(4 5 6) '())))
(cdr (cons '(1 2 3)
      (cons '(4 5 6) '())))
;;'((1 2 3) (4 5 6))
;;'(1 2 3)
;;'((4 5 6))

;'(1 2 3 .4)
(cons (cons 1
            (cons 2
                  (cons 3 4)))
      '())
(cdr (cons (cons 1
            (cons 2
                  (cons 3 4)))
      '()))
;;'((1 2 3 . 4))
;;'()

;受到启发后
(cdr (cons '()
           (cons 1
                 (cons 2
                       (cons 3 4)))))
;;'(1 2 3 . 4)
;正解如下
> (cdr '(1 2 3 .4))
'(2 3 0.4)
;延伸
> (car '(1 2 3 .4))
1
;验证
> (cons 1
        (cons 2
              (cons 3
                    (cons 0 4))))
'(1 2 3 0 . 4)

;(cons 3 (cons 2 (cons 1 '())))
(cons 3
           (cons 2
                 (cons 1 '())))
(cdr (cons 3
           (cons 2
                 (cons 1 '()))))
;;'(3 2 1)
;;'(2 1)

List 函数

  list函数使得我们可以构建包含数个元素的表。函数list有任意个数的参数,且返回由这些参数构成的表

> (list)
'()
> '()
'()
> (list (list 1 2))
'((1 2))
> (list '(1 2) '(3 4))
'((1 2) (3 4))
> (list 0)
'(0)
> (list 1 2)
'(1 2)
> (list 1
       (list 2
             3
             '(4 5)))
'(1 (2 3 (4 5)))

定义函数

  由于 Scheme是函数式编程语言,你需要通过编写小型函数来构造程序。

  因此,明白如何构造并组合这些函数对掌握 Scheme尤为关键。

  在前端定义函数非常不便,因此我们通常需要在文本编辑器中编辑好代码,并在解释器中加载它们。

如何定义函数并加载它们

  你可以使用define来将一个符号与一个值绑定

  你可以通过这个操作符定义例如数、字符、表、函数等任何类型的全局参数

  让我们使用任意一款编辑器(记事本亦可)来编辑代码片段1中展示的代码,并将它们存储为hello.scm,放置在类似于C:\doc\scheme\的文件夹下。如果可以的话,把这些文件放在你在第一章定义的MIT-Scheme默认文件夹下。

; Hello world as a variable
(define vhello "Hello world")     ;1

; Hello world as a function
(define fhello (lambda ()         ;2
         "Hello Scheme"))

  操作符define用于声明变量,它接受两个参数。

  define运算符会使用第一个参数作为全局参数,并将其与第二个参数绑定起来。

  因此,代码片段1的第1行中,我们声明了一个全局参数vhello,并将其与"Hello,World"绑定起来。

  紧接着,在第2行声明了一个返回“Hello Scheme”的过程。

1 ]=> (load "hello")

;Loading "hello.scm"... done
;Value: fhello

1 ]=> (cd "./")

;Value 13: #[pathname 13 "/home/yws/Documents/Demo/scm/./"]

1 ]=> vhello

;Value 14: "hello world"

1 ]=> fhello 

;Value 15: #[compound-procedure 15 fhello]

1 ]=> (fhello)

;Value 16: "hello scheme"

  特殊形式lambda用于定义过程。

  lambda需要至少一个的参数,第一个参数是由定义的过程所需的参数组成的表。因为本例fhello没有参数,所以参数表是空表。

  在解释器中输入vhello,解释器返回“Hello,World”。

  如果你在解释器中输入fhello,它也会返回像下面这样的值:#[compound-procedure 16 fhello]

  这说明了Scheme解释器把过程和常规数据类型用同样的方式对待

  正如我们在前面章节中讲解的那样,Scheme解释器通过内存空间中的数据地址操作所有的数据,因此,所有存在于内存空间中的对象都以同样的方式处理。

  如果把fhello当过程对待,你应该用括号括住这些符号,比如(fhello)。然后解释器会按照第二章讲述的规则那样对它求值,并返回“Hello Scheme”。

1 ]=> (define vhello "hello world")

;Value: vhello

1 ]=> vhello

;Value 38: "hello world"
------------------
1 ]=> (define fhello (lambda () "hello Scheme"))

;Value: fhello

fhello

;Value 39: #[compound-procedure 39 fhello]
1 ]=>  +

;Value 40: #[arity-dispatched-procedure 40]

注意,Value 后面的数值。。居然,保存着原来的位置,在内存的地址没变!!

img

#lang racket
(define vhello "Hello world")
vhello
(define fhello 
 (lambda ()
                "Hello world"))
fhello
(fhello)
(vhello)

;输出
欢迎使用 DrRacket, 版本 8.5 [cs].
语言: racket,带调试; memory limit: 128 MB.
"Hello world"
#<procedure:fhello>
"Hello world"
application: not a procedure;
expected a procedure that can be applied to arguments
 given: "Hello world"

定义有参数的函数

可以通过在lambda后放一个参数表来定义有参数的函数。

; hello with name
(define hello
  (lambda (name)
    (string-append "Hello " name "!")))

; sum of three numbers
(define sum3
  (lambda (a b c)
    (+ a b c)))

保存文件,并在解释器中载入此文件,然后调用我们定义的函数。

(load "farg.scm")
;Loading "farg.scm" -- done
;Value: sum3

(hello "Lucy")
;Value 20: "Hello Lucy!"

(sum3 10 20 30)    ; 重点!!不是lambda (...)
;Value: 60
Hello

函数hello有一个参数(name),并会把“Hello”name的值、和"!"连结在一起并返回。

预定义函数string-append,可以接受任意多个数的参数,并返回将这些参数连结在一起后的字符串。

sum3:此函数有三个参数并返回这三个参数的和。

(define hello
 (lambda (name)
   (string-append "Hello" name "!")))
(hello "scheme")

"Helloscheme!"

一种函数定义的短形式

  用lambda定义函数是一种规范的方法,但你也可以使用类似于代码片段3中展示的短形式。

; hello with name
(define (hello name)
  (string-append "Hello " name "!"))


; sum of three numbers
(define (sum3 a b c)
  (+ a b c))

  在这种形式中,函数按照它们被调用的形式被定义。代码片段2和代码片段3都是相同的。有些人不喜欢这种短形式的函数定义,但是我在教程中使用这种形式,因为它可以使代码更短小。

我只能说。。蒙圈了,练习题,看答案都搞不懂。。自己写的又报错。再看一遍。

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

  • 练习1

按照下面的要求编写函数。这些都非常简单但实用。

  1. 将参数加1的函数。
  2. 将参数减1的函数。
(define addone
 (lambda (a)
   (+ a 1)))
(addone 1)

(define (minusone a)
 (- a 1))
(minusone 1)

意想不到4年后随手写出来了。

posted @ 2019-07-22 15:35  Marchyi  阅读(368)  评论(0)    收藏  举报