学了一阵子Racket,遇到了惊喜,产生了一些前所未有的体会。
1 Racket不是传说中基于Scheme添加了很多艰深恐怖功能的“PLT Scheme”,我读那个Semantic Engineering的书时候一度对某些Lisp族语言产生了反感,原因是认为作者“反编程”—他们在编程语言已经清晰了当情况下外加一套天书般的“形式语言”反而比程序语言本身更繁琐了。
然而这源于我没有足够了解Racket就基于相关信息下的结论。很多讲课中清晰易懂的东西对应论文里天书一般的符号,这情况很多了。
2 Racket的介绍、文档都非常全面,循序渐进,可读性强。有些朋友知道我对某p姓开头的编程语言有特殊负面偏见,一个原因是配套的介绍和文档质量低,这个问题Racket基本上都没有。
3 实际上有两个Racket,一个是非常像Scheme近乎原始但表现力强、可以制造巨人的语言,另一个是内置的字符串处理函数比据我所知流行的编程语言都多,各种语法糖、命令式、函数式、面向对象、模块化、包管理应有尽有的大熔炉语言。
4 通过Racket我开始意识到“设计”的另一面:好的设计可以让人通过使用产品过程中自然体会到设计的初衷和原理,你可以接受也可以拒绝但设计者让你轻松地明白产品是怎么样的。
Racket是我见过的第一个自然地通过符号和操作复杂性实现功能区分的(有点辞不达意),举个例子define不是关键字,
这里一个内置函数把pi按格式输出:
(~r pi #:precision 2)
#:precision
这个东西是关键字,Racket里面有很多关键字,都以#:
开头但并不作为高频使用比如private、public、void这些,而是作为确实需要时候的必要之物,这根其他编程语言把关键字设计成高频事物的理念相反,然而接触多一些之后我觉得这才自然——高频的东西要自己定义,Racket跟Ruby、Pharo(SmallTalk)等语言一样内置函数大都可以改,我认为它强调加工后的产品要自成一体,而产品产生的背景要被淡化。
5 例子源于参考书:
(define last-check 1000)
(struct transaction
(date payee check-number amount)
#:mutable #:transparent
#:guard (λ (date payee num amt name)
(cond
[(< num 0)
(error "Not a valid check number")]
[(= num 0)
(let ([next-num (add1 last-check)])
(set! last-check next-num)
(values date payee next-num amt))]
[else
(set! last-check num)
(values date payee num amt)])))
\> (transaction 20170907 "John Doe" 0 100.10)
(transaction 20170907 "John Doe" 1001 100.1)
\> (transaction 20170907 "Jane Smith" 1013 65.25)
(transaction 20170907 "Jane Smith" 1013 65.25)
\> (transaction 20170907 "Acme Hardware" 0 39.99)
(transaction 20170907 "Acme Hardware" 1014 39.99)
与此同时:
(define trans (transaction 20170907 "John Doe" 1012 100.10))
\>(set-transaction-check-number! trans 1013)
相信见多识广的大家已经看出Racket的一些设计了:Racket里实现了很多编程语言无法自然实现的声明式和指令式的融合,一个“#:guard”直接加入一大段判断输入值是否正常的条件,而且有全套异常处理。
Racket对于结构体struct类型,自动生成getter、setter函数,自带IDE功能。这是“约定优于配置”的思想。当然如果不满意希望配置大于约定的话也可以对Racket进行“深度定制”。
原来,有一种好的设计理念是包容使用者自由,给使用者再定义这个产品的可能,这不是设计的缺位和无能,而恰恰是设计的周全。
我暂且愿将见闻归于“自然之美”。
参考
Racket Programming the Fun Way by James W. Stelly