Aulas práticas da cadeira de Introdução à Programação Ana Cardoso Cachopo Ano Lectivo 1998/1999
Conteúdo 1 Apresentação 2 2 Os elementos da programação 3 3 Introdução ao UNIX, emacs, mail, news e www 8 4 Recursão 13 5 Processos iterativos vs recursivos Recursão em árvores 19 6 Ordens de crescimento Procedimentos de ordem superior 23 7 Métodos gerais: procedimentos que retornam procedimentos 31 8 Abstracção de dados Pares em Scheme 36 9 Listas em Scheme 46 10 Listas em árvore em Scheme 53 11 Mais exercícios sobre listas 58 12 Quote 64 13 Modularidade, objectos e estado 68
1 Apresentação Sumário Esta aula vai servir para nos conhecermos melhor... Eu Eles IP Nome Entrei para a LEIC no primeiro ano em que ele existiu Escolhi o ramo de Inteligência Artificial Dou aulas desde o 4 o ano no Grupo de Inteligência Artificial do IST. Ver http://www.gia.ist.utl.pt Estão na primeira opção? Se não, quais eram as primeiras? Quais as expectativas em relação ao curso e à cadeira em particular? Vêm de Lisboa? Se não, de onde vêm, onde vivem? Vêm da via geral ou tecnológica? Sabem programar? Em que linguagem? São repetentes? A cadeira está muito diferente, têm que vir às aulas. Ensinamos programação, não Scheme Porquê Scheme (linguagem simples mas poderosa) A cadeira e o curso não são difíceis, mas dão muito trabalho. É necessário trabalhar e experimentar os programas em computador É necessário fazer inscrição nas turmas, de acordo com a turma atribuida pelo SOP É necessário trazer uma fotografia na próxima semana para o verbete Devem pedir área no camoes. Na biblioteca da LEIC, custa 1000$00 e uma fotografia Página da cadeira: http://www.gia.ist.utl.pt/cadeiras/ip Grupo de news ist.cadeiras.leic.ip Endereço de correio electrónico ip@gia.ist.utl.pt
2 Os elementos da programação Sumário O objectivo desta aula é que os alunos aprendam a utilizar o Scheme e a construir procedimentos simples. Resumo
Exercícios Exercício 2.1 (Livro 1.1) Em baixo é apresentada uma sequência de expressões. Diga qual é o resultado impresso pelo interpretador de Scheme quando é avaliada cada uma dessas expressões. Assuma que a sequência é avaliada pela ordem apresentada. 10 (+ 5 3 4) (- 9 1) (/ 6 2) (+ (* 2 4) (- 4 6)) (define a 3) (define b (+ a 1)) (+ a b (* a b)) (= a b) (if (and (> b a) (< b (* a b))) b a) (cond ((= a 4) 6) ((= b 4) (+ 6 7 a)) (else 25)) (+ 2 (if (> b a) b a)) (* (cond ((> a b) a) ((< a b) b) (else -1)) (+ a 1)) > 10 10 > (+ 5 3 4) 12
> (- 9 1) 8 > (/ 6 2) 3 > (+ (* 2 4) (- 4 6)) 6 > (define a 3) > (define b (+ a 1)) > (+ a b (* a b)) 19 > (= a b) #f > (if (and (> b a) (< b (* a b))) b a) 4 > (cond ((= a 4) 6) ((= b 4) (+ 6 7 a)) (else 25)) 16 > (+ 2 (if (> b a) b a)) 6 > (* (cond ((> a b) a) ((< a b) b) (else -1)) (+ a 1)) 16 Exercício 2.2 (Livro 1.2) Traduza a seguinte expressão para a notação prefixa: 5+4+(2 (3 (6+ 4 5 ))) 3(6 2)(2 7) (/ (+ 5 4 (- 2 (- 3 (+ 6 (/ 4 5))))) (* 3 (- 6 2)
(- 2 7))) O que dá -37/150 Se algum dos números fosse um real, o resultado seria -0.24666666666667 Exercício 2.3 Defina um procedimento que calcula o volume de uma esfera v = 4 3 πr3. (define (volume raio) (* (/ 4 3) pi (cubo raio))) (define (cubo x) (* x x x)) (define pi 3.1415927) Exercício 2.4 (Livro 1.3) Defina um procedimento que recebe três números como argumentos e devolve a soma dos quadrados dos dois maiores. (define (square a) (* a a)) (define (sum-of-squares a b) (+ (square a) (square b))) (define (largest a b c) (cond ((and (>= a b) (>= a c)) a) ((and (>= b a) (>= b c)) b) (else c))) Precisa de >= porque, se tiver só > e os argumentos forem 4, 4 e 3, não funciona. (define (largest2 a b c) (if (> a b) (if (> a c) a c) ; nao pode ser o b pq a ja e maior (if (> b c) b c)))
(define (max a b) (if (> a b) a b)) (define (largest3 a b c) (max a (max b c))) (define (second-largest a b c) (if (> a b) (if (> b c) b (if (> a c) c a)) (if (> a c) a (if (> b c) c b)))) (define (sum-of-squares-of-larger-two a b c) (sum-of-squares (largest a b c) (second-largest a b c)))
3 Introdução ao UNIX, emacs, mail, news e www Sumário O objectivo desta aula é que os alunos aprendam os rudimentos sobre a utilização do sistema operativo UNIX, bem como a utilização do editor de texto emacs, que pode também servir para ler mail e news. A primeira metade deve ser passada numa sala normal (com quadro) e deve ser apresentado o modo de interagir com o sistema operativo, o conceito de ficheiro, directoria e os rudimentos de edição de texto. Resumo Características do sistema operativo Unix Sistema multi-utilizador e multi-tarefa Conceitos essenciais: programas, ficheiros, processos Sistema de ficheiros hierárquico (nomes de ficheiros desde a raiz do disco) O utilizador É reconhecido pelo sistema através do seu username e tem a sua password Pertence a um grupo de utilizadores Tem um espaço reservado em disco (quota) Está (?) protegido de outros utilizadores Tem um ambiente personalizado Entrar no sistema Rede das Novas Licenciaturas login: ic-ip Password: Last login:mon Sep 29 18:41:32 from marte.gia.ist.utl.pt Bem-vindo a Rede das Novas Licenciaturas [...] There are No news. 6:52pm up 3 day(s), 23:40, 91 users, load average: 7.80, 6.79, 6.27 ic-ip@camoes[~]#101 Alterar a password ic-ip@camoes[~]#101 passwd passwd: Changing password for ic-ip Enter login password: New password: Password is too short - must be at least 6 characters. New password: Password must contain at least two alphabetic characters and
at least one numeric or special character. New password: Re-enter new password: Sair do sistema ic-ip@camoes[~]#102 logout ic-ip@camoes logged out at Mon Sep 29 18:53:24 WET DST 1997 Sintaxe dos comandos em UNIX comando [-opção*] [argumento*] As opções especificam uma forma particular de executar o comando. Os argumentos especificam informação sobre a qual o comando vai operar. Manipular directorias pwd mkdir nomedir rmdir nomedir cd [nomedir] ls Mostra a directoria corrente Cria uma directoria chamada nomedir Remove a directoria nomedir, se estiver vazia Muda para a directoria nomedir Lista os ficheiros da directoria corrente Manipular ficheiros cp nomefichvelho nomefichnovo Copia nomefichvelho para nomefichnovo cp nomefich* nomedir Copia um conjunto de ficheiros para a directoria nomedir mv nomefichvelho nomefichnovo Muda o nome de nomefichvelho para nomefichnovo mv nomefich* nomedir Move um conjunto de ficheiros para nomedir mv nomedirvelho nomedirnovo Move uma estrutura de directorias para outra directoria rm nomefich* Apaga um conjunto de ficheiros Visualizar o conteúdo de ficheiros cat nomefich* Lê caracteres do dispositivo de entrada standard ou de uma lista de ficheiros e mostra esses caracteres no dispositivo de saída standard more nomefich* Mostra o conteúdo de um conjunto de ficheiros, página a página head -n nomefich* Mostra as primeiras n linhas de um conjunto de ficheiros tail -n nomefich* Mostra as últimas n linhas de um conjunto de ficheiros Imprimir ficheiros
lpr [-P impressora] nomefich* Imprime os ficheiros na impressora lpq [-P impressora] job* userid* Escreve o status dos jobs especificados e/ou dos jobs dos utilizadores especificados que estão na impressora lprm [-P impressora] job* userid* Cancela todos os jobs especificados e/ou os jobs dos utilizadores especificados que estão na impressora Mudar as permissões de um ficheiro ls -l nomefich Para ver quais são as permissões de um ficheiro chmod -R mudanca [, mudanca]* [nomefich]+ Para mudar as permissões de ficheiros, onde mudanca é da forma: SujeitoSinalValor Sujeito u, g, o, a Sinal +, -, = Valor r, w, x Exemplo: chmod u+rwx,go-rwx fich umask [modo] Para mudar as permissões por omissão para os ficheiros e directorias criados, onde modo é um número em octal Para mais informações acerca de comandos e utilitários UNIX man palavra Mostra (com o more) as páginas do manual associadas a palavra man -k palavra Mostra as entradas do manual que contêm palavra Exemplos: man -k mode, man chmod Utilização do Mail mailx endereco Envia uma mensagem para endereco a partir da shell do UNIX. ctrl-d significa endof-file mailx [-f nomefich] [endereco]* Envia o ficheiro nomefich para endereco mailx Serve para ler o correio da área do utilizador.? dá ajuda sobre os vários comandos existentes Caracteres especiais importantes ctrl-d End-Of-File - indica o fim de um ficheiro ctrl-c Interrupt - termina o processo que está a ser executado ctrl-z Suspend - suspende o processo que está a ser executado
Redirecionamento de entrada e saída < nomefich redirecciona o dispositivo de entrada standard para nomefich Permite usar o conteúdo de um ficheiro como entrada para um processo > nomefich redirecciona o dispositivo de saída standard para nomefich Permite guardar o resultado de um processo num ficheiro Internet ist.cadeiras.leic.ip News grupos de discussão de assuntos de interesse para a cadeira, exercicios, avisos importantes, etc (emacs,tin, pine) http://www.gia.ist.utl.pt/cadeiras/ip WWW página da cadeira de IP (lynx, netscape, internet explorer) Manipular processos ps kill numprocesso bg [numprocesso] fg [numprocesso] & Mostra os processos do utilizador Mata processos do utilizador Passa um processo do utilizador para background Passa um processo do utilizador para foreground Executa um processo do utilizador em background No camoes setenv DISPLAY xxx:0 Onde xxx é o endereço IP da máquina na janela inicial (só para terminais gráficos) emacs -font fixed Pode ser necessário se tiverem problemas com as fontes, por não estarem instaladas no sistema O emacs O emacs é um editor de texto. Tem extensões para escrever código em várias linguagens de programação, ler mail e news, entre outras. O emacs tem ajuda e documentação disponível no menu Help, que pode ser utilizada para aprender a trabalhar com ele.
Exercícios Exercício 3.1 Escreva em Scheme os seguintes procedimentos: Um procedimento chamado hipotenusa que, dados os comprimentos dos dois catetos de um triângulo rectângulo, calcula o comprimento da hipotenusa. Dados os comprimentos dos catetos a e b do triângulo, a hipotenusa h é calculada como: h = a 2 + b 2 Um procedimento chamado perimetro que, dados os comprimentos dos dois catetos de um triângulo rectângulo calcula o seu perímetro. O perímetro de uma figura geométrica é a soma dos comprimentos de todos os seus lados. Os procedimentos descritos acima devem ser entregues electronicamente até dia 30 de Outubro. Uma vez que estes trabalhos vão ser executados para a verificação da sua correcção, é imprescindível que os procedimentos tenham exactamente os nomes indicados. Para entregar este trabalho de casa, deve colocar os procedimentos pedidos num único ficheiro de texto, verificando depois que ele pode ser carregado pelo compilador de Scheme que utilizou. Depois disto, deve, no camoes, colocar-se na directoria onde esse ficheiro se encontra e dar o seguinte comando: /users/cadeiras/ic-ip/entrega-tpc Este comando irá pedir o número de aluno e o nome do ficheiro onde se encontra o TPC, enviando de seguida o trabalho por correio electrónico para um endereço de entregas da cadeira de IP. A resposta sobre o sucesso da entrega é de seguida enviada por correio electrónico para o endereço de onde se executou o comando. Trabalhos noutro formato ou que não sejam correctamente enviados por correio electrónico até à data estipulada não serão considerados. Bom trabalho.
4 Recursão Sumário O objectivo desta aula é que os alunos aprendam a construir procedimentos recursivos e que se apercebam de como é que a recursão pode ser utilizada para implementar repetição. Resumo Um procedimento recursivo é um procedimento definido à custa de si próprio, isto é, que se chama a si mesmo.
Exercícios Exercício 4.1 Defina um procedimento que calcula uma potência inteira de x. Note que x n = x x n 1 e x 0 = 1. (define (potencia x n) (if (= n 0) 1 (* x (potencia x (- n 1))))) Exercício 4.2 Considere definidos os seguintes procedimentos: add1, sub1 e zero?, que somam um ao seu argumento, subtraem um ao seu argumento, ou testam se o seu argumento é igual a zero, respectivamente. Com base neles, defina os seguintes procedimentos: 1. O procedimento soma, que recebe dois inteiros superiores ou iguais a zero x e y, e calcula a soma entre eles. 2. O procedimento igual?, que dados dois inteiros superiores ou iguais a zero x e y, retorna verdadeiro se eles forem iguais e falso caso contrário. 3. O procedimento menor?, que dados dois inteiros superiores ou iguais a zero x e y, indica se x é menor que y. 4. O procedimento produto, que calcula o produto entre dois inteiros superiores ou iguais a zero x e y. Para definir este procedimento pode também usar o procedimento soma. 1. (define (soma x y) (if (zero? x) y (add1 (soma (sub1 x) y)))) 2. (define (igual? x y) (cond ((zero? x) (zero? y)) ((zero? y) #f) (else (igual? (sub1 x) (sub1 y))))) 3. (define (menor? x y) (cond ((zero? y) #f) ((zero? x) #t) (else (menor? (sub1 x) (sub1 y))))) 4. (define (produto x y) (if (zero? x) 0 (soma (produto (sub1 x) y) y)))
Exercício 4.3 O número de combinações de m objectos n a n pode ser calculado pela seguinte função: 1 se n = 0, Comb(m, n) = 1 se n = m, Comb(m 1, n) + Comb(m 1, n 1) se m > n, m > 0 e n > 0. Escreva um procedimento que calcula o número de combinações de m objectos n a n. Use a estrutura de blocos para garantir que o seu procedimento recebe sempre os argumentos correctos: inteiros superiores ou iguais a zero e m > n. Sabendo que existem 49 números possíveis para o totoloto e que cada chave tem 6 números diferentes, calcule o número de chaves existentes. Sabendo que cada aposta custa 40$00, quanto dinheiro teria que gastar para ter a certeza que ganhava um primeiro prémio? (define (comb m n) (define (comb-aux m n) (cond ((= n 0) 1) ((= m n) 1) (else (+ (comb-aux (- m 1) n) (comb-aux (- m 1) (- n 1)))))) (if (and (>= m n) (> m 0) (> n 0)) (comb-aux m n) #f)) Para calcular o número de chaves existentes para o totoloto, avaliar (comb 49 6). (Vai demorar algum tempo) O resutado deverá ser 13983816. Para saber quanto dinheiro se tem que gastar, basta multiplicar este valor por 40$00, que é quanto custa cada aposta; (* 13983816 40), o que dá 559352640. Isto significa que, para ter a certeza de ganhar o primeiro premio, teria que gastar mais de 559352 contos!!! Exercício 4.4 (Livro 1.4) Repare que o nosso modelo de avaliação permite a existência de combinações cujos operadores são expressões compostas. Use esta observação para descrever o comportamento do seguinte procedimento: (define (a-plus-abs-b a b) ((if (> b 0) + -) a b)) Este procedimento soma a com o modulo de b, pois o operador utilizado é + ou -, conforme b seja positivo ou negativo, respectivamente. Exercício 4.5 (Livro 1.5) O Zé Só Bites inventou um teste para determinar se o interpretador com que ele se deparou usa avaliação pela ordem aplicativa ou avaliação pela ordem normal. Ele define os dois procedimentos:
(define (p) (p)) (define (test x y) (if (= x 0) 0 y)) E depois avalia a expressão (test 0 (p)) Qual é o comportamento que o Zé vai observar com um interpretador que use a ordem de avaliação aplicativa? Qual é o comportamento que ele vai observar com um interpretador que use a ordem de avaliação normal? Explique a sua resposta. (Assuma que a regra de avaliação para a forma especial if é a mesma, quer o interpretador use a ordem de avaliação aplicativa, quer use a ordem de avaliação normal: primeiro avalia o predicado e o resultado determina se deve avaliar o consequente ou a alternativa.) Num Scheme que use a ordem aplicativa, como é o caso do que estamos a usar, os argumentos são avaliados antes de se aplicar o operador. Assim, para avaliar (test 0 (p)), começa-se por avaliar as sub-expressões test, 0 e (p) e só depois é que se aplica o operador (resultante da avaliação de test). Por isso, e uma vez que p corresponde a um ciclo infinito, a avaliacao de (test 0 (p)) não vai terminar. Num Scheme que use a ordem de avaliação normal ( fully expand and then reduce ), para avaliar (test 0 (p)) começa por expandir para (if (= 0 0) 0 (p)) e só depois é que vai começar a reduzir, aplicando a forma especial if aos seus argumentos. Neste caso, como a condição do if (= 0 0) tem o valor verdadeiro, é a primeira alternativa que é avaliada e é retornado o valor 0. Exercício 4.6 (Livro 1.6) A Alice não percebe porque é que o if precisa de ser uma forma especial. Porque é que não o posso definir como um procedimento normal em termos do cond? pergunta ela. Eva, uma amiga sua, diz que isso é possível e define uma nova versão do if: (define (new-if predicate then-clause else-clause) (cond (predicate then-clause) (else else-clause))) A Eva demonstra o programa à Alice: (new-if (= 2 3) 0 5) 5 (new-if (= 1 1) 0 5) 0 Encantada, a Alice usa o new-if para re-escrever o programa da raíz quadrada: (define (improve guess x) (average guess (/ x guess)))
(define (average x y) (/ (+ x y) 2)) (define (good-enough? guess x) (< (abs (- (square guess) x)) 0.001)) (define (sqrt-iter-new-if guess x) (new-if (good-enough? guess x) guess (sqrt-iter-new-if (improve guess x) x))) (define (sqrt x) (sqrt-iter-new-if 1.0 x)) O que é que acontece quando a Alice tenta calcular raízes quadradas? Explique. Como o new-if foi definido como sendo um procedimento normal, para ser avaliado vai ter que avaliar os seus argumentos. Assim, quando queremos executar o procedimento sqrt-iter, vamos ter que avaliar um new-if, que por sua vez vai ter que avaliar todos os seus argumentos, incluindo uma nova chamada a sqrt-iter, que por sua vez corresponde à avaliacao de um new-if. E assim sucessivamente... Exercício 4.7 (Livro 1.7) O teste good-enough? usado para calcular raízes quadradas não vai funcionar muito bem para raízes de números muito pequenos. Para além disso, nos computadores reais as operações aritméticas são quase sempre efectuadas com precisão limitada. Este facto torna o nosso teste inadequado para números muito grandes. Explique estas afirmações, com exemplos que mostrem como é que o teste falha para números muito pequenos e muito grandes. Uma estratégia alternativa para implementar o good-enough? é observar como é que o guess muda de uma iteração para a próxima e parar quando a mudança é uma fracção pequena do guess. Escreva um procedimento para calcular raízes quadradas que use este tipo de teste de terminação. Ela funciona melhor para números pequenos e grandes? Exercício 4.8 (Livro 1.8) O método de Newton para calcular raízes cúbicas é baseado no facto que se y é uma aproximação para a raíz cúbica de x, então uma melhor aproximação é dada por x y 2 +2y 3 Use esta fórmula para implementar um procedimento que calcula raízes cúbicas análogo ao que calcula raízes quadradas. (define (improve-cube guess x) (/ (+ (/ x (square guess)) (* 2 guess)) 3)) (define (cube x) (* x x x))
(define (cubert-good-enough? guess x) (< (abs (- (cube guess) x)) 0.00001)) (define (cubert-iter guess x) (if (cubert-good-enough? guess x) guess (cubert-iter (improve-cube guess x) x))) (define (cubert x) (cubert-iter 1.0 x)) Exercício 4.9 Escreva um procedimento para calcular o valor de sen(x) utilizando a expansão em série: sen(x) = x 1! x3 3! + x5 5! x7 7! +... O seu procedimento deve ter procedimentos para calcular o factorial e a potência. O seu procedimento deve também receber o número de termos que devem ser considerados. (define (seno x n) (define (seno-iter cont acum) (if (= cont n) acum (seno-iter (add1 cont) (+ acum (* (pot -1 cont) (/ (pot x (add1 (* 2 cont))) (fact (add1 (* 2 cont))))))))) (if (>= n 1) (seno-iter 0.0 0) (display "Tem que considerar pelo menos um termo."))) (define (fact n) (if (= n 0) 1 (* n (fact (sub1 n))))) (define (pot x n) (if (= n 0) 1 (* x (pot x (sub1 n)))))
5 Processos iterativos vs recursivos Recursão em árvores Sumário O objectivo desta aula é que os alunos aprendam a construir e distinguir entre processos iterativos e recursivos. Para além disso, devem também dominar a recursão em ávores. Resumo
Exercícios Exercício 5.1 O número de combinações de m objectos n a n pode ser calculado pela seguinte função: 1 se n = 0, Comb(m, n) = 1 se n = m, Comb(m 1, n) + Comb(m 1, n 1) se m > n, m > 0 e n > 0. 1. Escreva um procedimento que calcula o número de combinações de m objectos n a n. Use a estrutura de blocos para garantir que o seu procedimento recebe sempre os argumentos correctos: inteiros superiores ou iguais a zero e m n. 2. Sabendo que existem 49 números possíveis para o totoloto e que cada chave tem 6 números diferentes, calcule o número de chaves existentes. 3. Sabendo que cada aposta custa 40$00, quanto dinheiro teria que gastar para ter a certeza que ganhava um primeiro prémio? (define (comb m n) (define (comb-aux m n) (cond ((= n 0) 1) ((= m n) 1) (else (+ (comb-aux (- m 1) n) (comb-aux (- m 1) (- n 1)))))) (if (and (>= m n) (> m 0) (> n 0)) (comb-aux m n) #f)) Para calcular o número de chaves existentes para o totoloto, avaliar (comb 49 6). (Vai demorar algum tempo) O resutado deverá ser 13983816. Para saber quanto dinheiro se tem que gastar, basta multiplicar este valor por 40$00, que é quanto custa cada aposta; (* 13983816 40), o que dá 559352640. Isto significa que, para ter a certeza de ganhar o primeiro premio, teria que gastar mais de 559352 contos!!! Exercício 5.2 (Livro 1.9) Cada um dos seguintes procedimentos define um método para adicionar dois inteiros positivos em termos dos procedimentos inc, que incrementa o seu argumento de uma unidade, e dec, que decrementa o seu argumento de uma unidade. (define (+ a b) (define (+ a b) (if (= a 0) (if (= a 0) b b (inc (+ (dec a) b)))) (+ (dec a) (inc b)))) Usando o modelo da substituição, ilustre o processo gerado por cada procedimento ao avaliar (+ 4 5). Estes processos são iterativos ou recursivos?
Exercício 5.3 Defina um procedimento que calcula uma potência inteira de x usando um processo iterativo. Note que x n = x x n 1 e x 0 = 1. Modifique o procedimento anterior para que passe também a conseguir calcular potências em que o expoente é negativo. Note que x n = 1. x n Tal como já vimos, se usássemos um processo recursivo, ficaria: (define (potencia x n) (if (zero? n) 1 (* x (potencia x (sub1 n))))) Alternativamente, podemos usar um processo iterativo, usando um procedimento auxiliar com mais um argumento que vai funcionar como acumulador: (define (potencia x n) (define (pot x n acc) (if (zero? n) acc (pot x (sub1 n) (* acc x)))) (pot x n 1)) Para tratar os expoentes negativos, temos que testar, no procedimento exterior, qual o valor do expoente, e chamar o procedimento auxiliar da maneira adequada: (define (potencia x n) (define (pot x n acc) (if (zero? n) acc (pot x (sub1 n) (* acc x)))) (if (< n 0) (/ 1 (pot x (abs n) 1)) (pot x n 1))) Exercício 5.4 Com base em somas e subtrações, defina o procedimento produto, que calcula o produto entre dois inteiros superiores ou iguais a zero x e y. 1. Usando um processo recursivo 2. Usando um processo iterativo Usando um processo recursivo, fica: (define (produto x y) (if (zero? y) 0 (+ x (produto x (sub1 y)))))
Usando um processo iterativo, fica: (define (produto x y) (define (prod x y acc) (if (zero? y) acc (prod x (sub1 y) (+ acc x)))) (prod x y 0))
6 Ordens de crescimento Procedimentos de ordem superior Sumário Resumo
Exercícios Exercício 6.1 (Livro 1.15) O seno de um ângulo (especificado em radianos) pode ser calculado usando a aproximação sin x x se x for suficientemente pequeno, e a identidade trigonométrica sin x = 3 sin x 3 4 sin3x 3 para reduzir o valor do argumento de sin. (Para este exercício, vamos considerar que um ângulo é suficientemente pequeno se a sua magnitude não for maior que 0.1 radianos.) Estas ideias estão incorporadas nos procedimentos seguintes: (define (cube x) (* x x x)) (define (sine angle) (define (p x) (- (* 3 x) (* 4 (cube x)))) (if (<= (abs angle) 0.1) angle (p (sine (/ angle 3))))) 1. Quantas vezes é que o procedimento p é aplicado quando avaliamos (sine 12.5)? 2. Qual é a ordem de crescimento em espaço e número de passos (em função de a) usados pelo processo gerado pelo procedimento sine quando avaliamos (sine a)? Antes de começar, convém realçar que o procedimento p só serve para não ser necessário calcular 2 vezes o seno da terça parte do argumento. Ou seja, se usássemos o let, o procedimento sine ficaria: (define (sine angle) (if (<= (abs angle) 0.1) angle (let ((x (sine (/ angle 3)))) (- (* 3 x) (* 4 (cube x)))))) 1. Ao avaliar (sine 12.5), temos a seguinte sequência de chamadas: (sine 12.5) (p (sine (/ 12.5 3))) (p (p (sine (/ 4.16 3)))) (p (p (p (sine (/ 1.38 3))))) (p (p (p (p (sine (/ 0.462 3)))))) (p (p (p (p (p (sine (/ 0.15 3)))))) (p (p (p (p (p 0.05 ))))) Isto corresponde a 5 chamadas ao procedimento p.
2. Para sabermos qual a ordem de crescimento do processo, temos que analizar como é que são as suas chamadas: vamos dividindo o argumento do procedimento por três até o resultado ser menor que 0.1. Portanto, estamos interessados em saber quantas vezes é que temos que dividir o argumento por três até que o resultado seja menor que 0.1. a Ou seja, estamos interessados em resolver a seguinte inequação em ordem a n: 3 0,1. n Fazendo esse desenvolvimento, ficamos com a 0,1 3 n 10a 3 n Log 3 10a n Isto significa que este procedimento tem ordem de crescimento em tempo e em espaço de Log 3 10a. Exercício 6.2 (Livro 1.29) A Regra de Simpson é um método para fazer integração numérica. Usando a Regra de Simpson, o integal de uma função f entre a e b pode ser aproximado por h 3 [y 0 + 4y 1 + 2y 2 + 4y 3 + 2y 4 +... + 2y n 2 + 4y n 1 + y n ] onde h = (b a)/n, para algum inteiro par n, e y k = f(a + kh). (Aumentar o n aumenta a precisão da aproximação.) Defina um procedimento que recebe como argumentos f, a, b e n e retorna o valor do integral, calculado usando a Regra de Simpson. Use o seu procedimento para integrar o procedimento cubo entre 0 e 1 (com n = 100 e n = 1000), e compare os resultados com os do procedimento integral apresentado na página 60 do livro. Nota: Deve usar o procedimento sum, definido na página 58 do livro, como (define (sum term a next b) (if (> a b) 0 (+ (term a) (sum term (next a) next b)))) Exercício 6.3 (Livro 1.30) O procedimento sum apresentado acima gera recursão linear. No entanto, pode ser escrito de forma a gerar um processo iterativo. Mostre como é que isso poderia ser feito preenchendo as expressões que faltam na definição que se segue: (define (sum term a next b) (define (iter a result) (if <??> <??> (iter <??> <??>))) (iter <??> <??>))
(define (sum term a next b) (define (iter a result) (if (> a b) result (iter (next a) (+ (term a) result)))) (iter a 0)) Exercício 6.4 (Livro 1.31) 1. O procedimento sum é apenas o mais simples de um vasto número de abstracções semelhantes, que podem ser capturadas como procedimentos de ordem superior. Escreva um procedimento análogo chamado product, que retorna o produto dos valores de uma função para pontos pertencentes a um intervalo. Mostre como definir o factorial em termos do product. Use também o product para calcular aproximações de π usando a fórmula π 4 = 2 4 4 6 6 8... 3 3 5 5 7 7... 2. Se o seu procedimento product gerar um processo recursivo, escreva um que gere um processo iterativo. Se gerar um processo iterativo, escreva um que gere um processo recursivo. 1. (define (product term a next b) (if (> a b) 1 (* (term a) (product term (next a) next b)))) (define (fact n) (product (lambda (x) x) 1 add1 n)) Para calcular as aproximações de π, podemos separar a fórmula anterior em duas, e considerar que π 4 = 2 4 6... 3 5 7... 4 6 8... 3 5 7... Podemos usar o product para calcular o valor de cada uma destas fracções. Depois, basta multiplicar o resultado por 4 para ficarmos com o valor de π: (define (pi n) (define (numerator n) (* 2 n)) (define (denominator n) (add1(* 2 n))) (* 4 (* (/ (product numerator 1.0 add1 (/ n 2)) (product denominator 1.0 add1 (/ n 2))) (/ (product numerator 2.0 add1 (add1 (/ n 2))) (product denominator 1.0 add1 (/ n 2)))))) No entanto, esta versão funciona mal, porque tanto o numerador como o denominador atingem falores muito altos e fazem overflow. Mas também podemos considerar que a série, desde o termo 1 até ao termo n vai progredindo da
seguinte forma: nos termos de índice par, o numerador é o índice mais dois e o denominador o índice mais um; nos termos de índice ímpar, o numerador é o índice mais um e o denominador o índice mais dois. Assim, ficamos com o procedimento seguinte: (define (pi n) (define (term k) (if (even? k) (/ (+ k 2) (+ k 1)) (/ (+ k 1) (+ k 2)))) (* 4 (product term 1 add1 n))) 2. (define (product term a next b) (define (iter a result) (if (> a b) result (iter (next a) (* (term a) result)))) (iter a 1)) Exercício 6.5 (Livro 1.32) 1. Mostre que sum e product são ambos casos especiais de uma noção ainda mais geral chamada accumulate, que combina uma coleção de termos, usando uma função de acumulação geral: (accumulate combiner null-value term a next b) Accumulate recebe como argumentos o mesmo term e as mesmas especificações do intervalo a e b, bem como um procedimento combiner (de 2 argumentos) que especifica como é que o termo corrente deve ser combinado com a acumulação dos termos precedentes e um null-value, que especifica qual o valor a usar quando os termos acabam. Escreva o procedimento accumulate e mostre como é que sum e product podem ser definidos como simples chamadas a accumulate. 2. Se o seu procedimento accumulate gerar um processo recursivo, escreva um que gere um processo iterativo. Se gerar um processo iterativo, escreva um que gere um processo recursivo. 1. (define (accumulate combiner null-value term a next b) (if (> a b) null-value (combiner (term a) (accumulate combiner null-value term (next a) next
b)))) (define (sum term a next b) (accumulate + 0 term a next b)) (define (product term a next b) (accumulate * 1 term a next b)) 2. (define (accumulate combiner null-value term a next b) (define (iter a result) (if (> a b) null-value (iter (next a) (combiner (term a) result)))) (iter a null-value)) Exercício 6.6 (Livro 1.34) Suponha que definimos o procedimento (define (f g) (g 2)) Assim, temos: (f quadrado) 4 (f (lambda (z) (* z (+ z 1)))) 6 O que acontece se (perversamente) pedirmos ao interpretador para avaliar (f f)? Explique. Ao avaliar (f f), temos a seguinte sequência de avaliações: (f f) (f 2) (2 2) Como 2 não é o nome de nenhum procedimento, ao avaliar a última expressão vamos obter um erro, pois não é possível aplicar o procedimento 2 a nenhuns argumentos. Exercício 6.7 1. Escreva um procedimento que faz a composição de procedimentos. Por exemplo, a chamada (compoe f g x) aplica o procedimento f ao resultado de aplicar o procedimento g a x. 2. Com base no procedimento compoe, escreva um procedimento triplo-fact, que calcula o triplo do factorial do seu argumento. Por exemplo, (triplo-fact 3) deverá dar 18. 1. (define (compoe f g x) (f (g x)))
2. (define (triplo-fact x) (compoe (lambda (x) (* x 3)) fact x)) Exercício 6.8 Com base no procedimento sum, escreva um procedimento para calcular o valor de sen(x) utilizando a expansão em série: sen(x) = x 1! x3 3! + x5 5! x7 7! +... Assuma que já existem os procedimentos fact e pot que calculam o factorial e a potência, respectivamente. O seu procedimento deve receber, para além de x, o número n de termos que devem ser considerados. (define (fact n) (if (= n 0) 1 (* n (fact (sub1 n))))) (define (sum term a next b) (if (> a b) 0 (+ (term a) (sum term (next a) next b)))) (define (seno-com-sum x n) (if (< n 1) (display "Tem que considerar pelo menos um termo.") (sum (lambda (n) (* (expt -1 (sub1 n)) (/ (expt x (sub1 (* 2 n))) (fact (sub1 (* 2 n)))))) 1 add1 n))) Outra possibilidade para definir o procedimento seno-com-sum seria definir um procedimento auxiliar chamado, por exemplo, term. Tanto no caso de usarmos a lambda como no caso do procedimento auxiliar, temos que nos lembrar que o sum recebe um procedimento de um argumento. Por isso, mesmo que quiséssemos, por questão de clareza incluir mais argumentos, neste caso isso não seria possível. Com o procedimento auxiliar, seno-com-sum ficaria: (define (seno-com-sum x n) (define (term n) (* (expt -1 (sub1 n)) (/ (expt x (sub1 (* 2 n))) (fact (sub1 (* 2 n)))))) (if (< n 1) (display "Tem que considerar pelo menos um termo.") (sum term 1 add1 n)))
Exercício 6.9 Suponha que tem definido o procedimento comb, que recebe dois inteiros maiores ou iguais a zero, m e n, e calcula o número de combinações de m elementos n a n. Sabendo que existem 49 números possíveis para o totoloto e que cada chave tem 6 números diferentes, escreva um procedimento que escreve quanto dinheiro tem que ser gasto para ter a certeza de ganhar o primeiro prémio do totoloto, se as apostas custassem 40$00, 50$00 ou 60$00. (define (comb m n) (define (comb-aux m n) (cond ((= n 0) 1) ((= m n) 1) (else (+ (comb-aux (- m 1) n) (comb-aux (- m 1) (- n 1)))))) (if (and (>= m n) (> m 0) (> n 0)) (comb-aux m n) #f)) (define (precostotoloto) (let ((x (comb 49 6))) (display "Se as apostas custarem 40$00, tem que gastar ") (display (* 40 x)) (newline) (display "Se as apostas custarem 50$00, tem que gastar ") (display (* 50 x)) (newline) (display "Se as apostas custarem 60$00, tem que gastar ") (display (* 60 x)))) (precostotoloto)
7 Métodos gerais: procedimentos que retornam procedimentos Sumário Resumo
Exercícios Exercício 7.1 Considere definido o procedimento sum: (define (sum term a next b) (if (> a b) 0 (+ (term a) (sum term (next a) next b)))) Diga o que fazem as seguintes chamadas a esse procedimento: 1. (sum (lambda (x) x) 4 add1 500) 2. (sum (lambda (x) (sqrt x)) 5 (lambda (x) (+ x 5)) 500) 3. (sum (lambda (x) (sum (lambda (x) x) 1 add1 x)) 1 add1 5) 1. Soma os números entre 4 e 500. 2. Soma as raízes quadradas dos múltiplos de 5 entre 5 e 500. 3. Para cada um dos números entre 1 e 5, soma os números entre 1 e esse número: (1+(1+2)+(1+ 2 + 3) + (1 + 2 + 3 + 4) + (1 + 2 + 3 + 4 + 5)) Exercício 7.2 Considere a seguinte expressão matemática: 3x! + 4(x!) 3 1. Escreva um procedimento calc-expr que calcule o seu valor. 2. Usando a estrutura de blocos, garanta que o seu procedimento recebe sempre um argumento correcto (x 0). 3. Comente as afirmações seguintes: (a) Neste caso, não havia necessidade de utilizar a estrutura de blocos. (b) Neste caso, convém utilizar a forma especial let. (c) Neste caso, não devo definir o procedimento cubo. (d) O procedimento cubo, se for definido, deve ser definido dentro do procedimento calc-expr. 1. (define (fact x) (if (zero? x) 1 (* x (fact (sub1 x))))) (define (calc-expr x) (let ((x-fact (fact x))) (+ (* 3 x-fact) (* 4 (expt x-fact 3)))))
2. (define (calc-expr x) (define (calc-expr-aux x) (let ((x-fact (fact x))) (+ (* 3 x-fact) (* 4 (expt x-fact 3))))) (if (>= x 0) (calc-expr-aux x) (display "ERRO: o arg deve ser um inteiro positivo."))) 3. (a) É verdade que não havia necessidade de utilizar a estrutura de blocos, pois o teste se o argumento é válido só seria feito uma vez, mesmo que fosse feito no procedimento exterior. Faria sentido usar a estrutura de blocos se o procedimento fosse recursivo, para evitar a repetição do teste a cada iteração. (b) É verdade que convém utilizar a forma especial let, pois assim podemos guardar o valor de x!, que de outra forma teria que ser calculado duas vezes. (c) Esta afirmação não é verdadeira, pois aquilo que o procedimento cubo faz é uma operação bem definida e que pode ser útil noutros procedimentos. Assim, faz sentido definir este procedimento. (d) Tal como já dissémos, o procedimento cubo pode ser útil noutros procedimentos. Por isso, deve ser definido fora do procedimento calc-expr. Exercício 7.3 (Livro 1.41) Defina um procedimento que recebe como argumento um procedimento de um argumento e retorna um procedimento que aplica duas vezes o procedimento original. Por exemplo, se add1 for um procedimento que adiciona 1 ao seu argumento, então (double add1) deverá ser um procedimento que adiciona dois: ((double add1) 5) 7 (((double double) add1) 5) 9 Qual é o valor retornado por (((double (double double)) add1) 5)? Porquê? (define (double proc) (lambda (x) (proc (proc x)))) >((double add1) 5) 7 >(((double double) add1) 5) 9 >(((double (double double)) add1) 5) 21 Para explicarmos estes resultados, podemos analizar os vários passos pelos quais passa a avaliação de cada uma destas expressões: ((double add1) 5) ((lambda (x) (add1 (add1 x))) 5)
(add1 (add1 5)) (add1 6) 7 Para a segunda expressão, fica: (((double double) add1) 5) (((lambda (x) (double (double x))) add1) 5) ((double (double add1)) 5) ((double (lambda (x) (add1 (add1 x)))) 5) ((lambda (x2) ((lambda (x) (add1 (add1 x))) ((lambda (x) (add1 (add1 x))) x2))) 5) ((lambda (x) (add1 (add1 x))) (lambda (x) (add1 (add1 x)) 5)) ((lambda (x) (add1 (add1 x))) (add1 (add1 5))) ((lambda (x) (add1 (add1 x))) (add1 6)) ((lambda (x) (add1 (add1 x))) 7) (add1 (add1 7)) (add1 8) 9 Para a última expressão, fica: (((double (double double)) add1) 5) 21 Exercício 7.4 1. (Livro 1.42) Sejam f e g duas funções de um argumento. A composição f depois de g é definida como sendo a função x f(g(x)). Defina um procedimento compose que implementa a composição. Por exemplo, se inc for um procedimento que adiciona 1 ao seu argumento, ((compose square inc) 6) 49 2. Compare o procedimento compose com o procedimento compoe da aula passada. Porque é que eles são diferentes? 1. (define (compose f g) (lambda (x) (f (g x)))) 2. O procedimento compoe da aula passada recebia os procedimentos a compor e também os argumentos desses procedimentos. Retornava o resultado da aplicação dos procedimentos ao valor recebido. O procedimento compose recebe apenas os procedimentos e retorna outro procedimento. O resultado do procedimento compose pode ser usado como primeiro elemento de uma expressão em Scheme e o resultado do procedimento compoe não.
Exercício 7.5 (Livro 1.43) Se f for uma função numérica e n um inteiro positivo, então podemos formar a n-ésima repetição da aplicação de f, que é definida como a função cujo valor em x é f(f(...(f(x))...)). Por exemplo, se f for a função x x+1, então a n-ésima repetição da aplicação de f é a função x x + n. Se f for a operação de elevar um número ao quadrado, então a n-ésima repetição da aplicação de f é a função que eleva o seu argumento a 2 n. Escreva um procedimento chamado repeated, que recebe como argumentos um procedimento que calcula f e um inteiro positivo n e retorna um procedimento que calcula a n-ésima repetição da aplicação de f. O seu procedimento deverá poder ser usado da seguinte forma: ((repeated square 2) 5) 625 Sugestão: Pode ser conveniente usar o compose do execício anterior. (define (repeated f n) (if (= n 1) f (compose f (repeated f (sub1 n))))) Exercício 7.6 (Livro 1.44) A ideia de alisar uma função é um conceito importante em processamento de sinal. Se f é uma função e dx é um número pequeno, então a versão alisada de f é a função cujo valor no ponto x é a média de f(x dx), f(x) e f(x + dx). Escreva um procedimento smooth que recebe como argumento um procedimento que calcula f e retorna um procedimento que calcula f alisada. Algumas vezes, pode ser útil alisar repetidamente uma função (isto é, alisar a função alisada e assim sucessivamente) para obter a função alisada n-vezes. Mostre como é que poderia gerar a função alisada n-vezes de qualquer função usando smooth e repeated do exercício anterior. (define (smooth f) (let ((dx 0.00001)) (lambda (x) (/ (+ (f (- x dx)) (f x) (f (+ x dx))) 3)))) (define (n-fold-smooth f n) ((repeated smooth n) f))
8 Abstracção de dados Pares em Scheme Sumário Resumo
Exercícios Exercício 8.1 Diga qual o resultado de avaliar cada uma das seguintes expressões. Se alguma delas der origem a um erro, explique porquê. (cons 2 3) (car (cons 2 3)) (cddr (cons 2 3)) (cdr (cons "ola" "bom dia")) (list (cons 1 3) 4) (cdr (list 2 3)) (cdr (cons 2 3)) () (list ()) (cons (integer? (sqrt 4)) (integer? 2.0)) (pair? (cons 2 3)) (list? (cons 2 3)) (list? (list 2 3)) (pair? (list 2 3 4)) (cadr (list 2 3 4)) >(cons 2 3) (2. 3) >(car (cons 2 3)) 2 >(cddr (cons 2 3)) cddr: expects argument of type <cddrable value>; given (2. 3) >(cdr (cons "ola" "bom dia")) "bom dia"
>(list (cons 1 3) 4) ((1. 3) 4) >(cdr (list 2 3)) (3) >(cdr (cons 2 3)) 3 >() () >(list ()) (()) >(cons (integer? (sqrt 4)) (integer? 2.0)) (#t. #t) >(pair? (cons 2 3)) #t >(list? (cons 2 3)) #f >(list? (list 2 3)) #t >(pair? (list 2 3 4)) #t >(cadr (list 2 3 4)) 3 Exercício 8.2 Represente as seguintes listas e pares usando a notação de caixas e ponteiros: 1. (1) 2. (1. 2) 3. (1 2) 4. (1 (2 (3 (4 5)))) 5. (1 (2. 3) 4) 6. (((2 (6 (7. 8) 3)) 1)) 7. (1 (((2))))
Exercício 8.3 Considere as seguintes definições para o procedimento make-rat, que, dados dois inteiros, retorna o racional em que o primeiro é o numerador e o segundo é o denominador: (define (make-rat n d) (cons n d)) (define (make-rat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g)))) Em relação à primeira definição, a segunda tem a vantagem de reduzir o numerador e o denominador aos números mais pequenos possível. (Livro 2.1) Defina uma versão melhor de make-rat que considere argumentos positivos e negativos. make-rat deve normalizar o sinal, de forma a que, se o número racional for positivo, tanto o numerador como o denominador são positivos; e se o número racional for negativo, só o numerador é que é negativo. (define (gcd a b) (if (= b 0) a (gcd b (remainder a b)))) (define (make-rat n d) (let ((g (gcd (abs n) (abs d)))) (if (>= (* n d) 0) (cons (/ (abs n) (abs g)) (/ (abs d) (abs g))) (cons (- (/ (abs n) g)) (/ (abs d) g))))) Exercício 8.4 (Livro 2.2) Considere o problema de representar segmentos de recta num plano. Cada segmento é representado por um par de pontos: um ponto inicial e um ponto final. Defina um construtor make-segment e os selectores start-segment e end-segment que definem a representação dos segmentos em termos de pontos. Adicionalmente, um ponto pode ser representado como um par de números: a coordenada x e a coordenada y. Especifique o construtor make-point e os selectores x-point e y-point que definem esta representação. Usando os seus selectores e construtores, defina um procedimento midpoint-segment que recebe um segmento de recta como argumento e retorna o seu ponto médio (o ponto cujas coordenadas são a média das coordenadas dos pontos que definem o segmento). (define (make-segment ip fp) (cons ip fp))
(define (start-segment seg) (car seg)) (define (end-segment seg) (cdr seg)) ;==== (define (make-point x y) (cons x y)) (define (x-point p) (car p)) (define (y-point p) (cdr p)) ;==== (define (midpoint-segment seg) (let ((x1 (x-point (start-segment seg))) (y1 (y-point (start-segment seg))) (x2 (x-point (end-segment seg))) (y2 (y-point (end-segment seg)))) (make-point (/ (+ x1 x2) 2) (/ (+ y1 y2) 2)))) Exercício 8.5 (Livro 2.3) Implemente uma representação de rectângulos num plano. (Pode ser útil usar os resultados do exercício anterior.) Com base nos seus construtores e selectores, crie procedimentos que calculem o perímetro e a área de um dado rectângulo. Implemente uma representação diferente para os rectângulos. Consegue conceber o seu sistema com as barreiras de abstração adequadas, de forma a que os procedimentos que calculam a área e o perímetro funcionem com qualquer das representações? Antes de mais nada, devemos definir a interface dos procedimentos que vão manipular rectângulos. Os rectângulos vão ser sempre definidos à custa de quatro valores: as coordenadas y do seu lado superior (top) e do seu lado inferior (bottom) e as coordenadas x do seu lado esquerdo (left) e do seu lado direito (right). Os selectores vão dar precisamente cada um destes valores, independentemente da representação interna que for usada para os rectângulos. ; Numa implementacao, vamos definir os rectangulos como um par de pontos, ; o TopLeft e o BottomRight (define (make-rect top left right bottom) (cons (make-point left top) ; a primeira coordenada dos pontos e x (make-point right bottom))) Um melhoramento seria escrever o procedimento make-rect de forma a que este verificasse se os valoes recebidos fazem sentido (por exemplo, left < right).
(define (top-left-rect rect) (car rect)) (define (bottom-right-rect rect) (cdr rect)) (define (top rect) (y-point (top-left-rect rect))) (define (left rect) (x-point (top-left-rect rect))) (define (bottom rect) (y-point (bottom-right-rect rect))) (define (right rect) (x-point (bottom-right-rect rect))) (define (width rect) (- (right rect) (left rect))) (define (height rect) (- (top rect) (bottom rect))) (define (perimeter rect) (* 2 (+ (width rect) (height rect)))) (define (area rect) (* (width rect) (height rect))) Exercício 8.6 Com base nas respostas aos exercícios anteriores, escreva um procedimento dentro-rectangulo, que recebe um rectângulo e um ponto e retorna #t se o ponto estiver dentro do rectângulo (incluindo a fronteira) e #f se estiver fora do rectângulo. (define (dentro-rectangulo pt rect) (and (>= (top rect) (y-point pt) (bottom rect)) (>= (right rect) (x-point pt) (left rect)))) Exemplos: >(dentro-rectangulo (make-point 2 3) (make-rect 5 1 5 1)) #t
>(dentro-rectangulo (make-point 10 10) (make-rect 5 1 5 1)) #f >(dentro-rectangulo (make-point 1 5) (make-rect 5 1 5 1)) #t Exercício 8.7 Defina os seguintes procedimentos que operam sobre listas. Os seus procedimentos devem dar erro (usando o error) quando isso se justificar. Quando for possível, escreva dois procedimentos, um que gera um processo recursivo e outro que gera um processo iterativo. 1. O procedimento primeiro-par que recebe uma lista e retorna um par com os dois primeiros elementos da lista. 2. O procedimento maior-elemento que recebe uma lista de inteiros e retorna o maior elemento dessa lista. 3. O procedimento soma-elementos que recebe uma lista e retorna a soma de todos os elementos dessa lista. 4. O procedimento aplica-op-com-passo que recebe uma lista e dois procedimentos e retorna outra lista, cujos elementos são obtidos aplicando o primeiro procedimento ao primeiro elemento da lista inicial, e das sucessivas listas que são obtidas por aplicação do segundo procedimento (até que a lista fique vazia). Por exemplo, (aplica-op-com-passo (list 1 2 3 4 5) (lambda (x) (* 2 x)) cddr) deverá retornar (2 6 10). 5. O procedimento imprime-lista-de-pares que recebe uma lista de pares e imprime os pares, um por linha. O seu procedimento deve assinalar quando é que chega ao fim da lista. Por exemplo, (imprime-lista-de-pares (list (cons "Luisa" 12345678) (cons "Jorge" 23456789) (cons "Maria" 34567890) (cons "Rui" 45678901))) Deverá imprimir Luisa -> 12345678 Jorge -> 23456789 Maria -> 34567890 Rui -> 45678901 Fim da lista
1. (define (primeiro-par l) (if (and (pair? l) (pair? (cdr l))) (cons (car l) (cadr l)) (error "primeiro-par: espera uma lista com pelo menos 2 elementos, recebeu" l))) Exemplos: >(primeiro-par ()) primeiro-par: espera uma lista com pelo menos 2 elementos, recebeu () >(primeiro-par (list 1)) primeiro-par: espera uma lista com pelo menos 2 elementos, recebeu (1) >(primeiro-par (list 1 2 3 4))) (1. 2) >(primeiro-par (list 1 (cons 2 3) 4)) (1 2. 3) Neste caso, não vamos gerar nenhum processo recursivo, pois isso não tem interesse. 2. (define (maior-elemento l) (define (m-e-aux l) (if (null? (cdr l)) (car l) (max (car l) (m-e-aux (cdr l))))) (if (or (null? l) (not (pair? l))) (error "maior-elemento: espera uma lista com pelo menos 1 elemento, recebeu" l) (m-e-aux l))) Para o procedimento maior-elemento gerar um processo iterativo, usamos o seguinte: (define (maior-elemento l) (define (m-e-aux l maximo) (if (null? l) maximo (m-e-aux (cdr l) (max (car l) maximo)))) (if (or (null? l) (not (pair? l))) (error "maior-elemento: espera uma lista com pelo menos 1 elemento, recebeu" l) (m-e-aux (cdr l) (car l)))) Exemplos: >(maior-elemento ()) maior-elemento: espera uma lista com pelo menos 1 elemento, recebeu () >(maior-elemento (list 1 5 2 10 4)) 10 3. (define (soma-elementos l) (if (null? l) 0 (+ (car l)
(soma-elementos (cdr l))))) Para gerar um processo iterativo, precisamos de um argumento auxiliar e por causa disso de um procedimento auxiliar (define (soma-elementos l) (define (s-e-aux l acc) (if (null? l) acc (s-e-aux (cdr l) (+ (car l) acc)))) (s-e-aux l 0)) Exemplos: >(soma-elementos (list 1 5 2 10 4)) 22 4. (define (aplica-op-com-passo l p1 p2) (if (null? l) () (cons (p1 (car l)) (aplica-op-com-passo (p2 l) p1 p2)))) Exemplos: (aplica-op-com-passo (list 1 2 3 4 5) (lambda (x) (* 2 x)) cddr) cddr: expects argument of type <cddrable value>; given (5) O problema foi que no último passo da recursão chamámos p2 com (5), o que originou este erro. No entanto, no procedimento aplica-op-com-passo não podemos resolver este problema, pois não sabemos qual vai ser o procedimento passado no terceiro argumento. Assim, quando chamarmos aplica-op-com-passo é que vamos ter o cuidado de passar uma lista que não vá originar um erro. Neste exemplo, a lista deveria ter mais um elemento, mas que não vai ser usado para nada. Por exemplo: (aplica-op-com-passo (list 1 2 3 4 5 ()) (lambda (x) (* 2 x)) cddr) (2 6 10) Outra alternativa seria que o segundo procedimento testasse se pode ser aplicado, de modo a não dar erro. Por exemplo: (aplica-op-com-passo (list 1 2 3 4 5) (lambda (x) (* 2 x)) (lambda (x) (if (>= (length x) 2) (cddr x) ()))) Para gerar um processo iterativo, precisamos de um argumento auxiliar e por causa disso de um procedimento auxiliar. Uma possibilidade é: (define (aplica-op-com-passo l p1 p2) (define (a-o-c-p-aux l res) (if (null? l) res (a-o-c-p-aux (p2 l) (cons (p1 (car l)) res)))) (a-o-c-p-aux l ()))
Exemplos: (aplica-op-com-passo (list 1 2 3 4 5 ()) (lambda (x) (* 2 x)) cddr) (10 6 2) O problema é que assim ficamos com a lista de resultados invertida! Não é trivial criar um processo iterativo neste caso, pois não sabemos acrescentar elementos no fim de listas. Para isso deveríamos definir um outro procedimento, mas ficaria muito pouco eficiente porque tinha que percorrer sempre toda a lista, e assim iríamos perder as vantagens dos processos iterativos. 5. (define (imprime-lista-de-pares lista) (cond ((null? lista) (display "Fim da lista")) ((not (pair? (car lista))) (error "imprime-lista-de-pares: espera uma lista de pares, recebeu" lista)) (else (display (caar lista)) (display " -> ") (display (cdar lista)) (newline) (imprime-lista-de-pares (cdr lista))))) Exemplos: >(imprime-lista-de-pares (list (cons "Luisa" 12345678) (cons "Jorge" 23456789) (cons "Maria" 34567890) (cons "Rui" 45678901))) Luisa -> 12345678 Jorge -> 23456789 Maria -> 34567890 Rui -> 45678901 Fim da lista >(imprime-lista-de-pares (list (cons "Luisa" 12345678) (cons "Jorge" 23456789) (cons "Maria" 34567890) "Rui")) Luisa -> 12345678 Jorge -> 23456789 Maria -> 34567890 imprime-lista-de-pares: espera uma lista de pares, recebeu ("Rui") Este exemplo gera um processo iterativo e neste caso não faz sentido criar um recursivo.
9 Listas em Scheme Sumário Resumo
Exercícios Exercício 9.1 (Livro 2.17) Defina um procedimento last-pair, que retorna a lista que contém apenas o último elemento de uma dada lista não vazia: (define (last-pair l) (if (null? (cdr l)) l (last-pair (cdr l)))) Exemplos: >(last-pair (list 23 72 149 34)) (34) >(last-pair ()) cdr: expects argument of type <pair>; given () >(last-pair (list ())) (()) Exercício 9.2 (Livro 2.18) Defina um procedimento reverse, que recebe como argumento uma lista e retorna uma lista com os mesmos elementos, mas pela ordem inversa: (reverse (list 1 4 9 16 25)) (25 16 9 4 1) (define (reverse l) (if (null? l) () (cons (car l) (reverse (cdr l))))) Exemplos: >(reverse (list 1 4 9 16 25)) (1 4 9 16 25) Mas este procedimento não inverte a lista! Precisamos de ir tirando os elementos do início da lista e ir colocando numa lista auxiliar. Para isso, definimos um procedimento auxiliar: (define (reverse l) (define (r-aux l res) (if (null? l) res (r-aux (cdr l) (cons (car l) res)))) (r-aux l ())) Exemplos:
>(reverse (list 1 4 9 16 25)) (25 16 9 4 1) Exercício 9.3 Defina um procedimento map, que recebe como argumentos um procedimento de um argumento e uma lista, e retorna a lista dos resultados produzidos aplicando o procedimento a cada elemento da lista. (map abs (list -10 2.5-11.6 17)) (10 2.5 11.6 17) (define (map proc l) (if (null? l) () (cons (proc (car l)) (map proc (cdr l))))) Exemplos: >(map abs (list -10 2.5-11.6 17)) (10 2.5 11.6 17) >(map (lambda (x) (* x 5)) (list 1 2 3 4 5)) (5 10 15 20 25) Exercício 9.4 (Livro 2.21) O procedimento square-list recebe como argumento uma lista de números e retorna uma lista com os quadrados desses números. (square-list (list 1 2 3 4)) (1 4 9 16) Seguem-se duas definições diferentes para o procedimento square-list. Complete ambas as definições, preenchendo as expressões que faltam: (define (square-list items) (if (null? items) () (cons <??> <??>))) (define (square-list items) (map <??> <??>)) (define (square-list items) (if (null? items) () (cons ((lambda (x) (* x x)) (car items)) (square-list (cdr items))))) (define (square-list items) (map (lambda (x) (* x x)) items))
Exemplos: >(square-list (list 1 2 3 4)) (1 4 9 16) Exercício 9.5 (Livro 2.22) O Luís tenta re-escrever o primeiro procedimento square-list do exercício anterior de modo a que ele passe a gerar um processo iterativo: (define (square-list items) (define (iter things answer) (if (null? things) answer (iter (cdr things) (cons ((lambda (x) (* x x)) (car items)) answer)))) (iter items ())) Infelizmente, definir o procedimento square-list desta maneira produz a lista de resposta pela ordem inversa à desejada. Porquê? O Luís tenta então corrigir este erro trocando os argumentos do cons: (define (square-list items) (define (iter things answer) (if (null? things) answer (iter (cdr things) (cons answer ((lambda (x) (* x x)) (car items)))))) (iter items ())) Isto também não funciona. Explique porquê. Exemplos: Exercício 9.6 (Livro 2.23) O procedimento for-each é semelhante ao map. Recebe como argumentos um procedimento e uma lista de elementos. No entanto, em vez de formar uma lista com os resultados, for-each apenas aplica o procedimento a cada um dos elementos de cada vez, da esquerda para a direita. Os valores retornados pela aplicação do procedimento aos elementos não são usados for-each é usado com procedimentos que executam uma acção, tal como imprimir. Por exemplo:
(for-each (lambda (x) (newline) (display x)) (list 57 321 28)) 57 321 28 O valor retornado pela chamada a for-each (não ilustrado acima) pode ser qualquer coisa, como verdadeiro. Apresente uma implementação para o procedimento for-each. (define (for-each p l) (cond ((null? l) #t) (else (p (car l)) (for-each p (cdr l))))) Exemplos: >(for-each (lambda (x) (newline) (display x)) (list 57 321 28)) 57 321 28 #t Exercício 9.7 Implemente o procedimento imprime-lista-de-pares da aula passada usando o procedimento for-each. Lembre-se que o procedimento recebe uma lista de pares e imprime os pares, um por linha, e deve assinalar quando é que chega ao fim da lista. Por exemplo, (imprime-lista-de-pares (list (cons "Luisa" 12345678) (cons "Jorge" 23456789) (cons "Maria" 34567890) (cons "Rui" 45678901))) Deverá imprimir Luisa -> 12345678 Jorge -> 23456789 Maria -> 34567890 Rui -> 45678901 Fim da lista (define (imprime-elemento el) (cond ((not (pair? el)) (error "imprime-elemento: espera um par, recebeu" el)) (else (display (car el)) (display " -> ") (display (cdr el)) (newline)))) (define (imprime-lista-de-pares lista) (for-each imprime-elemento lista))
Nesta definição, o fim da lista é assinalado pelo valor retornado pelo procedimento for-each. Exemplos: >(imprime-lista-de-pares (list (cons "Luisa" 12345678) (cons "Jorge" 23456789) (cons "Maria" 34567890) (cons "Rui" 45678901))) Luisa -> 12345678 Jorge -> 23456789 Maria -> 34567890 Rui -> 45678901 #t >(imprime-lista-de-pares (list (cons "Luisa" 12345678) (cons "Jorge" 23456789) (cons "Maria" 34567890) "Rui")) Luisa -> 12345678 Jorge -> 23456789 Maria -> 34567890 imprime-elemento: espera um par, recebeu "Rui" Exercício 9.8 (Livro 2.24) Suponha que avaliamos a expressão (list 1 (list 2 (list 3 4))). Mostre o resultado impresso pelo interpretador, a estrutura de caixas e ponteiros correspondente. >(list 1 (list 2 (list 3 4))) (1 (2 (3 4))) Exercício 9.9 (Livro 2.25) Apresente combinações de cars e cdrs que seleccionem o 7 de cada uma das listas seguintes: (1 3 (5 7) 9) ((7)) (1 (2 (3 (4 (5 (6 7)))))) >(car (cdaddr (1 3 (5 7) 9))) 7 >(caar ((7))) 7 >(cadadr (cadadr (cadadr (1 (2 (3 (4 (5 (6 7))))))))) 7 Exercício 9.10 (Livro 2.26) Suponha que definimos x e y como sendo duas listas:
(define x (list 1 2 3)) (define y (list 4 5 6)) Qual é o resultado impresso pelo interpretador como resposta a cada uma das seguintes expressões? (append x y) (cons x y) (list x y) >(append x y) (1 2 3 4 5 6) >(cons x y) ((1 2 3) 4 5 6) >(list x y) ((1 2 3) (4 5 6))
10 Listas em árvore em Scheme Sumário Resumo
Exercícios Exercício 10.1 (Livro 2.27) Modifique o procedimento reverse (do Livro 2.18) para produzir um procedimento deep-reverse que recebe uma lista como argumento e retorna a lista com os seus elementos invertidos e com todas as suas sublistas tambem invertidas. Por exemplo, (define x (list (list 1 2) (list 3 4))) x ((1 2) (3 4)) (reverse x) ((3 4) (1 2)) (deep-reverse x) ((4 3) (2 1)) Lembre-se que o procedimento reverse recebe como argumento uma lista e retorna uma lista com os mesmos elementos, mas pela ordem inversa: (define (reverse l) (define (r-aux l res) (if (null? l) res (r-aux (cdr l) (cons (car l) res)))) (r-aux l ())) (define (deep-reverse l) (cond ((null? l) ()) ((pair? (car l)) (append (deep-reverse (cdr l)) (list (deep-reverse (car l))))) (else (append (deep-reverse (cdr l)) (list (car l)))))) Exemplos: >(define x (list (list 1 2) (list 3 4))) >(deep-reverse x) ((4 3) (2 1)) >(deep-reverse (list x x)) (((4 3) (2 1)) ((4 3) (2 1))) Exercício 10.2 (Livro 2.28) Escreva um procedimento fringe que recebe como argumento uma árvore (representada como uma lista de listas) e retorna uma lista cujos elementos são todas as folhas da árvore da esquerda para a direita. Por exemplo,
(define x (list (list 1 2) (list 3 4))) (fringe x) (1 2 3 4) (fringe (list x x)) (1 2 3 4 1 2 3 4) (define (fringe l) (cond ((null? l) ()) ((pair? (car l)) (append (fringe (car l)) (fringe (cdr l)))) (else (cons (car l) (fringe (cdr l)))))) Exemplos: >(define x (list (list 1 2) (list 3 4))) >(fringe x) (1 2 3 4) >(fringe (list x x)) (1 2 3 4 1 2 3 4) >(fringe (list 1 2 (cons 3 4) (list 5 6))) car: expects argument of type <pair>; given 4 Exercício 10.3 (Livro 2.30) Defina o procedimento square-tree análogo ao square-list (do Livro 2.18). O procedimento square-tree deve-se comportar da seguinte forma: (square-tree (list 1 (list 2 (list 3 4) 5) (list 6 7))) (1 (4 (9 16) 25) (36 49)) Deve definir este procedimento directamente (isto é, sem usar procedimentos de ordem superior) e também usando o procedimento map. (define (square-tree l) (cond ((null? l) ()) ((pair? (car l)) (cons (square-tree (car l)) (square-tree (cdr l)))) (else (cons (square (car l)) (square-tree (cdr l)))))) (define (square x) (* x x))
Exemplos: >(square-tree (list 1 (list 2 (list 3 4) 5) (list 6 7))) (1 (4 (9 16) 25) (36 49)) Usando o procedimento map, ficaria: (define (square-tree-com-map tree) (map (lambda (x) (if (pair? x) (square-tree-com-map x) (square x))) tree)) Exemplos: >(square-tree-com-map (list 1 (list 2 (list 3 4) 5) (list 6 7))) (1 (4 (9 16) 25) (36 49)) Exercício 10.4 (Livro 2.31) Abstraia a sua resposta ao exercício anterior para produzir um procedimento tree-map, com a propriedade que square-tree poderia ser definido como: (define (square-tree tree) (tree-map square tree)) (define (tree-map proc tree) (cond ((null? tree) ()) ((pair? (car tree)) (cons (tree-map proc (car tree)) (tree-map proc (cdr tree)))) (else (cons (proc (car tree)) (tree-map proc (cdr tree)))))) (define (square-tree tree) (tree-map square tree)) Exemplos: >(square-tree (list 1 (list 2 (list 3 4) 5) (list 6 7))) (1 (4 (9 16) 25) (36 49))
Exercício 10.5 (Livro 2.32) Podemos representar um conjunto como uma lista de elementos distintos, e podemos representar o conjunto de todos os subconjuntos de um conjunto como uma lista de listas. Por exemplo, se o conjunto é (1 2 3), então o conjunto de todos os seus subconjuntos é (() (3) (2) (2 3) (1) (1 3) (1 2) (1 2 3)). Complete a seguinte definição de um procedimento que gera o conjunto dos subconjuntos de um conjunto e dê uma explicação clara de porque é que ele funciona. (define (subsets s) (if (null? s) (list ()) (let ((rest (subsets (cdr s)))) (append rest (map <??> rest))))) (define (subsets s) (if (null? s) (list ()) (let ((rest (subsets (cdr s)))) (append rest (map (lambda (x) (cons (car s) x)) rest))))) Exemplos: >(subsets (list 1 2 3)) (() (3) (2) (2 3) (1) (1 3) (1 2) (1 2 3))
11 Mais exercícios sobre listas Sumário Resumo
Exercícios Exercício 11.1 Considere que foi definido o tipo árvore binária. Para este tipo, estão definidas as operações: constroi-arvore que recebe a raiz, a árvore esquerda e a árvore direita e constrói a árvore correspondente. arvore-raiz que recebe uma árvore binária e retorna a sua raiz. arvore-esquerda que recebe uma árvore binária e retorna a sua árvore esquerda. arvore-direita que recebe uma árvore binária e retorna a sua árvore direita. arvore-vazia? que recebe um objecto e retorna verdadeiro se ele corresponder a uma árvore vazia e falso caso contrário. Com base nas operações descritas, escreva os seguintes procedimentos para percorrer árvores binárias: 1. percorre-inorder recebe uma árvore binária e retorna uma lista com todas as sua folhas, percorrendo primeiro a árvore esquerda, depois a raiz e depois a árvore direita da árvore inicial. 2. percorre-preorder recebe uma árvore binária e retorna uma lista com todas as sua folhas, percorrendo primeiro a raíz, depois a árvore esquerda e depois a árvore direita da árvore inicial. 3. percorre-posorder recebe uma árvore binária e retorna uma lista com todas as sua folhas, percorrendo primeiro a árvore esquerda, depois a árvore direita e depois a raiz da árvore inicial. Exercício 11.2 Uma árvore binária de procura é uma árvore binária em que todos os elementos que estão na sua árvore esquerda são menores que a raiz e todos os elementos que estão na sua árvore direita são maiores que a raiz. Com base nas operações definidas no exercício anterior, escreva os seguintes procedimentos: 1. insere-elemento que recebe um elemento e uma árvore binária de procura e o insere na árvore. 2. ordena-lista que recebe uma lista de elementos e retorna uma nova lista com os elementos ordenados. Exercício 11.3 Escreva um procedimento filtra que recebe um predicado e uma lista e retorna uma lista que contém apenas os elementos da lista inicial que satisfazem o predicado. Por exemplo:
(filtra even? (list 1 2 3 4 5)) (2 4) (filtra even? (list 1 3 5 7)) () (define (filtra pred lst) (cond ((null? lst) ()) ((pred (first lst)) (cons (first lst) (filtra pred (rest lst)))) (else (filtra pred (rest lst))))) Exercício 11.4 Escreva um procedimento todos? que recebe um predicado e uma lista e retorna verdadeiro se todos os elementos da lista satisfizerem o predicado e falso caso contrário. Por exemplo: (todos? even? (list 1 2 3 4 5)) #f (todos? even? (list 2 4 6)) #t (define (todos? pred lst) (cond ((null? lst) #t) ((pred (first lst)) (todos? pred (rest lst))) (else #f))) Exercício 11.5 Escreva um procedimento algum? que recebe um predicado e uma lista e retorna verdadeiro se algum dos elementos da lista satisfizer o predicado e falso caso contrário. Por exemplo: (algum? odd? (list 1 2 3 4 5)) #t (algum? odd? (list 2 4 6)) #f (define (algum? pred lst) (cond ((null? lst) #f) ((pred (first lst)) #t) (else (algum? pred (rest lst)))))
Exercício 11.6 Escreva um procedimento substitui que recebe dois elementos e uma lista e retorna uma outra lista que resulta de substituir todas as ocorrências do primeiro elemento pelo segundo na lista inicial. Por exemplo: (substitui 2 3 (list 1 2 3 2 5)) (1 3 3 3 5) (substitui 2 3 (list 1 3 5 7)) (1 3 5 7) (define (substitui velho novo lst) (cond ((null? lst) ()) ((= (first lst) velho) (cons novo (substitui velho novo (rest lst)))) (else (cons (first lst) (substitui velho novo (rest lst)))))) Exercício 11.7 Escreva um procedimento fold-right que recebe um procedimento de dois argumentos, o valor inicial de um acumulador e uma lista e retorna o resultado de aplicar o procedimento ao elemento inicial e ao resultado de aplicar o procedimento a todos os elementos que estão à sua direita. Quando a lista for vazia, este procedimento deve retornar o valor inicial. Por exemplo: (fold-right + 0 (list 1 2 3 4)) 10 (fold-right + 0 ()) 0 (define (fold-right proc inicial lst) (if (null? lst) inicial (proc (first lst) (fold-right proc inicial (rest lst))))) Exercício 11.8 Com base no procedimento fold-right escreva os seguintes procedimentos: 1. multiplica-lista que recebe uma lista e retorna o produto de todos os seus elementos. 2. maximo-lista que recebe uma lista e retorna o maior dos seus elementos. 3. inverte-lista que recebe uma lista e retorna outra lista com os elementos da lista inicial pela ordem inversa.
4. junta-listas que recebe duas listas e retorna outra lista que resulta de juntar as duas. 1. (define (multiplica-lista lst) (if (null? lst) (error "multiplica-lista: espera uma lista com pelo menos 1 elemento, recebeu" lst) (fold-right * 1 lst))) (multiplica-lista (list 1 2 3 4)) 24 (multiplica-lista ()) multiplica-lista: espera uma lista com pelo menos 1 elemento, recebeu () 2. (define (maximo-lista lst) (if (null? lst) (error "maximo-lista: espera uma lista com pelo menos 1 elemento, recebeu" lst) (fold-right max (first lst) (rest lst)))) (maximo-lista (list 1 2 3 4)) 4 (maximo-lista ()) maximo-lista: espera uma lista com pelo menos 1 elemento, recebeu () 3. (define (inverte-lista lst) (fold-right (lambda (x y) (append y (list x))) () lst)) (inverte-lista (list 1 2 3 4)) (4 3 2 1) (inverte-lista ()) () 4. (define (junta-listas lst1 lst2) (fold-right cons lst2 lst1)) (junta-listas (list 1 2 3 4) (list 5 6 7)) (1 2 3 4 5 6 7) (junta-listas () (list 5 6 7)) (5 6 7) Exercício 11.9 Uma forma de compactar listas de números é, dada uma lista de números (possivelmente repetidos), transformá-la numa lista em que ocorrências consecutivas de um mesmo número são substituídas por um par, em que o primeiro elemento é o número de vezes que o número aparece repetido e o segundo elemento é o número. Escreva o procedimento run-length-encode que compacta listas de inteiros. Por exemplo,
(run-length-encode (1 1 1 1 1 1 1 2 3 3 3 3 4 4 4 4 1 3 3 3 3)) ((7. 1) 2 (4. 3) (4. 4) 1 (4. 3)) (run-length-encode (1 2 1 2 3 3 3 3 4 4 4 4 1 1 3 3 3 3 3)) (1 2 1 2 (4. 3) (4. 4) (2. 1) (5. 3)) Repare que as sequências de apenas um elemento não são substituidas. Depois de ter uma lista compactada, pode ser necessário saber qual era a lista original. Escreva o procedimento run-length-decode que, dada uma lista de inteiros compactada, retorna a lista original. Por exemplo, (run-length-decode ((7. 1) 2 (4. 3) (4. 4) 1 (4. 3))) (1 1 1 1 1 1 1 2 3 3 3 3 4 4 4 4 1 3 3 3 3) (run-length-decode (1 2 1 2 (4. 3) (4. 4) (2. 1) (5. 3))) (1 2 1 2 3 3 3 3 4 4 4 4 1 1 3 3 3 3 3) (define (run-length-encode lst) (define (encode-seq lst last count) (define (make-code last count) (if (> count 1) (cons count last) last)) (cond ((null? lst) (list (make-code last count))) ((= last (first lst)) (encode-seq (rest lst) last (add1 count))) (else (cons (make-code last count) (encode-seq (rest lst) (first lst) 1))))) (if (null? lst) () (encode-seq (rest lst) (first lst) 1))) (define (run-length-decode encoded-lst) (define (decode code) (define (make-list count elem) (if (zero? count) () (cons elem (make-list (sub1 count) elem)))) (if (pair? code) (make-list (car code) (cdr code)) (list code))) (if (null? encoded-lst) () (append (decode (first encoded-lst)) (run-length-decode (rest encoded-lst)))))
12 Quote Sumário Resumo
Exercícios Exercício 12.1 (Livro exemplo das páginas 143-4) Considere que foram feitas as definições: (define a 1) (define b 2) Diga qual o valor de cada uma das seguintes expressões: (list a b) (list a b) (list a b) (first (a b c)) (rest (a b c)) (list a b) (1 2) (list a b) (a b) (list a b) (a 2) (first (a b c)) a (rest (a b c)) (b c) Exercício 12.2 (Livro exemplo da página 144) Defina o procedimento memq, que recebe um símbolo e uma lista e retorna falso se o símbolo não estiver contido na lista (isto é, não for eq? a nenhum dos elementos da lista) e a sublista que começa com a primeira ocorrência do símbolo na lista caso contrário. Por exemplo, (memq apple (pear banana prune)) #f (memq apple (x (apple sauce) y apple pear)) (apple pear) (define (memq item lst) (cond ((null? lst) #f) ((eq? item (first lst)) lst) (else (memq item (rest lst)))))
Exercício 12.3 (Livro 2.53) O que é que o interpretador de Scheme imprime como resposta à avaliação de cada uma das seguintes expressões: (list a b c) (list (list george)) (cdr ((x1 x2) (y1 y2))) (cadr ((x1 x2) (y1 y2))) (pair? (car (a short list))) (memq red ((red shoes) (blue socks))) (memq red (red shoes blue socks)) (list a b c) (a b c) (list (list george)) ((george)) (cdr ((x1 x2) (y1 y2))) ((y1 y2)) (cadr ((x1 x2) (y1 y2))) (y1 y2) (pair? (car (a short list))) #f (memq red ((red shoes) (blue socks))) #f (memq red (red shoes blue socks)) (red shoes blue socks) Exercício 12.4 (Livro 2.54) Duas listas são equal? se contiverem elementos iguais e estes estiverem pela mesma ordem. Por exemplo, (equal? (this is a list) (this is a list)) é verdade, mas (equal? (this is a list) (this (is a) list)) é falso. Para sermos mais precisos, podemos definir equal? recursivamente em termos da igualdade básica entre símbolos eq?, dizendo que a e b são equal? se forem ambos símbolos e forem eq? ou forem ambos listas em que (first a) é equal? a (first b) e (rest a) é equal? a (rest b). Usando esta ideia, implemente equal? como um procedimento.
(define (equal? l1 l2) (cond ((and (number? l1) (number? l2)) (= l1 l2)) ((and (symbol? l1) (symbol? l2)) (eq? l1 l2)) ((and (null? l1) (null? l2)) #t) ((or (atom? l1) (atom? l2)) #f) (else (and (equal? (first l1) (first l2)) (equal? (rest l1) (rest l2)))))) (equal? (this is a list) (this is a list)) #t (equal? (this is a list) (this (is a) list)) #f (equal? (2 3 a 4) (2 3 a 4)) #t (equal? (2 3 a 4) (2 3)) #f Exercício 12.5 (Livro 2.55) O resultado de avaliar a expressão (first abracadabra) é quote. Explique porquê. A expansão de (first abracadabra) é (first (quote (quote abracadabra))). Para avaliar a segunda expressão: 1. Avaliamos o operador. Neste caso, first, que tem como resultado o procedimento que dado uma lista retorna o seu primeiro argumento. 2. Avaliamos os seus argumentos, por qualquer ordem. Neste caso, avaliar a expressão (quote (quote abracadabra)) tem como resultado a lista (quote abracadabra). 3. Aplicamos o operador aos valores dos argumentos e retornamos o resultado. Neste caso, aplicar o procedimento first à lista (quote abracadabra) tem como resultado o primeiro elemento da lista, que é o símbolo quote.
13 Modularidade, objectos e estado Sumário Resumo
Exercícios Exercício 13.1 Diga o que é impresso pelo interpretador de Scheme ao avaliar cada uma das seguintes expressões: (define a 3) (set! a "ola") (+ a 1) (begin (let ((a 5)) (+ a (* 45 327)) (sqrt (length (1 a b "bom dia" (2 5) 3)))) (display a) a) (set! c 78) >(define a 3) >(set! a "ola") >(+ a 1) +: expects type <number> as 1st argument, given: "ola"; other arguments were: 1 >(begin (let ((a 5)) (+ a (* 45 327)) (sqrt (length (1 a b "bom dia" (2 5) 3)))) (display a) a) a "ola" >(set! c 78) set!: cannot set undefined identifier: c Exercício 13.2 (Livro 3.1) Um acumulador é um procedimento que é chamado repetidamente com apenas um argumento numérico e acumula os seus argumentos numa soma. De cada vez que é chamado, retorna a soma acumulada até ao momento. Escreva um procedimento make-accumulator que gera acumuladores, cada um dos quais mantendo uma soma independente. O valor de entrada para o procedimento make-accumulator deve especificar o valor inicial da soma. Por exemplo,
(define A (make-accumulator 5)) (A 10) 15 (A 10) 25 (define (make-accumulator init) (lambda (num) (set! init (+ init num)) init)) Exercício 13.3 (Livro 3.2) Em aplicações para testar software, é útil ser capaz de contar o número de vezes que um procedimento é chamado durante o decurso de uma computação. Escreva um procedimento make-monitored que recebe um procedimento f como argumento, que por sua vez é um procedimento de um argumento. O resultado retornado pelo procedimento make-monitored é um terceiro procedimento mf que mantém um registo do número de vezes que foi chamado através de um contador interno. Se o valor de entrada para mf for o símbolo how-many-calls?, então mf deve retornar o valor do contador. Se o valor de entrada for o símbolo reset-count, então mf deve inicializar o contador a zero. Para qualquer outro valor de entrada, mf retorna o valor de aplicar f a esse valor e incrementa o contador. Por exemplo, podemos criar uma versão monitorizada do procedimento sqrt: (define s (make-monitored sqrt)) (s 100) 10 (s how-many-calls?) 1 (define (make-monitored proc) (let ((calls 0)) (lambda (input) (cond ((eq? input how-many-calls?) calls) ((eq? input reset-count) (set! calls 0)) (else (set! calls (add1 calls)) (proc input)))))) Exercício 13.4 Desenhe o diagrama dos ambientes criados pelos seguintes exemplos de código:
1. (define x 63) (define square (lambda (x) (* x x))) (define sum-sq (lambda (x y) (+ (square x) (square y)))) (sum-sq 3 4) O resultado é 25. 2. (define (make-adder n) (lambda (x) (+ x n))) (define addthree (make-adder 3)) (define addfive (make-adder 5)) (addfive 7) (addthree 7) 3. Os ambientes permitem-nos perceber como é que podemos usar procedimentos como representações para tipos abstractos de dados. Por exemplo, podemos criar rectângulos da seguinte forma: (define (make-rect w h) (define (dispatch op) (cond ((eq? op width) w) ((eq? op height) h) ((eq? op area) (* w h)) ((eq? op perimeter) (* 2 (+ w h))) (else (error "rectangle: non-existent operation" op)))) dispatch) (define r1 (make-rect 5 30)) (r1 height) Exercício 13.5 Introduzir a forma especial set! na nossa linguagem obriga-nos a pensar no significado de igualdade e mudança. Dê exemplos de procedimentos simples que sejam: 1. Um procedimento referencialmente transparente
2. Um procedimento referencialmente opaco (não transparente) 1. (define (soma-transparente inicial) (lambda (num) (+ inicial num))) (define st1 (soma-transparente 25)) (define st2 (soma-transparente 25)) (st1 20) 45 (st1 20) 45 (st2 20) 45 Neste caso, podemos dizer que st1 e st2 são iguais, pois um pode ser substituido pelo outro em qualquer lugar da computação sem alterar o resultado. Uma linguagem que suporta o conceito de que iguais podem ser substituidos por iguais numa expressão sem alterar o valor da expressão diz-se referencialmente transparente. 2. A transparência referencial é violada quando introduzimos set! na linguagem, o que dificulta a verificação de quando é que podemos simplificar expressões substituindo-as por outras equivalentes. (define (soma-opaco inicial) (lambda (num) (set! inicial (+ inicial num)) inicial)) (define so1 (soma-opaco 25)) (define so2 (soma-opaco 25)) (so1 20) 45 (so1 20) 65 (so2 20) 45