基于SICP中练习2.49的发散思维

经过SICP中练习2.49的实现,我发现在不知不觉之中我竟然几乎是实现了一个MIT-scheme的迷你作图库,然后我发现可以用这个迷你函数库来做一些有趣的事情。比如实现如下的一个画出割圆法的实现:
cyclotomy.scm

(load "enumerate-interval.scm")

; 均等分割一个单位圆周为 n 份
; 并做出内接正 n 边形
; 结果通过一个 line-list 返回
(define (cyclotomy n)
    ; 定义 n 等分后的弧度
    (define 2pi/n (/ (* 4 (asin 1)) n))
    
    ; 求出单位圆周上 n 个等分点的位置坐标
    ; 以列表的形式返回
    (define point-list
        (map
            (lambda (i)
                (let ((arg (* i 2pi/n)))
                    (list (cos arg) (sin arg))))
            (enumerate-interval 0 (-1+ n))))
    
    ; 连接两个点成一条直线
    (define (get-line p1 p2)
        (append p1 p2))
    
    ; 将等分点连接成正多边形
    (define (get-polygon first used-again other result)
        (if (null? other)
            (cons (get-line used-again first) result)
            (let ((temp (car other)))
                (get-polygon
                    first
                    temp
                    (cdr other)
                    (cons (get-line used-again temp) result)))))
    
    ; 返回多边形列表
    (let ((first (car point-list)))
        (get-polygon
            first
            first
            (cdr point-list)
            (list))))

在这里因为不想导入太多的东西,所以很多的内容就直接在过程cyclotom之中定义了,而不是从向量工具模块中导入。如果导入上一篇文章中的vector-tools.scm,相应的,代码应该可以得到很大的简化。但是这样写有利于读者阅读,从而不必再去从上一篇文章中寻找对应的过程了。
运行的测试代码如下:
test.scm

(load "cyclotomy.scm")

; 定义一个正方形
; (define line-list
    ; (list
        ; (list (- 1) 0 0 1)
        ; (list 0 1 1 0)
        ; (list 1 0 0 (- 1))
        ; (list 0 (- 1) (- 1) 0)))

; 定义一个正八边形
; (define line-list
    ; (let ((sq2 (/ 1 (sqrt 2))))
        ; (list
            ; (list (- 1) 0 (- sq2) sq2)
            ; (list (- sq2) sq2 0 1)
            ; (list 0 1 sq2 sq2)
            ; (list sq2 sq2 1 0)
            ; (list 1 0 sq2 (- sq2))
            ; (list sq2 (- sq2) 0 (- 1))
            ; (list 0 (- 1) (- sq2) (- sq2))
            ; (list (- sq2) (- sq2) (- 1) 0))))


; 定义一个正三角形
;(define line-list (cyclotomy 3))

; 定义一个正二十边形
;(define line-list (cyclotomy 20))

; 定义一个正七边形
;(define line-list (cyclotomy 7))

; 定义一个正100边形
; 效果非常接近一个圆
(define line-list (cyclotomy 100))

; 画 line-list
; 并延时显示 5秒
(load "draw.scm")

运行效果如下图:
正100边形
接下来我们可以更近一步,比如画一下函数 y=f(x):
draw-function.scm

(load "vector-tools.scm")
(load "enumerate-interval.scm")

; 在区域 {(x,y)|-range <= x,y <= range} 内
; 绘制函数 y = f(x) 的图像
; 逼近程度由 clarity 控制
; 区域内函数不能存在瑕点
(define (draw-function f range clarity)
    ; 定义最小分割单位
    (define unit (/ (* 2 range) clarity))
    
    ; 获得坐标数据
    (define get-point-list
        (map
            (lambda (n)
                (let ((x (+ (- range) (* n unit))))
                    (point x (f x))))
            (enumerate-interval 0 clarity)))
    
    ; 将点连线成函数图形
    (define (get-segment-list point other result)
        (if (null? other)
            result
            (let ((temp (car other)))
                (get-segment-list
                    temp
                    (cdr other)
                    (cons (make-segment point temp) result)))))
    
    ; 返回函数图像列表
    (get-segment-list
        (car get-point-list)
        (cdr get-point-list)
        (list)))

先来做一下测试:
test.scm

(load "segment-painter.scm")
(load "draw-function.scm")

; 在区域 {(x,y)|-7 <= x,y <= 7} 内
; 画一个余弦函数
; (define segment-list
    ; (draw-function
        ; cos
        ; 7
        ; 1000))

; 在区域 {(x,y)|-1.2 <= x,y <= 1.2} 内
; 画一个正切函数
(define segment-list
    (draw-function
        tan
        1.2
        1000))

; 在区域 {(x,y)|-3 <= x,y <= 3} 内
; 画一个半径为3的圆
; (define segment-list
    ; (append 
        ; (draw-function
            ; (lambda (x) (sqrt (- 9 (square x))))
            ; 3
            ; 1000)
        ; (draw-function
            ; (lambda (x) (- (sqrt (- 9 (square x)))))
            ; 3
            ; 1000)))

(define line-list
    (transform-in segment-list #t))
; 画 line-list
; 并延时显示 5秒
(load "draw.scm")

以下是画出的tan函数的效果:
tan函数图
接下来可以画出一个cos函数:
cos函数图
但是,还有一个问题就是正如我源代码中所备注的那样,在作图的定义域范围内存在瑕点的函数画不出来。
首先,这可能跟我的算法有关,因为我是基于先前的割圆法得来的函数近似作图的方法,其实画出的不是函数本身,而是一副非常接近于函数的折线图,就像小学的时候我们在数学课上所画的数据统计方面的折线图那样,只是选取的坐标点非常多,看上去非常接近函数本身了而已。
其次是因为我的一个过程 transform-in 的缘故,当你查看上一篇文章中关于这个过程的定义时就会发现它其实是一个等比例缩放图形的过程。因为MIT-scheme中提供的默认画板只能做出在区域 {(x,y)|-1 <= x,y <= 1} 内的坐标点,所以我就定义了 transform-in 这个过程将更大范围内的图像反演到这个正方形之中。
结合这两个原因,这就导致了在对于瑕玷作图时 draw-function 过程的天生性bug。
那怎么处理这个bug呢?一种可能的方法是可以考虑在函数作图范围内如果遇到y坐标点在某处发散了,那就打一个图像的断点,不去求值, 然后依据断点再对图像列表做拆分,生成每一段的图像列表,那就都能保证在它们之上没有瑕玷的存在,然后对他们用 draw-function 做 map,这有点类似于在做瑕积分时候的处理方法。但是我们就此打住,因为这样想下去可能又会有新的bug,然后又去处理,这样地无限下去。诚然,一个程序的优化是一条永无止境的道路,但是作为一个生命有限的程序员来说应该要学会在哪里打住。
还有一个比较有意思的想法是这样的。之前实现过一个画图的框架draw.scm。我的想法是:执行这个脚本一次可以画出一张图片,那如果用for-each循环执行这个脚本,那MIT-scheme就会在屏幕的某一块区域内进行循环地作图,如果每个图片算1帧,那在循环的时候用delayed控制好每一帧出现的时间,我们是否就有可能实现一个简单的动画呢?但是我也不准备实现它了,我宁可把时间花在书中之后的内容上,因为我相信后面的内容应该会更有意思,哈哈。
最后要说一下SICP这本书,虽说我连前2章都还没有看完,但是已经受益匪浅了。这是一本启发思维的奇书,让你越看越喜欢,越看越爱不释手。我真不敢想象当我把它全部看完之后会变成什么样子,如果换成习武之人,它就好比一本易筋经,能让一个武功非常平庸的游坦之一夜之间变成可以和降龙十八掌过招的人,哈哈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值