6 — Recursive Functions
Friday, 24 January 2020
Presenters (1) M Eldridge, K Zafar (2) N. Resnik, T Sachleben
The Design of Locally Recursive Functions
Language designers can choose from several design alternatives:
- some introduce explicitly recursive function declarations, something that we might represent with
(rec-fun fname parameter body)
that is, an alternative fun expression.But this is rather old fashioned and rarely found in modern languages.
- some change decl so that its scope is recursive, that is, in
(decl vat rhs-expr body-expr)
var is visible in both rhs-expr and body-expr.This change yields recursive functions. It is also easy to generalize so that decl declares many mutually recursive functions in one single block mixed together with ordinary variable initializations.
The design is orthogonal in that it keeps decl and fun cleanly separated.
But it allows programmers to write strange programs:(decl "x" (node + "x" 1) "x")
This alternative is often used in untyped (aka "dynamic") languages.
others restrict the syntax of decl so that only fun can show up as the initial value of the variable:
some specialize decl so that its scope is recursive, that is, in(decl fname (fun var function-body-expr) body-expr)
Now fname is visible in (fun ...) and body-expr.
Stop! With otherwise ordinary scoping rules in place, it is possible that fname is not visible in function-body-expr. Illustrate the idea with a concrete example.
This design generalizes to nests of recursive functions but it demands a separate form for variable initialization. Indeed, it has a minor effect on the programmer’s experience. In an expression such asf cannot "see" g, meaning programmers must spend some more time thinking about arranging blocks.On the positive side, the sacrifice of orthogonality avoids the problem with referring to variables while their initial value is being determined.
This design is common in typed languages.
(if-0 test-expr then-expr else-expr)
Let’s study a simple version of the second design alternative in some detail.
Lectures/6/rec-as-data.rkt
#lang racket ;; internal representation of BSL (functional expressions) (struct node [op left right] #:transparent) (struct decl [variable value scope] #:transparent) (struct fun [parameter body] #:transparent) (struct call [fname argument] #:transparent) (struct if-0 [test then else] #:transparent) #; {RecExpr = Int || (node O RecExpr RecExpr) || (decl Var RecExpr RecExpr) || Var (fun Var RecExpr) (call RecExpr RecExpr) ;; - - - - - - - - - - - - - (if-0 RecExpr RecExpr RecExpr)} #; {O = + || *} #; {Var = String} (define (rec-expr? x) (or (integer? x) (node? x) (decl? x) (string? x) ;; - - - - - - - - - - - - - (fun? x) (call? x) (if-0? x))) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; SCOPE in changed: #; (decl var rhs-expr body-expr) ;; var is visible in _both_ rhs-expr and _body-expr_ ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; example (define prog1 ;; factorial, the dumbest recursive example ever (decl "!" (fun "x" (if-0 "x" 1 (node * "x" (call "!" (node + "x" -1))))) (call "!" 10))) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (provide rec-expr? (struct-out node) (struct-out decl) (struct-out fun) (struct-out call) (struct-out if-0) prog1)
Revising the Interpreter— Why the Design Recipe Matters
in the third clause because it concerns decls
the addition of a final clause, concerning if-0 expressions
The first change demands a radical change in scoping, which is what
environments represent. Hence we wish—
The second change calls for a straightforward exercise in program design. The recipe gives us everything we need.
Lectures/6/rec-interpreter.rkt
#lang racket (require "environment.rkt") (require "rec-as-data.rkt") (require "../4/possible-values.rkt") #; {Value = Number || (function-value parameter FExpr Env)} #; {FExpr -> Value} ;; determine the value of ae via a substitutione semantics (define (value-of ae0) #; {FExpr Env -> Value} ;; ACCUMULATOR env tracks all declarations between ae and ae0 (define (value-of ae env) (match ae [(? integer?) ae] [(node o a1 a2) (o (value-of a1 env) (value-of a2 env))] [(decl x a1 a2) (value-of a2 (add-rec x (λ (env) (value-of a1 env)) env))] [(? string?) (if (defined? ae env) (lookup ae env) (error 'value-of "undeclared variable ~e" ae))] [(call ae1 ae2) (fun-apply (value-of ae1 env) (value-of ae2 env))] [(fun para body) (function-value para body env)] ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [(if-0 t thn els) (define test-value (value-of t env)) (if (and (number? test-value) (= test-value 0)) (value-of thn env) (value-of els env))])) #; {Value Value -> Value} (define (fun-apply function-representation argument-value) (match function-representation [(function-value fpara fbody env) (value-of fbody (add fpara argument-value env))])) (value-of ae0 empty)) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (module+ test (require rackunit) (define (! n) (if (zero? n) 1 (* n (! (sub1 n))))) (check-equal? (value-of prog1) (! 10))) (provide value-of)
The Revised Implementation of Environments
When we wish to extend an environment so that it represents a recursive scope, we are saying that the extended environment is supposed to be the environment of the newly added value. But since we don’t have the environment yet, that’s a problem.
"a level of indirection solves every problem"
Here the indirection is that the value to be added to the environment depends on the newly created environment. And that’s why we study mathematics in K-12. The word "depends" should immediately trigger a thought of "is a function of". Seen this way add-rec has the signature:
; Var [Env -> Val] -> Env
The question is who should apply this function and when.
It might be tempting to think of this:(define (add-rec x val-maker env) (define env++ (cons (list x val-maker) env)) (define val++ (val-maker env++)) (cons (list x val++) env)) But, is env++ the resulting environment to which the value maker should be applied? After all, the function returns a different environment in the end.This is where the second design idea comes in handy.
when we use "information hiding" two or more "methods" can conspire to accomplish a common purpose.
Instead of trying to make the value right in add-rec, we create a different kind of environment extension, one that records that we have entered a value maker, not a value.
Of course, this means that lookup must do something special. Fortunately it knows the exact environment that add-rec created and can create the value by applying val-maker.
The following figure displays a typed variant of environments.
Lectures/6/typed-environment.rkt
#lang typed/racket (provide empty add add-rec defined? lookup) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; here is one way to implement an environment (define-type Env [Listof Entry]) (define-type Make (Env -> Val)) (define-type Entry (U [List Var Val] [List Var Symbol Make])) (define-type Var String) (require/typed "rec-as-data.rkt" [#:opaque RecExpr rec-expr?]) (require/typed "../4/possible-values.rkt" [#:opaque FVal function-value?]) (define-type Val (U Number FVal)) (: empty Env) (define empty '[]) (: add (Var Number Env -> Env)) (define (add x val env) (cons (list x val) env)) (: add-rec (Var [Env -> Val] Env -> Env)) (define (add-rec x val-maker env) (cons (list x 'rec val-maker) env)) (: defined? (Var Env -> Any)) (define (defined? x env) (assq x env)) (: lookup (Var Env -> Val)) ;; ASSUME #; (defined? x env0) (define (lookup x env0) (let lookup ([env env0]) (cond [(equal? (first (first env)) x) (dispatch env)] [else (lookup (rest env))]))) (: dispatch (Env -> Val)) (define (dispatch env) (match (first env) [(list (? string? x) 'rec val) (val env)] [(list (? string? x) val) val]))
The Problem
(value-of (decl "x" (node + "x" 1) "x"))