单元测试
写测试的好处是不用盯着屏幕看了,需要跑几十个用例的时候,用眼看是不是符合预期是非常二的行为。(商用的东西有几万个测试都很正常。。。)
即便是自己开发,也应该写一些非常简单的、预期函数结果的那种测试。
当然只有测试是不够的,关键是要对自己写下的东西有自信,相信它覆盖了足够多的情况。甚至可以称之为“对的”(在所有情况下都表现出正确的行为)
正如课程要求所说
The objective is not simply to write programs that get the correct answers; it is to write answers in the style of programs written in class.
提前写出测试,然后写实现的做法叫做测试驱动开发(TDD),我觉着 TDD 用在小的函数层次还是很管用的。毕竟一个函数越小,它的行为就越容易预测,吧。
不只是方便验证结果。直接拿测试用例那一行,把对应的实参改成形参的时候,往往能想出更好的参数名字。
Racket 自带的测试框架叫 rackunit
http://docs.racket-lang.org/rackunit/api.html
这个作业自带的测试大概长这样,具体实现请自行研究。
(test-suite "countdown"
(test-equal-if-defined countdown
((countdown 5) '(5 4 3 2 1 0))))
还有就是测试结果有 failure 和 error 的区分。比如一个函数没定义,认为是failure,确实是函数逻辑错了,认为是error。
Natural Recursion
Natural Recursion ,或者说naturally recursive,直接理解就是自然而然的递归。
这个词我搜了很久都没找到确切的意思
参考了这个http://web.mit.edu/6.005/www/fa15/classes/10-recursion/
Think about several ways to break down the problem, and try to write the recursive steps. You want to find the one that produces the simplest, most natural recursive step.
而且所有的问题都不允许用循环,甚至不让用 accumulator 存储中间结果。
姑且认为是用分治法来解决问题,通过把原问题转化为更小的子问题,以及trivial情况下返回显然的解。来解题。
或者反过来说,在给定更小子问题的解的前提下,如何得到更大问题的解。
也就是所谓递推公式、数学归纳。
举个例子
(define (countdown n)
(if (= n -1)
'()
(cons n (countdown (- n 1)))))
(countdown N) 返回从 N到0 的列表。
我这里的实现是说,n==-1时,trivial case,直接返回空表
否则,返回 把 N cons 到 (countdown (- n 1)) 的结果
很多数据类型都可以看成递归数据类型,
lisp 的 list,其实就是个单链表,链表可以是空表,也可以是Head元素指向另一个链表。这就是所谓递归定义。
字符串,字符串有很多种切碎的方法,
可以看作是首元素+字符串
或者 字符串+末尾元素
或者中间切碎 字符串 + 。。。 + 字符串
自然数 是
0
或者 某个自然数的后继 (所谓后继就是数数,0的后继是1,1的后继是2(0的后继的后继),以此类推)
关于分治法可以去补补算法课,MIT 6.006/6.046 都可以的。
我一直觉着分治和动态规划几乎是一回事。。。
都是转成子问题,然后base case直接有解。
BrainTeaser /Just Dessert
这两个单元都是难题,会遇到各种各样奇怪又好玩的东西。有的太难我根本不会。感叹人要有自知之明啊,如果跟这些东西死磕太久,怕是要耗费大把时光还一无所得。
collatz
(define collatz
(letrec
([odd-case
(lambda (recur)
(lambda (x)
(cond
((and (positive? x) (odd? x)) (collatz (add1 (* x 3))))
(else (recur x)))))]
[even-case
(lambda (recur)
(lambda (x)
(cond
((and (positive? x) (even? x)) (collatz (/ x 2)))
(else (recur x)))))]
[one-case
(lambda (recur)
(lambda (x)
(cond
((zero? (sub1 x)) 1)
(else (recur x)))))]
[base
(lambda (x)
(error 'error "Invalid value ~s~n" x))])
(one-case (odd-case (even-case base)));; this should be a single line, without lambda
))
这次的BrainTeaser叫 collatz,是个很有意思的程序,注意到每个case都是先拿一个参数作为出口,然后再返回一个 number->number
。 也就是
(number->number)->(number->number)
只有base是 number->number
然后就可以把每个case组合起来,得到一个考虑到所有case的number->number
base:number->number
odd-case (number->number)->(number->number)
(even-case base) : (number->number)
后面以此类推
quine
quine是这样一种程序,你对它求值,返回程序原代码本身。也就是说,把得到的东西再求职,还得到原代码本身。然后作业是让你自己构造个quine出来。。。
并不会啊。。。
quine参考链接
https://en.wikipedia.org/wiki/Quine_%28computing%29
http://www.nyx.net/~gthompso/quine.htm
后记/扯淡
厚颜无耻的贴个自己的答案https://github.com/hgztheyoung/c311/blob/master/c311recap/a1.rkt
Racket大法好啊。 #;可以注释掉接下来的一对括号内的内容。语义级别的注释真好用啊
注释可以加图片。多好的功能啊。(当然不能在作业里加,会和测试框架冲突。)
Racket的文档质量真的高。遇到程序里不懂的东西,比如match啊,letrec啊, 直接右键,查看文档就好。当然直接看 the little schemer/the seasoned schemer也可以。
DrRacket有重构表达式的功能,很好用,右键,重命名XXX,即可。不过要求整个文件都没有错误,比如重复定义,语法错误等等。
缩进,全选,Tab。即可。
关于Racket 的( ) 、[ ] 和 { }
一个意思, [ ] 起到美观的作用。我的惯用法是
(let ((x 2)
(y 3))
(+ x y))
(cond
((eq? #t #t) 1)
(else 2))
(match x
(1 2)
(#t 3))
写成
(let ([x 2]
[y 3])
(+ x y))
(cond
[(eq? #t #t) 1]
[else 2])
(match x
[1 2]
[#t 3])
这破文又扯了好多没用的啊。。。
自己认真做作业才会有收获的。
工作好难找啊。
我在行乞发广告摆摊卖艺寻求捐赠。
唯一指定邮箱是 hgz92929@sina.com