开始编写方案
TL;DR:下载Racket,运行plt-r6rs my-file.scm
。请收藏本书,有问题时可以查阅。
Scheme是一门古老的编程语言,诞生于七十年代中期。它的标志是极简主义——它真的没有太多的语言。但这不应与强大的表达能力混淆——你可以用 Scheme 编写任何你目前用你最喜欢的语言编写的代码。
然而,这种极简主义使其特别适合学术研究。语言不再成为阻碍,让你专注于实际的逻辑。
一些选择传统计算机科学大学课程的人可能在课程中接触过这个工具。不过,并非所有课程都使用它——我的课程就没用——自学者也可能不会偶然发现它。我认为你错过了一个非常适合学习算法和编程语言的工具。Scheme 是一种非常适合讨论计算本身的语言,几乎可以作为一种正式的符号。没有任何噪音来分散你的注意力——只有你和你的算法。
这是一个常见的例子。人们(特别是我)可能会这样用C 语言编写欧几里得算法来寻找两个数的最大公约数:
// euclid.c
#include <stdio.h>
/**Calculate the greatest common divisor of positive integers m and n.
* Returns -1 on error
*/
int gcd(int,int);
int main(void)
{
int m = 119, n = 544, res;
res = gcd(m,n);
printf("%d", res);
return 0;
}
int gcd(int dividend, int divisor)
{
int m = dividend, n = divisor;
// Out of bounds
if (n <= 0 || m <= 0)
return -1;
for (;;)
{
// 1. Let `r` be the remainder of diving `m / n`.
int r = m % n;
// 2. If `r == 0`, complete. Return `n`.
if (r == 0)
return n;
// 3. `m <- n`, `n <- r`, goto 1.
m = n;
n = r;
}
}
C 语言之所以适合算法研究,原因也类似——它是一种小型语言。它没有大量的语法噪音,甚至没有隐含的魔法——程序员在告诉计算机你的需求时,承担了大部分的负担。这种限制实际上根本不是限制——你完全掌控着一切,而当你研究和优化算法以加深理解时,这正是你想要的。
不过,还是有点吵。什么<stdio.h>
?什么"%d"
?为什么int
到处都说?
以下是 Scheme 中相同算法的实现:
; euclid.scm
(import (rnrs))
(define (gcd m n)
(if (and (>= m 0) (>= n 0))
(cond [(= n 0) m]
[else (gcd n (mod m n))])
(- 1)))
(display (gcd 119 544))
这简洁多了!你可以直接用树形结构来编写语法树。
要定义绑定,您可以使用define
:
(define my-name "Ben")
(display my-name)
这也适用于函数,上面的代码片段使用了简写。Scheme 中的每个函数实际上都是一个匿名 lambda,您可以为其创建绑定,而且由于这是一种非常常见的操作,所以我们可以使用这个语法糖。如果没有它,这个程序看起来就像这样:
(define gcd
(lambda (m n)
(if (and (>= m 0) (>= n 0))
(cond [(= n 0) m]
[else (gcd n (mod m n))])
(- 1))))
该函数本身是一个匿名 lambda,我们为其定义了一个 binding gcd
。当你使用更短的 时(define (gcd m n) (...))
,实际上会发生以下情况。
这种规律延伸到了语言的各个部分。函数调用是一个用括号括起来的列表,函数本身位于列表首位:
(= n 0) ; n == 0
(mod m n) ; n % m
condA && condB
我们使用而不是(and condA condB)
。这几乎完全消除了歧义——只需顺着括号嵌套,就能追踪其中的逻辑。if
表达式如下所示:
(if predicate onTrue onFalse)
您可以将这三个参数替换为所需的实际形式。如果代码变得过于复杂,您可以define
在函数外部进行阻塞,或者使用let
来创建本地绑定。该cond
语句是具有多个分支的 的简写if
,例如switch
。
有个小问题是,你甚至不能直接写负数:-1
在失败分支中不起作用。只有一个参数时,-
表示“取反”——所以(- 3 2)
减去,得到1
,(- 1)
取反,得到-1
。
值得注意的是,这个版本是用递归方式编写的,而我们的 C 语言是命令式的。Scheme 已标准化,支持正确的尾递归,因此,虽然这个函数在语法上是递归的,但由于递归发生在尾部,它实际上的执行方式更像是一个命令式循环,重用堆栈框架并交换操作数。
你可以用递归的方式编写任何你遇到的命令式算法,尽管它们并不总是那么方便。如果你不清楚如何针对你正在使用的算法进行递归,你可能需要使用 Scheme 来尝试一下!
我使用的标准称为R6RS
,或者更通俗地说RNRS
,指的是与版本无关的 Scheme。与 C++ 或 ECMAScript 类似,Scheme 仅作为标准存在,并且存在许多相互竞争的实现。令人沮丧的是,这些实现实际上彼此之间存在相当大的差异,但核心语言将保持一致——这正是标准的意义所在!R6RS 标准可以在这里找到。
实际上现在也有一个 R7RS 方案,但除非你开始认真对待方案工作,否则不要太担心。社区里显然存在一些关于极简主义、价值观之类的争论——你知道,就是政治。我来这里不是为了政治,而是为了算法。R6RS 完全没问题。
我发现开始使用 Scheme 最简单的方法是通过Racket 。Racket 是一个比 RNRS Scheme强大得多的系统,但它支持一项名为#lang
指令的功能,可以让你像这样限制 Racket 遵循特定标准。你可以将此#!r6rs
指令直接放在文件中,并使用标准的 Racket 构建工具,但 Racket 还附带一个名为 的 CLI 工具plt-r6rs
。你可以直接在 Scheme 文件上调用它,无需任何#lang
额外配置:
$ plt-r6rs euclid.scm
17
关于通过 Racket 使用 R6RS 的更多信息,可以在这里找到。这里再次提供了Scheme 手册,以及R6RS 标准。这些链接应该可以帮助你开始自己的实验。如果你渴望更上一层楼的 Scheme 启蒙,那就直接跑到The Little Schemer(不要走)那里,给自己做个三明治吧。
玩得开心!
照片由 Chinh Le Duc 在 Unsplash 上拍摄
鏂囩珷鏉ユ簮锛�https://dev.to/decidously/get-started-writing-scheme-3aj0