Agora que temos um avaliador expresso como um programa Lisp, podemos experimentar com escolhas alternativas no design da linguagem simplesmente modificando o avaliador. De fato, novas linguagens são frequentemente inventadas primeiro escrevendo um avaliador que incorpora a nova linguagem dentro de uma linguagem de alto nível existente. Por exemplo, se quisermos discutir algum aspecto de uma modificação proposta ao Lisp com outro membro da comunidade Lisp, podemos fornecer um avaliador que incorpora a mudança. O destinatário pode então experimentar o novo avaliador e enviar comentários como modificações adicionais. Não apenas a base de implementação de alto nível facilita o teste e a depuração do avaliador; além disso, a incorporação permite que o designer aproveite características da linguagem subjacente, assim como nosso avaliador Lisp incorporado usa primitivas e estrutura de controle da linguagem Lisp subjacente. Somente mais tarde (se for o caso) o designer precisará se preocupar em construir uma implementação completa em uma linguagem de baixo nível ou em hardware. Nesta seção e na próxima, exploraremos algumas variações no Scheme que fornecem poder expressivo adicional significativo.
Em 1.1, onde começamos nossa discussão sobre modelos de avaliação, nós observamos que o Scheme é uma linguagem de ordem aplicativa, ou seja, todos os argumentos para procedimentos do Scheme são avaliados quando o procedimento é aplicado. Em contraste, linguagens de ordem normal atrasam a avaliação dos argumentos do procedimento até que os valores reais dos argumentos sejam necessários. Atrasar a avaliação dos argumentos do procedimento até o último momento possível (por exemplo, até que sejam exigidos por uma operação primitiva) é chamado de avaliação preguiçosa.236 Considere o procedimento
(define (try a b)
(if (= a 0) 1 b))
Avaliar (try 0 (/ 1 0))
gera um erro no Scheme. Com
avaliação preguiçosa, não haveria erro. Avaliar a expressão retornaria
1, porque o argumento (/ 1 0)
nunca seria avaliado.
Um exemplo que explora a avaliação preguiçosa é a definição de um
procedimento
unless
(define (unless condition
usual-value
exceptional-value)
(if condition
exceptional-value
usual-value))
que pode ser usado em expressões como
(unless (= b 0)
(/ a b)
(begin
(display "exception: returning 0")
0))
Isso não funcionará em uma linguagem de ordem aplicativa porque tanto o
valor usual quanto o valor excepcional serão avaliados antes que
unless
seja chamado (compare
Exercício 1.6). Uma vantagem
da avaliação preguiçosa é que alguns procedimentos, como
unless
, podem realizar computações úteis mesmo que a
avaliação de alguns de seus argumentos produza erros ou não termine.
Se o corpo de um procedimento é executado antes que um argumento tenha sido avaliado, dizemos que o procedimento é não estrito em relação a esse argumento. Se o argumento é avaliado antes que o corpo do procedimento seja executado, dizemos que o procedimento é estrito em relação a esse argumento.237 Em uma linguagem puramente de ordem aplicativa, todos os procedimentos são estritos em cada argumento. Em uma linguagem puramente de ordem normal, todos os procedimentos compostos são não estritos em cada argumento, e procedimentos primitivos podem ser estritos ou não estritos. Há também linguagens (veja Exercício 4.31) que dão aos programadores controle detalhado sobre a estrita dos procedimentos que eles definem.
Um exemplo marcante de um procedimento que pode ser útilmente tornado
não estrito é cons
(ou, em geral, quase qualquer construtor
para estruturas de dados). Pode-se realizar computações úteis,
combinando elementos para formar estruturas de dados e operando nas
estruturas de dados resultantes, mesmo que os valores dos elementos não
sejam conhecidos. Faz todo o sentido, por exemplo, calcular o
comprimento de uma lista sem conhecer os valores dos elementos
individuais na lista. Nós exploraremos essa ideia em
4.2.3 para implementar os fluxos de
Capítulo 3 como listas formadas
por pares cons
não estritos.
Exercício 4.25: Suponha que (no Scheme de ordem aplicativa comum) definimos
unless
como mostrado acima e então definimosfactorial
em termos deunless
como(define (factorial n) (unless (= n 1) (* n (factorial (- n 1))) 1))
O que acontece se tentarmos avaliar
(factorial 5)
? Nossas definições funcionariam em uma linguagem de ordem normal?
Exercício 4.26: Ben Bitdiddle e Alyssa P. Hacker discordam sobre a importância da avaliação preguiçosa para implementar coisas como
unless
. Ben aponta que é possível implementarunless
em ordem aplicativa como uma forma especial. Alyssa contra-argumenta que, se alguém fizesse isso,unless
seria meramente sintaxe, não um procedimento que poderia ser usado em conjunto com procedimentos de ordem superior. Preencha os detalhes de ambos os lados do argumento. Mostre como implementarunless
como uma expressão derivada (comocond
oulet
), e dê um exemplo de uma situação onde seria útil terunless
disponível como um procedimento, em vez de como uma forma especial.
Nesta seção, implementaremos uma linguagem de ordem normal que é a mesma que o Scheme, exceto que os procedimentos compostos são não estritos em cada argumento. Procedimentos primitivos ainda serão estritos. Não é difícil modificar o avaliador de 4.1.1 para que a linguagem que ele interpreta se comporte dessa maneira. Quase todas as mudanças necessárias giram em torno da aplicação de procedimentos.
A ideia básica é que, ao aplicar um procedimento, o interpretador deve determinar quais argumentos devem ser avaliados e quais devem ser atrasados. Os argumentos atrasados não são avaliados; em vez disso, são transformados em objetos chamados thunks.238 O thunk deve conter as informações necessárias para produzir o valor do argumento quando ele for necessário, como se tivesse sido avaliado no momento da aplicação. Assim, o thunk deve conter a expressão do argumento e o ambiente em que a aplicação do procedimento está sendo avaliada.
O processo de avaliar a expressão em um thunk é chamado de forçamento.239 Em geral, um thunk será forçado apenas quando seu valor for necessário: quando ele for passado para um procedimento primitivo que usará o valor do thunk; quando ele for o valor de um predicado de uma condicional; e quando ele for o valor de um operador que está prestes a ser aplicado como um procedimento. Uma escolha de design que temos disponível é se devemos ou não memorizar thunks, como fizemos com objetos atrasados em 3.5.1. Com memorização, a primeira vez que um thunk é forçado, ele armazena o valor que é calculado. Forçamentos subsequentes simplesmente retornam o valor armazenado sem repetir a computação. Faremos nosso interpretador memorizar, porque isso é mais eficiente para muitas aplicações. No entanto, há considerações complicadas aqui.240
A principal diferença entre o avaliador preguiçoso e o de
4.1 está no tratamento de
aplicações de procedimentos em eval
e apply
.
A cláusula application?
de eval
se torna
((application? exp)
(apply (actual-value (operator exp) env)
(operands exp)
env))
Isso é quase o mesmo que a cláusula application?
de
eval
em 4.1.1.
Para avaliação preguiçosa, no entanto, chamamos apply
com
as expressões dos operandos, em vez dos argumentos produzidos por sua
avaliação. Como precisaremos do ambiente para construir thunks se os
argumentos forem atrasados, devemos passá-lo também. Ainda avaliamos o
operador, porque apply
precisa do procedimento real a ser
aplicado para despachar em seu tipo (primitivo versus composto) e
aplicá-lo.
Sempre que precisamos do valor real de uma expressão, usamos
(define (actual-value exp env)
(force-it (eval exp env)))
em vez de apenas eval
, para que, se o valor da expressão
for um thunk, ele seja forçado.
Nossa nova versão de apply
também é quase a mesma que a
versão em 4.1.1. A diferença
é que eval
passou expressões de operandos não avaliadas:
Para procedimentos primitivos (que são estritos), avaliamos todos os
argumentos antes de aplicar o primitivo; para procedimentos compostos
(que são não estritos), atrasamos todos os argumentos antes de aplicar o
procedimento.
(define (apply procedure arguments env)
(cond ((primitive-procedure? procedure)
(apply-primitive-procedure
procedure
(list-of-arg-values
arguments
env))) ; changed
((compound-procedure? procedure)
(eval-sequence
(procedure-body procedure)
(extend-environment
(procedure-parameters procedure)
(list-of-delayed-args
arguments
env) ; changed
(procedure-environment procedure))))
(else (error "Unknown procedure
type: APPLY"
procedure))))
Os procedimentos que processam os argumentos são como
list-of-values
de
4.1.1, exceto que
list-of-delayed-args
atrasa os argumentos em vez de
avaliá-los, e list-of-arg-values
usa
actual-value
em vez de eval
:
(define (list-of-arg-values exps env)
(if (no-operands? exps)
'()
(cons (actual-value
(first-operand exps)
env)
(list-of-arg-values
(rest-operands exps)
env)))
(define (list-of-delayed-args exps env)
(if (no-operands? exps)
'()
(cons (delay-it
(first-operand exps)
env)
(list-of-delayed-args
(rest-operands exps)
env))))
O outro lugar onde devemos mudar o avaliador é no tratamento de
if
, onde devemos usar actual-value
em vez de
eval
para obter o valor da expressão do predicado antes de
testar se é verdadeiro ou falso:
(define (eval-if exp env)
(if (true? (actual-value (if-predicate exp)
env))
(eval (if-consequent exp) env)
(eval (if-alternative exp) env)))
Finalmente, devemos mudar o procedimento driver-loop
(4.1.4) para usar actual-value
em vez de eval
, para
que, se um valor atrasado for propagado de volta ao loop de
leitura-avaliação-impressão, ele será forçado antes de ser impresso.
Também mudamos os prompts para indicar que este é o avaliador
preguiçoso:
(define input-prompt ";;; L-Eval input:")
(define output-prompt ";;; L-Eval value:")
(define (driver-loop)
(prompt-for-input input-prompt)
(let ((input (read)))
(let ((output (actual-value
input
the-global-environment)))
(announce-output output-prompt)
(user-print output)))
(driver-loop))
Com essas mudanças feitas, podemos iniciar o avaliador e testá-lo. A
avaliação bem-sucedida da expressão try
discutida em
4.2.1 indica que o interpretador está
realizando avaliação preguiçosa:
(define the-global-environment
(setup-environment))
(driver-loop)
;;; L-Eval input:
(define (try a b) (if (= a 0) 1 b))
;;; L-Eval value:
ok
;;; L-Eval input:
(try 0 (/ 1 0))
;;; L-Eval value:
1
Nosso avaliador deve organizar a criação de thunks quando procedimentos
são aplicados a argumentos e forçar esses thunks posteriormente. Um
thunk deve empacotar uma expressão junto com o ambiente, para que o
argumento possa ser produzido mais tarde. Para forçar o thunk,
simplesmente extraímos a expressão e o ambiente do thunk e avaliamos a
expressão no ambiente. Usamos actual-value
em vez de
eval
para que, caso o valor da expressão seja ele mesmo um
thunk, nós o forcemos, e assim por diante, até chegarmos a algo que não
seja um thunk:
(define (force-it obj)
(if (thunk? obj)
(actual-value (thunk-exp obj)
(thunk-env obj))
obj))
Uma maneira fácil de empacotar uma expressão com um ambiente é fazer uma lista contendo a expressão e o ambiente. Assim, criamos um thunk da seguinte forma:
(define (delay-it exp env)
(list 'thunk exp env))
(define (thunk? obj) (tagged-list? obj 'thunk))
(define (thunk-exp thunk) (cadr thunk))
(define (thunk-env thunk) (caddr thunk))
Na verdade, o que queremos para nosso interpretador não é exatamente
isso, mas sim thunks que foram memorizados. Quando um thunk é forçado,
nós o transformamos em um thunk avaliado, substituindo a expressão
armazenada por seu valor e mudando a tag thunk
para que ele
possa ser reconhecido como já avaliado.241
(define (evaluated-thunk? obj)
(tagged-list? obj 'evaluated-thunk))
(define (thunk-value evaluated-thunk)
(cadr evaluated-thunk))
(define (force-it obj)
(cond ((thunk? obj)
(let ((result
(actual-value
(thunk-exp obj)
(thunk-env obj))))
(set-car! obj 'evaluated-thunk)
;; replace exp with its value:
(set-car! (cdr obj) result)
;; forget unneeded env:
(set-cdr! (cdr obj) '())
result))
((evaluated-thunk? obj)
(thunk-value obj))
(else obj)))
Observe que o mesmo procedimento delay-it
funciona tanto
com quanto sem memorização.
Exercício 4.27: Suponha que digitamos as seguintes definições no avaliador preguiçoso:
(define count 0) (define (id x) (set! count (+ count 1)) x)
Forneça os valores ausentes na seguinte sequência de interações e explique suas respostas.242
(define w (id (id 10))) ;;; L-Eval input: count ;;; L-Eval value: ⟨response⟩ ;;; L-Eval input: w ;;; L-Eval value: ⟨response⟩ ;;; L-Eval input: count ;;; L-Eval value: ⟨response⟩
Exercício 4.28:
Eval
usaactual-value
em vez deeval
para avaliar o operador antes de passá-lo paraapply
, a fim de forçar o valor do operador. Dê um exemplo que demonstre a necessidade desse forçamento.Exercício 4.29: Exiba um programa que você esperaria que rodasse muito mais lentamente sem memorização do que com memorização. Além disso, considere a seguinte interação, onde o procedimento
id
é definido como em Exercício 4.27 ecount
começa em 0:(define (square x) (* x x)) ;;; L-Eval input: (square (id 10)) ;;; L-Eval value: ⟨response⟩ ;;; L-Eval input: count ;;; L-Eval value: ⟨response⟩
Forneça as respostas tanto quando o avaliador memoriza quanto quando não memoriza.
Exercício 4.30: Cy D. Fect, um programador C reformado, está preocupado que alguns efeitos colaterais possam nunca ocorrer, porque o avaliador preguiçoso não força as expressões em uma sequência. Como o valor de uma expressão em uma sequência, exceto a última, não é usado (a expressão está lá apenas para seu efeito, como atribuir a uma variável ou imprimir), não pode haver uso subsequente desse valor (por exemplo, como argumento para um procedimento primitivo) que o forçará. Cy, portanto, pensa que, ao avaliar sequências, devemos forçar todas as expressões na sequência, exceto a final. Ele propõe modificar
eval-sequence
de 4.1.1 para usaractual-value
em vez deeval
:(define (eval-sequence exps env) (cond ((last-exp? exps) (eval (first-exp exps) env)) (else (actual-value (first-exp exps) env) (eval-sequence (rest-exps exps) env))))
- Ben Bitdiddle acha que Cy está errado. Ele mostra a Cy o procedimento
for-each
descrito em Exercício 2.23, que dá um exemplo importante de uma sequência com efeitos colaterais:(define (for-each proc items) (if (null? items) 'done (begin (proc (car items)) (for-each proc (cdr items)))))
Ele afirma que o avaliador no texto (com o
eval-sequence
original) lida com isso corretamente:;;; L-Eval input: (for-each (lambda (x) (newline) (display x)) (list 57 321 88)) 57 321 88 ;;; L-Eval value: done
Explique por que Ben está certo sobre o comportamento de
for-each
.- Cy concorda que Ben está certo sobre o exemplo
for-each
, mas diz que esse não é o tipo de programa que ele estava pensando quando propôs sua mudança paraeval-sequence
. Ele define os seguintes dois procedimentos no avaliador preguiçoso:(define (p1 x) (set! x (cons x '(2))) x) (define (p2 x) (define (p e) e x) (p (set! x (cons x '(2)))))
Quais são os valores de
(p1 1)
e(p2 1)
com oeval-sequence
original? Quais seriam os valores com a mudança proposta por Cy paraeval-sequence
?- Cy também aponta que mudar
eval-sequence
como ele propõe não afeta o comportamento do exemplo na parte a. Explique por que isso é verdade.- Como você acha que as sequências devem ser tratadas no avaliador preguiçoso? Você gosta da abordagem de Cy, da abordagem no texto ou de alguma outra abordagem?
Exercício 4.31: A abordagem tomada nesta seção é um tanto desagradável, porque faz uma mudança incompatível com o Scheme. Seria mais agradável implementar a avaliação preguiçosa como uma extensão compatível, ou seja, de modo que programas comuns do Scheme funcionem como antes. Podemos fazer isso estendendo a sintaxe de declarações de procedimentos para permitir que o usuário controle se os argumentos devem ser atrasados. Enquanto estamos nisso, também podemos dar ao usuário a escolha entre atrasar com ou sem memorização. Por exemplo, a definição
(define (f a (b lazy) c (d lazy-memo)) …)
definiria
f
como um procedimento de quatro argumentos, onde o primeiro e o terceiro argumentos são avaliados quando o procedimento é chamado, o segundo argumento é atrasado, e o quarto argumento é atrasado e memorizado. Assim, definições comuns de procedimentos produzirão o mesmo comportamento que o Scheme comum, enquanto adicionar a declaraçãolazy-memo
a cada parâmetro de cada procedimento composto produzirá o comportamento do avaliador preguiçoso definido nesta seção. Projete e implemente as mudanças necessárias para produzir tal extensão ao Scheme. Você terá que implementar novos procedimentos de sintaxe para lidar com a nova sintaxe paradefine
. Você também deve arranjar para queeval
ouapply
determinem quando os argumentos devem ser atrasados, e forçar ou atrasar argumentos conforme apropriado, e você deve arranjar para que o forçamento memorize ou não, conforme apropriado.
Em 3.5.1, mostramos como
implementar fluxos como listas atrasadas. Introduzimos formas especiais
delay
e cons-stream
, que permitiram construir
uma “promessa” para calcular o cdr
de um fluxo, sem
realmente cumprir essa promessa até mais tarde. Poderíamos usar essa
técnica geral de introduzir formas especiais sempre que precisamos de
mais controle sobre o processo de avaliação, mas isso é incômodo. Por um
lado, uma forma especial não é um objeto de primeira classe como um
procedimento, então não podemos usá-la junto com procedimentos de ordem
superior.243
Além disso, fomos forçados a criar fluxos como um novo tipo de objeto de
dados semelhante, mas não idêntico a listas, e isso nos obrigou a
reimplementar muitas operações comuns de listas (map
,
append
, e assim por diante) para uso com fluxos.
Com avaliação preguiçosa, fluxos e listas podem ser idênticos, então não
há necessidade de formas especiais ou de operações separadas de listas e
fluxos. Tudo o que precisamos fazer é arranjar as coisas para que
cons
seja não estrito. Uma maneira de conseguir isso é
estender o avaliador preguiçoso para permitir primitivas não estritas, e
implementar cons
como uma delas. Uma maneira mais fácil é
lembrar (2.1.3) que não há
necessidade fundamental de implementar cons
como uma
primitiva. Em vez disso, podemos representar pares como procedimentos:244
(define (cons x y) (lambda (m) (m x y)))
(define (car z) (z (lambda (p q) p)))
(define (cdr z) (z (lambda (p q) q)))
Em termos dessas operações básicas, as definições padrão das operações de listas funcionarão com listas infinitas (fluxos) assim como com listas finitas, e as operações de fluxos podem ser implementadas como operações de listas. Aqui estão alguns exemplos:
(define (list-ref items n)
(if (= n 0)
(car items)
(list-ref (cdr items) (- n 1))))
(define (map proc items)
(if (null? items)
'()
(cons (proc (car items))
(map proc (cdr items)))))
(define (scale-list items factor)
(map (lambda (x) (* x factor))
items))
(define (add-lists list1 list2)
(cond ((null? list1) list2)
((null? list2) list1)
(else (cons (+ (car list1)
(car list2))
(add-lists
(cdr list1)
(cdr list2))))))
(define ones (cons 1 ones))
(define integers
(cons 1 (add-lists ones integers)))
;;; L-Eval input:
(list-ref integers 17)
;;; L-Eval value:
18
Observe que essas listas preguiçosas são ainda mais preguiçosas que os
fluxos de Capítulo 3: O
car
da lista, assim como o cdr
, é atrasado.245
De fato, até mesmo acessar o car
ou cdr
de um
par preguiçoso não precisa forçar o valor de um elemento da lista. O
valor será forçado apenas quando for realmente necessário—por exemplo,
para uso como argumento de uma primitiva, ou para ser impresso como uma
resposta.
Pares preguiçosos também ajudam com o problema que surgiu com fluxos em
3.5.4, onde descobrimos que
formular modelos de fluxos de sistemas com loops pode exigir que
espalhemos nossos programas com operações explícitas de
delay
, além das fornecidas por cons-stream
.
Com avaliação preguiçosa, todos os argumentos para procedimentos são
atrasados uniformemente. Por exemplo, podemos implementar procedimentos
para integrar listas e resolver equações diferenciais como originalmente
pretendíamos em 3.5.4:
(define (integral integrand initial-value dt)
(define int
(cons initial-value
(add-lists (scale-list integrand dt)
int)))
int)
(define (solve f y0 dt)
(define y (integral dy y0 dt))
(define dy (map f y))
y)
;;; L-Eval input:
(list-ref (solve (lambda (x) x) 1 0.001) 1000)
;;; L-Eval value:
2.716924
Exercício 4.32: Dê alguns exemplos que ilustram a diferença entre os fluxos de Capítulo 3 e as listas preguiçosas “mais preguiçosas” descritas nesta seção. Como você pode tirar vantagem dessa preguiça extra?
Exercício 4.33: Ben Bitdiddle testa a implementação de listas preguiçosas dada acima avaliando a expressão
(car '(a b c))
Para sua surpresa, isso produz um erro. Depois de pensar um pouco, ele percebe que as “listas” obtidas ao ler expressões entre aspas são diferentes das listas manipuladas pelas novas definições de
cons
,car
, ecdr
. Modifique o tratamento de expressões entre aspas pelo avaliador para que listas entre aspas digitadas no loop de driver produzam listas preguiçosas verdadeiras.
Exercício 4.34: Modifique o loop de driver para o avaliador para que pares e listas preguiçosas sejam impressos de alguma maneira razoável. (O que você vai fazer sobre listas infinitas?) Você também pode precisar modificar a representação de pares preguiçosos para que o avaliador possa identificá-los a fim de imprimi-los.
235 Snarf: “Pegar, especialmente um documento ou arquivo grande com o propósito de usá-lo com ou sem a permissão do proprietário.” Snarf down: “Pegar, às vezes com a conotação de absorver, processar ou entender.” (Essas definições foram snarfadas de Steele et al. 1983. Veja também Raymond 1993.)
236 A diferença entre a terminologia “preguiçosa” e a terminologia “ordem normal” é um tanto vaga. Geralmente, “preguiçosa” refere-se a mecanismos de avaliadores específicos, enquanto “ordem normal” refere-se à semântica de linguagens, independentemente de qualquer estratégia de avaliação específica. Mas isso não é uma distinção rígida, e as duas terminologias são frequentemente usadas de forma intercambiável.
237 A terminologia “estrita” versus “não estrita” significa essencialmente a mesma coisa que “ordem aplicativa” versus “ordem normal”, exceto que se refere a procedimentos e argumentos individuais em vez de se referir à linguagem como um todo. Em uma conferência sobre linguagens de programação, você pode ouvir alguém dizer: “A linguagem de ordem normal Hassle tem certas primitivas estritas. Outros procedimentos tomam seus argumentos por avaliação preguiçosa.”
238 A palavra thunk foi inventada por um grupo de trabalho informal que estava discutindo a implementação de chamada por nome em Algol 60. Eles observaram que a maior parte da análise de (“pensar sobre”) a expressão poderia ser feita em tempo de compilação; assim, em tempo de execução, a expressão já teria sido “pensada” (Ingerman et al. 1960).
239 Isso
é análogo ao uso de force
nos objetos atrasados que
foram introduzidos em
Capítulo 3 para representar
fluxos. A diferença crítica entre o que estamos fazendo aqui e o que
fizemos em Capítulo 3 é que
estamos construindo atraso e forçamento no avaliador, e assim
tornando isso uniforme e automático em toda a linguagem.
240 Avaliação preguiçosa combinada com memorização é às vezes referida como chamada por necessidade, em contraste com chamada por nome. (Chamada por nome, introduzida em Algol 60, é semelhante à avaliação preguiçosa não memorizada.) Como designers de linguagens, podemos construir nosso avaliador para memorizar, não memorizar, ou deixar isso como uma opção para programadores (Exercício 4.31). Como você pode esperar de Capítulo 3, essas escolhas levantam questões que se tornam tanto sutis quanto confusas na presença de atribuições. (Veja Exercício 4.27 e Exercício 4.29.) Um excelente artigo de Clinger (1982) tenta esclarecer as múltiplas dimensões de confusão que surgem aqui.
241
Observe que também apagamos o env
do thunk uma vez que
o valor da expressão foi calculado. Isso não faz diferença nos
valores retornados pelo interpretador. Isso ajuda a economizar
espaço, no entanto, porque remover a referência do thunk para o
env
uma vez que ele não é mais necessário permite que
essa estrutura seja
coletada como lixo e seu espaço reciclado, como
discutiremos em 5.3.
Da mesma forma, poderíamos ter permitido que ambientes
desnecessários nos objetos atrasados memorizados de
3.5.1 fossem coletados
como lixo, fazendo memo-proc
fazer algo como
(set! proc '())
para descartar o procedimento
proc
(que inclui o ambiente em que o
delay
foi avaliado) após armazenar seu valor.
242 Este exercício demonstra que a interação entre avaliação preguiçosa e efeitos colaterais pode ser muito confusa. Isso é exatamente o que você pode esperar da discussão em Capítulo 3.
243 Esse
é exatamente o problema com o procedimento unless
, como
em Exercício 4.26.
244
Essa é a representação procedural descrita em
Exercício 2.4.
Essencialmente, qualquer representação procedural (por exemplo, uma
implementação de passagem de mensagens) funcionaria tão bem. Observe
que podemos instalar essas definições no avaliador preguiçoso
simplesmente digitando-as no loop de driver. Se tivéssemos
originalmente incluído cons
, car
, e
cdr
como primitivas no ambiente global, elas serão
redefinidas. (Veja também
Exercício 4.33 e
Exercício 4.34.)
245 Isso nos permite criar versões atrasadas de tipos mais gerais de estruturas de listas, não apenas sequências. Hughes 1990 discute algumas aplicações de “árvores preguiçosas.”