Logo
  • Home
  • Classes
  • Conveying Computing
  • Exams
  • Fractal Gallery
  • Guides
  • Problem Sets
  • Syllabus

PS7 Comments

Question 1: Explain the result of the last evaluation above, evalInGlobal("square"). (Hint: use str to turn this value into a string.)
Invoking evalInGlobal("(define square (lambda (x) (* x x)))") creates an instance of the Procedure class in the globalEnvironment. When we invoke evalInGlobal("square"), we only give the the name of a Procedure instance to the interpreter, with no arguments, which evaluates to a pointer to the object itself. Our "square" is an instance of __main__.Procedure stored in memory at location 0x03C0BE18.

Evaluating str(evalInGlobal("square")) results in the string

   <Procedure ['x'] / ['*', 'x', 'x']>
because str() invokes the __str__ function defined in the Procedure class:
   def __str__(self):
       return '<Procedure %s / %s>' % (str(self._params), str(self._body))

Question 2: Define a factorial procedure in Charme. Express your procedure as string in Python by defining a variable called charmeFactorialDefinition. When you evaluate evalInGlobal(charmeFactorialDefinition), it should define a Charme procedure called factorial. Note that the Charme interpreter does not support the full Scheme language.
(define factorial
   (lambda (x)
      (if (= x 0) 1 (* x (factorial (- x 1))))))
Question 3: Extend the Charme interpreter by adding a primitive procedure <= to the global environment. You will need to define a procedure that implements the primitive, and modify initializeGlobalEnvironment to install your primitive.
To add the <= procedure we define primitiveLessThanOrEqualTo using the Python <= operator:
def primitiveLessThanOrEqualTo (operands):
    checkOperands (operands, 2, "<=")
    return operands[0] <= operands[1]
Then, we add it to the global environment:
def initializeGlobalEnvironment():
    global globalEnvironment
    globalEnvironment = Environment(None)
    globalEnvironment.addVariable('true', True)
    globalEnvironment.addVariable('false', False)
    globalEnvironment.addVariable('+', primitivePlus)
    globalEnvironment.addVariable('-', primitiveMinus)
    globalEnvironment.addVariable('*', primitiveTimes)
    globalEnvironment.addVariable('=', primitiveEquals)
    globalEnvironment.addVariable('zero?', primitiveZero)
    globalEnvironment.addVariable('>', primitiveGreater)
    globalEnvironment.addVariable('<', primitiveLessThan)
    globalEnvironment.addVariable('<=', primitiveLessThanOrEqualTo)
Question 4: Extend the Charme interpreter by adding primitive procedures cons, car and cdr that behave similarly to the primitive Scheme procedures. (Read on for some hints.)
We start by defining a class that represents a cons cell. It has a constructor __init__ that takes two inputs (the first and second parts of the pair), and provides methods getFirst and getSecond that retrieve the respective parts of the pair. We also define a __str__(self): method so that evalLoop and evalToplevelExp will print out Cons cells similarly to how they are displayed in Scheme.
class Cons:
    def __init__(self, left, right):
        self._left = left
        self._right = right

    def __str__(self):
        return "(%s . %s)" % (str(self._left), str(self._right))

    def getFirst(self):
        return self._left

    def getSecond(self):
        return self._right

Then, we define the primitive procedures:
def primitiveCons (operands):
    checkOperands (operands, 2, "cons")
    res = Cons(operands[0], operands[1])
    return res

def primitiveCar(operands):
    checkOperands(operands, 1, "car")
    return operands[0].getFirst();

def primitiveCdr(operands):
    checkOperands(operands, 1, "cdr")
    return operands[0].getSecond();
We add these to the global environment, by adding these three lines to initializeGlobalEnvironment:
    globalEnvironment.addVariable('cons', primitiveCons)
    globalEnvironment.addVariable('car', primitiveCar)
    globalEnvironment.addVariable('cdr', primitiveCdr)
Since we used the special name __str__ for our procedure that produces a string representation of a cons, the cons pairs will display correctly in the evalLoop with,
    print str(meval (expr, globalEnvironment))
Question 5: Extend the Charme interpreter by defining the null and null? primitives. (Note that names in Python cannot include question marks, so you will have to use a different name for the Python procedure you use to implement null?.)
We can use anything we want to represent null, as long as we define null? correspondingly. The most obvious thing to use is the Python value None:
def primitiveIsNull (operands):
    checkOperands(operands, 1, "null?")
    return operands[0] == None
(note that we cannot use ? in identifier names in Python, so need to use a different name).

To define null (which is not a procedure), we just add it to the global environment by adding these statements to initializeGlobalEnvironment:

    globalEnvironment.addVariable('null', None) 
    globalEnvironment.addVariable('null?', primitiveIsNull)

Question 6: Extend the Charme interpreter by defining the list primitive procedure. Like the Scheme list primitive procedure, it should take any number of operands and produce as output a list containing each operand as an element in order.
To add the list primitive, we define primitiveList as:
def primitiveList (operands):
    if (len(operands) == 0):
        return None
    else:
        return Cons(operands[0], primitiveList(operands[1:]))
and add it to the global environment:
    globalEnvironment.addVariable('list', primitiveList)
With this definition, lists will just print out as nested cons pairs:
Charme> (list 1 2 3 4)
(1 . (2 . (3 . (4 . None))))
If we want them to print out as they do in DrRacket, we also need to modify the __str__ method of Cons to do something special when the cons pair is a list.

We define isList:

def isList(expr):
    if expr == None:
        return True
    elif isinstance(expr, Cons):
        return isList(expr.getSecond())
    else:
        return False
Note that this matches exactly our definition of the list datatype - either null, or a pair whose second part is a list!

Then, we modify the Cons __str__ method (using the new strAsList helper method:

class Cons:
    def __init__(self, left, right):
        self._left = left
        self._right = right

    def __str__(self):
        if isList(self):
            return "(%s)" % self.strAsList()
        return "(%s . %s)" % (str(self._left), str(self._right))

    def strAsList(self):
        if self._right == None:
            return str(self._left)
        else:
            return str(self._left) + " " + self._right.strAsList()

    def getFirst(self):
        return self._left

    def getSecond(self):
        return self._right
Question 7: Extend the Charme interpreter to support the cond special form, with the same meaning as the Scheme cond expression. (Your Charme cond expression does not need to support the special else syntax supported by Scheme.)
Here is the procedure for the conditional evaluation rule:
def isConditional(expr):
    return isSpecialForm(expr, 'cond')

def evalConditional(expr, env):
    assert isConditional(expr)
    if len(expr) <= 2:
        evalError ("Bad conditional expression: %s" % str(expr))
    for clause in expr[1:]:
        if len(clause) != 2:
            evalError ("Bad conditional clause: %s" % str(clause))
        predicate = clause[0]
        result = meval(predicate, env)
        if not result == False:
            return meval(clause[1], env)
    return None
We also need to add a clause for cond expressions to meval:
def meval(expr, env):
    if isPrimitive(expr):
       return evalPrimitive(expr)
    elif isIf(expr):             
       return evalIf(expr, env)
    elif isConditional(expr):
       return evalConditional(expr, env)
    elif isDefinition(expr):                
       evalDefinition(expr, env)
    elif isName(expr):
       return evalName(expr, env)
    elif isLambda(expr):
       return evalLambda(expr, env)
    elif isApplication(expr):
       return evalApplication(expr, env)
    else:
       error ('Unknown expression type: ' + str(expr))

Question 8: Modify the Charme interpreter to support memoizing for all procedure applications. (Hint: review Python dictionaries from Class 31.)
We have to figure out where to store the memoized results and how to maintain them. Many answers attempted to store the results globally, with a global dictionary of <procedure(operands), value> pairs. This sort of works, except is assumes the same name always refers to the same procedure. This is not the case with the environment model of evaluation. We would need a dictionary associated with each environment instead of a global dictionary. An easier approach is to associate the dictionary with the procedure. This is better because it is simpler, but it also means we can memoize results for procedures that do not have names. The memoized results table is associated with the procedure itself (which was produced by a lambda expression). (The one drawback of this approach is it means we are not memoizing the results of primitive procedures. Since the provided primitives are all fast procedures, however, this is not a serious drawback.)

The modified Procedure class adds the _memos instance variable, initializes it to the empty dictionary, and defines the procedure hasResult, storeResult, and getResult:

class Procedure:
    def __init__(self, params, body, env):
        self._params = params
        self._body = body
        self._env = env
        self._memos = { }

    def getParams(self):
        return self._params

    def getBody(self):
        return self._body

    def getEnvironment(self):
        return self._env

    def hasResult(self, operands):
        return self._memos.has_key(str(operands))

    def storeResult(self, operands, result):
        self._memos[str(operands)] = result

    def getResult(self, operands):
        assert self.hasResult(str(operands))
        return self._memos[str(operands)]
    def __str__(self):
        return "" % (str(self._params), str(self._body))
We use str(operands) as the key to our dictionary. Dictionary keys cannot be lists, so we need to turn the operands into a string. Since the operands are the values of the operand subexpressions, not the expressions, they have already been evaluated. Hence, we do not need to worry about names having different values in the operand values list.

We modify mapply to check if an application has a stored result. If it has a stored result, we use that as the result. Otherwise, we compute the result as before, and store it in the memoized results table.

def mapply(proc, operands):
    if (isPrimitiveProcedure(proc)):
        return proc(operands)
    elif isinstance(proc, Procedure):
            params = proc.getParams()
            if proc.hasResult(operands):
                return proc.getResult(operands)
            else:
                newenv = Environment(proc.getEnvironment())
                if len(params) != len(operands):
                    evalError ("Parameter length mismatch: %s given operands %s" % 
					           (proc.toString(), str(operands)))
                for i in range(0, len(params)):
                    newenv.addVariable(params[i], operands[i])        
                result = meval(proc.getBody(), newenv)
                proc.storeResult(operands, result)
                return result
    else:
        evalError("Application of non-procedure: %s" % (proc))
Now, evaluating (fibo 60) takes no time at all!
Charme> (define fibo (lambda (n) (if (= n 1) 1 (if (= n 2) 1 (+ (fibo (- n 1)) (fibo (- n 2)))))))
Charme> (fibo 60)
1548008755920

Print Friendly Print Get a PDF version of this webpage PDF

Leave a Reply Cancel reply

You must be logged in to post a comment.

cs1120 | RSS | Comments RSS | Book | Using These Materials | Login | Admin | Powered by Wordpress