A memoized thunk performs demanded work at most once.
Delay compound-procedure operands, force primitive operands and predicates, and rewrite a thunk into an evaluated-thunk after its first actual-value request.
Guiding question
How can an evaluator postpone argument work while preventing repeated references from recomputing the same expression?
Delay operand expressions together with their calling environments
Force an operator before deciding how to apply it
Force every primitive argument and if predicate to an actual value
Pass delayed arguments to compound procedures
Memoize a thunk by changing its tag and stored payload
Observe unused, repeated, and unselected expressions separately
apply-lazy distinguishes host primitives from represented compound procedures. A primitive needs actual argument values, so its operands are forced before host apply. A compound procedure instead receives thunk records containing the original operand expressions and calling environment. Variable lookup returns that record, and actual-value decides whether it must be forced.
force-it evaluates a fresh thunk once, rewrites the same mutable record to evaluated-thunk, stores the result, and discards the saved expression and environment. The unused argument therefore performs no probe. The duplicated x expression is demanded twice but probes once. The false branch of the selected if is never evaluated. This evaluator implements call by need for the listed forms under an explicit work budget.
SICP code7,892 of 1,048,576 UTF-8 bytes
(begin(define(tagged-list?expressiontag)(and(pair?expression)(eq?(carexpression)tag)))(define(self-evaluating?expression)(or(number?expression)(string?expression)(boolean?expression)))(define(quoted?expression)(tagged-list?expression'quote))(define(if?expression)(tagged-list?expression'if))(define(lambda?expression)(tagged-list?expression'lambda))(define(begin?expression)(tagged-list?expression'begin))(define(definition?expression)(tagged-list?expression'define))(define(text-of-quotationexpression)(cadrexpression))(define(if-predicateexpression)(cadrexpression))(define(if-consequentexpression)(caddrexpression))(define(if-alternativeexpression)(cadddrexpression))(define(lambda-parametersexpression)(cadrexpression))(define(lambda-bodyexpression)(cddrexpression))(define(begin-actionsexpression)(cdrexpression))(define(definition-variableexpression)(if(symbol?(cadrexpression))(cadrexpression)(car(cadrexpression))))(define(definition-valueexpression)(if(symbol?(cadrexpression))(caddrexpression)(cons'lambda(cons(cdr(cadrexpression))(cddrexpression)))))(define(operatorexpression)(carexpression))(define(operandsexpression)(cdrexpression))(define(pair-bindingsvariablesvalues)(cond((and(null?variables)(null?values))'())((null?variables)(error"too many guest arguments"))((null?values)(error"too few guest arguments"))(else(cons(cons(carvariables)(carvalues))(pair-bindings(cdrvariables)(cdrvalues))))))(define(make-framevariablesvalues)(cons'*frame*(pair-bindingsvariablesvalues)))(define(first-frameenvironment)(carenvironment))(define(frame-bindingsframe)(cdrframe))(define(extend-environmentvariablesvaluesbase)(cons(make-framevariablesvalues)base))(define(lookup-variable-valuevariableenvironment)(if(null?environment)(error"unbound lazy guest variable"variable)(let((record(assocvariable(frame-bindings(first-frameenvironment)))))(ifrecord(cdrrecord)(lookup-variable-valuevariable(cdrenvironment))))))(define(define-variable!variablevalueenvironment)(let*((frame(first-frameenvironment))(record(assocvariable(frame-bindingsframe))))(ifrecord(set-cdr!recordvalue)(set-cdr!frame(cons(consvariablevalue)(frame-bindingsframe)))))(list'definedvariable))(define(make-procedureparametersbodyenvironment)(list'compoundparametersbodyenvironment))(define(compound-procedure?procedure)(tagged-list?procedure'compound))(define(procedure-parametersprocedure)(cadrprocedure))(define(procedure-bodyprocedure)(caddrprocedure))(define(procedure-environmentprocedure)(cadddrprocedure))(define(delay-itexpressionenvironment)(list'thunkexpressionenvironment))(define(thunk?object)(tagged-list?object'thunk))(define(evaluated-thunk?object)(tagged-list?object'evaluated-thunk))(define(force-itobject)(cond((thunk?object)(let((result(actual-value(cadrobject)(caddrobject))))(set-car!object'evaluated-thunk)(set-car!(cdrobject)result)(set-cdr!(cdrobject)'())result))((evaluated-thunk?object)(cadrobject))(elseobject)))(define(actual-valueexpressionenvironment)(force-it(lazy-evalexpressionenvironment)))(define(true?value)(not(eq?value#f)))(define(eval-ifexpressionenvironment)(if(true?(actual-value(if-predicateexpression)environment))(lazy-eval(if-consequentexpression)environment)(lazy-eval(if-alternativeexpression)environment)))(define(eval-sequenceexpressionsenvironment)(cond((null?expressions)'ok)((null?(cdrexpressions))(lazy-eval(carexpressions)environment))(else(actual-value(carexpressions)environment)(eval-sequence(cdrexpressions)environment))))(define(eval-definitionexpressionenvironment)(define-variable!(definition-variableexpression)(actual-value(definition-valueexpression)environment)environment))(define(list-of-arg-valuesexpressionsenvironment)(if(null?expressions)'()(cons(actual-value(carexpressions)environment)(list-of-arg-values(cdrexpressions)environment))))(define(list-of-delayed-argsexpressionsenvironment)(if(null?expressions)'()(cons(delay-it(carexpressions)environment)(list-of-delayed-args(cdrexpressions)environment))))(define(apply-lazyprocedureargument-expressionscalling-environment)(cond((procedure?procedure)(applyprocedure(list-of-arg-valuesargument-expressionscalling-environment)))((compound-procedure?procedure)(eval-sequence(procedure-bodyprocedure)(extend-environment(procedure-parametersprocedure)(list-of-delayed-argsargument-expressionscalling-environment)(procedure-environmentprocedure))))(else(error"not a lazy guest procedure"procedure))))(define(lazy-evalexpressionenvironment)(cond((self-evaluating?expression)expression)((symbol?expression)(lookup-variable-valueexpressionenvironment))((quoted?expression)(text-of-quotationexpression))((if?expression)(eval-ifexpressionenvironment))((lambda?expression)(make-procedure(lambda-parametersexpression)(lambda-bodyexpression)environment))((begin?expression)(eval-sequence(begin-actionsexpression)environment))((definition?expression)(eval-definitionexpressionenvironment))((pair?expression)(apply-lazy(actual-value(operatorexpression)environment)(operandsexpression)environment))(else(error"unknown lazy guest expression"expression))))(defineprobes0)(define(probevalue)(set!probes(+probes1))value)(defineprimitive-bindings(list(cons'++)(cons'--)(cons'**)(cons'//)(cons'==)(cons'<<)(cons'>>)(cons'listlist)(cons'conscons)(cons'carcar)(cons'cdrcdr)(cons'null?null?)(cons'pair?pair?)(cons'probeprobe)))(defineglobal-environment(list(cons'*frame*primitive-bindings)))(defineunused'((lambda(x)1)(probe10)))(defineduplicated'((lambda(x)(+xx))(probe10)))(defineselected-branch'(if#t(quotesafe)(probe99)))(set!probes0)(defineunused-value(actual-valueunusedglobal-environment))(defineunused-probesprobes)(set!probes0)(defineduplicated-value(actual-valueduplicatedglobal-environment))(defineduplicated-probesprobes)(set!probes0)(definebranch-value(actual-valueselected-branchglobal-environment))(list(list'unusedunused-valueunused-probes)(list'duplicatedduplicated-valueduplicated-probes)(list'branchbranch-valueprobes)))
Follow compound application into list-of-delayed-args, then find the first lookup that sends a thunk to force-it. Verify the record mutation to evaluated-thunk and the second lookup returning the cached value. In the if expression, confirm that only the predicate and selected consequent are evaluated.
Try it yourself
Change the program and compare the result.
Add ((lambda (x) (+ x (+ x x))) (probe 4)) and predict the value and probe count. Then remove the thunk mutation in force-it and compare the result.
Show hint
With memoization the argument probes once no matter how many x lookups occur. Without mutation, every lookup re-evaluates the saved expression.