scheme
Table of Contents
links:
- https://schemers.org/
- http://www.scheme.com/
- SICP http://mitpress.mit.edu/sicp/
- TSPL http://www.scheme.com/tspl4/
- Daniel P. Friedman http://www.cs.indiana.edu/~dfried/
- An Introduction to Scheme and its Implementation
1. 两种delay/force实现
第一个是使用闭包,第二个使用mcons可变对象。可能可变对象的开销比较小。
;(define (my-delay f) ; (let [(x #f)] ; (lambda () ; (or x ; (begin ; (set! x (f)) ; x))))) ; ;(define (my-force f) (f)) (define (my-delay th) (mcons #f th)) ;; 还可以使用macro定义好看一些 ;(require compatibility/defmacro) ;(define-macro (my-delay th) ; `(mcons #f (lambda () ,th))) (define (my-force p) (if (mcar p) (mcdr p) (begin (set-mcar! p #t) (set-mcdr! p ((mcdr p))) (mcdr p))))
2. define-syntax
`define-syntax` 可能是racket语言专有的,感觉功能上比 `define-macro` 要更简单容易使用:
- 支持hygienic变量绑定
- 可以在一个special form里面定义多种形式
- 不用写,和,@, 宏会自动展开表达式
(define-syntax my-delay ; name (syntax-rules () ; keywords ;; special form cases [(my-delay th) (mcons #f (lambda () th))])) (define-syntax my-if (syntax-rules (then else) [(my-if e1 then e2 else e3) (if e1 e2 e3)] [(my-if e1 then e2) (if e1 e2 #f)])) (my-if #t then 20 else 30) (my-if #t then 20) (define-syntax my-double (syntax-rules () [(my-double e) (let [(x e)] ; hygienic variable bindings ; no need to call (gensym) (+ x x))])) (my-double (begin (printf "hello~n") 10)) (define-syntax while (syntax-rules () [(while e1 e2) (letrec [(loop (lambda () (if e1 (begin e2 (loop)) #f)))] (loop))])) (let [(x 0)] (while (< x 5) (begin (printf "save~n") (set! x (+ x 1)))))
3. 重新认识closure
关于closure的一些问题:
- closure包含了什么? closure包含了函数定义,以及创建这个函数时的上下文(或者叫做环境)。
- 什么时候创建closure? 在函数定义的时候,此时不仅仅需要保存函数定义,还要保存此时的环境。
- closure什么时候被调用? 当被call的时候,closure被调用。
- closure是如何计算的?
- 首先去除closure的函数定义部分,看这个函数有哪些形式参数;
- 在当前的环境current-env下面计算相应的实际参数,并且绑定到形式参数上,形成param-bindings;
- 然后将param-bindings添加到closure所在环境closure-env上;
- 最后在closure-env上面,计算closure的函数体部分。
下面这段代码是 https://www.coursera.org/learn/programming-languages-part-b/ 这么课程的作业,我摘取了代码片段来帮助说明上面这些问题
(struct fun (nameopt formal body) #:transparent) ;; a recursive(?) 1-argument function (struct call (funexp actual) #:transparent) ;; function call ;; a closure is not in "source" programs but /is/ a MUPL value; it is what functions evaluate to (struct closure (env fun) #:transparent) (define (eval-under-env e env) ;; (display "env = ") (print env) (display ", e = ")(print e) (newline) (cond [(var? e) (envlookup env (var-string e))] [(fun? e) (closure env e)] ;; 这里仅仅创建closure对象 [(call? e) (let ([clj (eval-under-env (call-funexp e) env)]) (if (closure? clj) (let* ([fn (closure-fun clj)] [clj-env (closure-env clj)] ;; 如果是有名称函数的话,在原有的env基础上增加name->clj的绑定 [ext-env (if (fun-nameopt fn) (extend-env (fun-nameopt fn) clj env) env)] [ext-clj-env (if (fun-nameopt fn) (extend-env (fun-nameopt fn) clj clj-env) clj-env)] [new-env (extend-env (fun-formal fn) ;; 在当前环境下面对实参求值,这个求值也可以使用当前函数 (eval-under-env (call-actual e) ext-env) ;; 绑定到closure的环境上,对body内部求值 ext-clj-env)]) (eval-under-env (fun-body fn) new-env)) (error "MULP call applied to non-closure")))]))
因为closure实现上需要保存环境,如果翻译成为C语言的话,通常需要在函数参数列表最后面增加一个参数 `void *env`. closure在运行效率上没有什么问题, 所有对于变量的访问都可以转换成为直接内存访问,而不用去在运行时查表找到内存。closure效率问题主要是在空间上,就是如何来保存这个env.
一种比较有效的实现,就是我们不保存整个env, 而是将函数里面所有使用到的外部符号解析出来,然后只保存这些符号映射表。这些外部符号变量称为自由变量(free variables). 因为每个函数里面通常使用的外部变量不是很多,所以这些符号映射表并不是很大,相对保存整个env可以节省不少空间。
具体实现上,可以分为两步:1. 首先分析整个函数体,分析出这个函数体使用了哪些外部符号(freevars) 2. 在生成代码或者是解释代码阶段,可以给这些freevars生成映射表。 如果是生成代码的话,还可以计算这个映射表的大小,以及每个变量所在的偏移。
;; 检查每个表达式中使用了那些变量,并且返回 ;; 在fun这个表达式中,排除掉形参,就是使用fun的freevars. (define (compute-free-vars e) (define (C e) (cond [(var? e) (cons e (set-add (set) (var-string e)))] [(add? e) (let ([v1 (C (add-e1 e))] [v2 (C (add-e2 e))]) (cons (add (car v1) (car v2)) (set-union (cdr v1) (cdr v2))))] [(fun? e) (let* ([name (fun-nameopt e)] [formal (fun-formal e)] [v2 (C (fun-body e))] ;; body里面可以使用name和formal. 所以排除 [vars (set-remove (set-remove (cdr v2) formal) name)]) (cons (fun-challenge name formal (car v2) vars) vars))] [(mlet? e) (let* ([v1 (C (mlet-e e))] [v2 (C (mlet-body e))] ;; e里面不能使用mlet-var,所以不排除 ;; 但是body里面可以使用mlet-var,所以排除 [vars (set-union (cdr v1) (set-remove (cdr v2) (mlet-var e)))]) (cons (mlet (mlet-var e) (car v1) (car v2)) vars))]))) (define (create-env-by-vars env vars) (if (set-empty? vars) null (let ([var (set-first vars)]) (extend-env var (envlookup env var) (create-env-by-vars env (set-rest vars)))))) (define (eval-under-env-c e env) ;; (display "env = ") (print env) (display ", e = ")(print e) (newline) (cond [(var? e) (envlookup env (var-string e))] [(fun-challenge? e) (let ([new-env (create-env-by-vars env (fun-challenge-freevars e))]) (closure new-env e))] ;; 这里仅仅创建closure对象 [(call? e) (let ([clj (eval-under-env-c (call-funexp e) env)]) (if (closure? clj) (let* ([fn (closure-fun clj)] [clj-env (closure-env clj)] ;; 如果是有名称函数的话,在原有的env基础上增加name->clj的绑定 [ext-env (if (fun-challenge-nameopt fn) (extend-env (fun-challenge-nameopt fn) clj env) env)] [ext-clj-env (if (fun-challenge-nameopt fn) (extend-env (fun-challenge-nameopt fn) clj clj-env) clj-env)] [new-env (extend-env (fun-challenge-formal fn) ;; 在当前环境下面对实参求值,这个求值也可以使用当前函数 (eval-under-env-c (call-actual e) ext-env) ;; 绑定到closure的环境上,对body内部求值 ext-clj-env)]) ; (display "call ....") (newline) ; (display "new-env = ") (print new-env) (newline) ; (display "fun-formal = ") (print (fun-formal fn)) (newline) ; (display "call-actual = ") (print (call-actual e)) (newline) ; (display "fun-body = ") (print (fun-body fn)) (newline) (eval-under-env-c (fun-challenge-body fn) new-env)) (error "MULP call applied to non-closure")))]))
4. let,let*,letrec
它们之间的差别是,let是一次性绑定,let*则是顺序绑定。letrec是let的扩展,解决递归函数问题,语义上相同用于一次性现将所有的ids创建好。
let和let*之间的差别可以用下面代码说明
(define x 10) (let ([x 5] [y (+ x 1)]) y) ;; 11 (let* ([x 5] [y (+ x 1)]) y) ;; 6
那么letrec是如何解决递归问题的呢?这点我在完成 https://www.coursera.org/learn/programming-languages-part-b/ 这么课程的作业在考虑。 因为这个作业里面需要使用自己定义的MUPL语言来实现curring map. 一种是直接返回函数,一种则是使用mlet绑定匿名函数到变量,然后返回变量。
;;; 使用scheme语言实现curring map. (define (my-map-let f) (letrec ([foo (lambda (xs) (if (null? xs) null (cons (f (car xs)) (foo (cdr xs)))))]) foo)) (define (my-map f) (define (foo xs) (if (null? xs) null (cons (f (car xs)) (foo (cdr xs))))) foo) ;;; 下面MUPL对应版本 (define mupl-map (let ([xs (var "xs")] [fn (var "fn")] [loop (var "loop")]) (fun #f "fn" (fun "loop" "xs" (ifaunit xs (aunit) (apair (call fn (fst xs)) (call loop (snd xs)))))))) (define mupl-map-mlet (let ([xs (var "xs")] [fn (var "fn")] [loop (var "loop")]) (fun #f "fn" (mlet "loop" (fun #f "xs" (ifaunit xs (aunit) (apair (call fn (fst xs)) (call loop (snd xs))))) loop))))
但是因为MUPL里面的mlet语义其实类似scheme里面的let而不是letrec, 所以在 `(call loop)` 时候会找不到定义。是否可以将mlet实现成为letrec呢?如何实现呢?
我想了想其实是不需要的,MUPL和scheme不同,MUPL可以将 `(fun …)` 对象赋值,而scheme没有类似 `(let ([x (define (loop …))]))` 这样的结构。 上面函数我们稍作修改其实就可以工作了。
(define mupl-map-mlet (let ([xs (var "xs")] [fn (var "fn")] [loop (var "loop")]) (fun #f "fn" (mlet "anything" (fun "loop" "xs" (ifaunit xs (aunit) (apair (call fn (fst xs)) (call loop (snd xs))))) (var "anything")))))