7.7.0.3

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:

Recursion is also only useful when the language has a conditional expression. To keep things simple, let’s extend our representative language with an if-0 expression:

(if-0 test-expr then-expr else-expr)

The evaluation of the expression determines whether test-expr evaluates to 0 and, if so, produces the value of then-expr as the result of the entire expression; otherwise it produces the value of the 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)
   

Figure 23: Locally Defined Recursive Functions

Revising the Interpreter—Why the Design Recipe Matters

Given that we revised the scope of decl and added if-0, the interpreter should change in two ways:
  • in the third clause because it concerns decls

  • the addition of a final clause, concerning if-0 expressions

That’s why we use the design recipe to create programs—they must always be revised.

The first change demands a radical change in scoping, which is what environments represent. Hence we wish—as in wish list—for a new way, a recursive way, of extending environments and defer the actual work to the re-design of the environment implementation.

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)
   

Figure 24: Interpreting Locally Defined Recursive Functions

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.

Two design ideas solve this 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]))
   

Figure 25: An Environment with Recursive Extensibility

The Problem

Try

(value-of (decl "x" (node + "x" 1) "x"))